From 2a0d45d775394f7b69d81ba0c5ffe9d45512a4de Mon Sep 17 00:00:00 2001 From: Nino Kodabande Date: Thu, 24 Oct 2024 11:48:20 -0700 Subject: [PATCH] Subscribe to container engine API for published ports Monitor container creation and deletion events by subscribing to the container engine's API. Upon receiving a container creation or deletion event, the system immediately forwards the port mappings through the aggregated channel. This ensures that the ports are opened on the host without any latency. Signed-off-by: Nino Kodabande --- cmd/lima-guestagent/daemon_linux.go | 33 +- cmd/lima-guestagent/install_systemd_linux.go | 34 +- go.mod | 54 ++- go.sum | 163 +++++++- .../boot/25-guestagent-base.sh | 33 +- pkg/cidata/cidata.TEMPLATE.d/lima.env | 3 + pkg/cidata/cidata.go | 5 + pkg/cidata/template.go | 8 + pkg/guestagent/events/containerd_linux.go | 364 ++++++++++++++++++ pkg/guestagent/events/containerd_others.go | 16 + pkg/guestagent/events/docker_linux.go | 228 +++++++++++ pkg/guestagent/events/docker_others.go | 16 + pkg/guestagent/events/eventutils.go | 38 ++ pkg/guestagent/events/kubernetes_linux.go | 286 ++++++++++++++ pkg/guestagent/events/kubernetes_others.go | 16 + pkg/guestagent/guestagent_linux.go | 91 +++-- pkg/limatype/lima_yaml.go | 17 + pkg/limayaml/defaults.go | 48 +++ pkg/limayaml/defaults_test.go | 19 +- pkg/limayaml/validate.go | 52 +++ pkg/portfwd/listener.go | 3 +- templates/default.yaml | 14 + templates/docker-rootful.yaml | 4 + templates/docker.yaml | 4 + templates/k0s.yaml | 4 + templates/k3s.yaml | 4 + templates/k8s.yaml | 4 + 27 files changed, 1501 insertions(+), 60 deletions(-) create mode 100644 pkg/guestagent/events/containerd_linux.go create mode 100644 pkg/guestagent/events/containerd_others.go create mode 100644 pkg/guestagent/events/docker_linux.go create mode 100644 pkg/guestagent/events/docker_others.go create mode 100644 pkg/guestagent/events/eventutils.go create mode 100644 pkg/guestagent/events/kubernetes_linux.go create mode 100644 pkg/guestagent/events/kubernetes_others.go diff --git a/cmd/lima-guestagent/daemon_linux.go b/cmd/lima-guestagent/daemon_linux.go index 748196f0c52..350f2ac8432 100644 --- a/cmd/lima-guestagent/daemon_linux.go +++ b/cmd/lima-guestagent/daemon_linux.go @@ -28,16 +28,25 @@ func newDaemonCommand() *cobra.Command { daemonCommand.Flags().Duration("tick", 3*time.Second, "Tick for polling events") daemonCommand.Flags().Int("vsock-port", 0, "Use vsock server instead a UNIX socket") daemonCommand.Flags().String("virtio-port", "", "Use virtio server instead a UNIX socket") + daemonCommand.Flags().StringSlice("docker-sockets", []string{}, "Paths to Docker socket files to monitor for exposed ports") + daemonCommand.Flags().StringSlice("containerd-sockets", []string{}, "Paths to Containerd socket files to monitor for exposed ports") + daemonCommand.Flags().StringSlice("kubernetes-configs", []string{}, "Path to Kubernetes config file to monitor for ports") return daemonCommand } func daemonAction(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() + if os.Geteuid() != 0 { + return errors.New("must run as the root user") + } socket := "/run/lima-guestagent.sock" tick, err := cmd.Flags().GetDuration("tick") if err != nil { return err } + if tick == 0 { + return errors.New("tick must be specified") + } vSockPort, err := cmd.Flags().GetInt("vsock-port") if err != nil { return err @@ -46,12 +55,19 @@ func daemonAction(cmd *cobra.Command, _ []string) error { if err != nil { return err } - if tick == 0 { - return errors.New("tick must be specified") + dockerSockets, err := cmd.Flags().GetStringSlice("docker-sockets") + if err != nil { + return err } - if os.Geteuid() != 0 { - return errors.New("must run as the root user") + containerdSockets, err := cmd.Flags().GetStringSlice("containerd-sockets") + if err != nil { + return err } + kubernetesConfig, err := cmd.Flags().GetStringSlice("kubernetes-configs") + if err != nil { + return err + } + logrus.Infof("event tick: %v", tick) newTicker := func() (<-chan time.Time, func()) { @@ -62,7 +78,14 @@ func daemonAction(cmd *cobra.Command, _ []string) error { return ticker.C, ticker.Stop } - agent, err := guestagent.New(ctx, newTicker, tick*20) + agent, err := guestagent.New( + &guestagent.Config{ + Ticker: newTicker, + IptablesIdle: tick * 20, + DockerSockets: dockerSockets, + ContainerdSockets: containerdSockets, + KubernetesConfigs: kubernetesConfig, + }) if err != nil { return err } diff --git a/cmd/lima-guestagent/install_systemd_linux.go b/cmd/lima-guestagent/install_systemd_linux.go index 91ff53b19b5..eaf4ba99777 100644 --- a/cmd/lima-guestagent/install_systemd_linux.go +++ b/cmd/lima-guestagent/install_systemd_linux.go @@ -26,6 +26,9 @@ func newInstallSystemdCommand() *cobra.Command { } installSystemdCommand.Flags().Int("vsock-port", 0, "Use vsock server on specified port") installSystemdCommand.Flags().String("virtio-port", "", "Use virtio server instead a UNIX socket") + installSystemdCommand.Flags().StringSlice("docker-sockets", []string{}, "Paths to Docker socket files to monitor for exposed ports") + installSystemdCommand.Flags().StringSlice("containerd-sockets", []string{}, "Paths to Containerd socket files to monitor for exposed ports") + installSystemdCommand.Flags().StringSlice("kubernetes-configs", []string{}, "Path to Kubernetes config files to monitor for ports") return installSystemdCommand } @@ -43,7 +46,25 @@ func installSystemdAction(cmd *cobra.Command, _ []string) error { if err != nil { return err } - unit, err := generateSystemdUnit(vsockPort, virtioPort, debug) + dockerSockets, err := cmd.Flags().GetStringSlice("docker-sockets") + if err != nil { + return err + } + containerdSockets, err := cmd.Flags().GetStringSlice("containerd-sockets") + if err != nil { + return err + } + kubernetesConfigs, err := cmd.Flags().GetStringSlice("kubernetes-configs") + if err != nil { + return err + } + unit, err := generateSystemdUnit( + vsockPort, + virtioPort, + dockerSockets, + containerdSockets, + kubernetesConfigs, + debug) if err != nil { return err } @@ -82,7 +103,7 @@ func installSystemdAction(cmd *cobra.Command, _ []string) error { //go:embed lima-guestagent.TEMPLATE.service var systemdUnitTemplate string -func generateSystemdUnit(vsockPort int, virtioPort string, debug bool) ([]byte, error) { +func generateSystemdUnit(vsockPort int, virtioPort string, dockerSockets, containerdSockets, kubeConfigs []string, debug bool) ([]byte, error) { selfExeAbs, err := os.Executable() if err != nil { return nil, err @@ -98,6 +119,15 @@ func generateSystemdUnit(vsockPort int, virtioPort string, debug bool) ([]byte, if debug { args = append(args, "--debug") } + if len(dockerSockets) > 0 { + args = append(args, "--docker-sockets", strings.Join(dockerSockets, ",")) + } + if len(containerdSockets) > 0 { + args = append(args, "--containerd-sockets", strings.Join(containerdSockets, ",")) + } + if len(kubeConfigs) > 0 { + args = append(args, "--kubernetes-configs", strings.Join(kubeConfigs, ",")) + } m := map[string]string{ "Binary": selfExeAbs, diff --git a/go.mod b/go.mod index bce4a3c3224..6a15a4a89ab 100644 --- a/go.mod +++ b/go.mod @@ -17,10 +17,13 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.7 github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 github.com/diskfs/go-diskfs v1.7.0 // gomodjail:unconfined + github.com/docker/docker v28.3.3+incompatible + github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/elastic/go-libaudit/v2 v2.6.2 github.com/foxcpp/go-mockdns v1.1.0 github.com/goccy/go-yaml v1.18.0 + github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.7.0 github.com/google/yamlfmt v0.17.2 github.com/invopop/jsonschema v0.13.0 @@ -63,6 +66,7 @@ require ( github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/creack/pty v1.1.18 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e // indirect github.com/dimchansky/utfbom v1.1.1 // indirect @@ -70,7 +74,6 @@ require ( github.com/elliotchance/orderedmap v1.8.0 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fatih/color v1.18.0 // indirect - // gomodjail:unconfined github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -78,7 +81,7 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/gopacket v1.1.19 // indirect @@ -101,7 +104,11 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runtime-spec v1.1.0 // indirect + github.com/opencontainers/selinux v1.11.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -113,11 +120,15 @@ require ( github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/term v0.35.0 // indirect - golang.org/x/time v0.9.0 // indirect + golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect @@ -141,3 +152,40 @@ require ( sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) + +require github.com/containerd/containerd v1.7.28 + +require ( + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect + github.com/Microsoft/hcsshim v0.11.7 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/containerd/containerd/api v1.9.0 + github.com/containerd/errdefs v0.3.0 // indirect + github.com/containerd/fifo v1.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/containerd/ttrpc v1.2.7 // indirect + github.com/containerd/typeurl/v2 v2.2.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/signal v0.7.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect + google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect +) + +require ( + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect +) diff --git a/go.sum b/go.sum index 013b0d6e78e..88cf1a363c2 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,23 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= 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/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= +github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= 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= @@ -32,12 +42,36 @@ github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0 github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI= github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/containerd v1.7.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c= +github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= +github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= +github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= +github.com/containerd/errdefs v0.3.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/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= +github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= 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/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= +github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= +github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= github.com/containers/gvisor-tap-vsock v0.8.7 h1:mFMMU5CIXO9sbtsgECc90loUHx15km3AN6Zuhg3X4qM= github.com/containers/gvisor-tap-vsock v0.8.7/go.mod h1:Rf2gm4Lpac0IZbg8wwQDh7UuKCxHmnxar0hEZ08OXY8= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= @@ -45,8 +79,9 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V 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/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/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= @@ -59,10 +94,18 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8= github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 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/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= 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/elastic/go-libaudit/v2 v2.6.2 h1:1PM6wVBTJHJQYsKl8jfA9/Aw9pFty5uUezPiUfKtOI4= @@ -75,8 +118,14 @@ github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1Ugj github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +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.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -86,6 +135,7 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 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= @@ -106,12 +156,32 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -122,10 +192,13 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 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.17.2 h1:TkXxhmj7dnpmOnlWGOXog92Gs6MWcTZqnf3kuyp8yFQ= github.com/google/yamlfmt v0.17.2/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= 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= @@ -191,12 +264,32 @@ github.com/mikefarah/yq/v4 v4.47.2 h1:Jb5fHlvgK5eeaPbreG9UJs1E5w6l5hUzXjeaY6LTTW github.com/mikefarah/yq/v4 v4.47.2/go.mod h1:ulYbZUzGJsBDDwO5ohvk/KOW4vW5Iddd/DBeAY1Q09g= 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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= +github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= @@ -209,6 +302,12 @@ github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= 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/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= 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.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -225,6 +324,9 @@ github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -249,6 +351,7 @@ 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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -256,6 +359,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= @@ -272,10 +376,18 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +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.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= @@ -284,6 +396,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= @@ -302,6 +416,10 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -315,10 +433,15 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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= @@ -331,8 +454,11 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -347,6 +473,7 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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= @@ -356,6 +483,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w 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-20211025201205-69cdffdb9359/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -397,9 +525,13 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -418,10 +550,33 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +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 v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +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.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +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.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -442,6 +597,8 @@ 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= k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh index 182dd3ac2c0..35f17a3a76d 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh @@ -41,7 +41,12 @@ name="lima-guestagent" description="Forward ports to the lima-hostagent" command=${LIMA_CIDATA_GUEST_INSTALL_PREFIX}/bin/lima-guestagent -command_args="daemon --debug=${LIMA_CIDATA_DEBUG} --vsock-port \"${LIMA_CIDATA_VSOCK_PORT}\" --virtio-port \"${LIMA_CIDATA_VIRTIO_PORT}\"" +command_args="daemon --debug=${LIMA_CIDATA_DEBUG} \ +--docker-sockets \"${LIMA_CIDATA_PORT_MONITOR_DOCKER}\" \ +--containerd-sockets \"${LIMA_CIDATA_PORT_MONITOR_CONTAINERD}\" \ +--kubernetes-configs \"${LIMA_CIDATA_PORT_MONITOR_KUBERNETES}\" \ +--vsock-port \"${LIMA_CIDATA_VSOCK_PORT}\" \ +--virtio-port \"${LIMA_CIDATA_VIRTIO_PORT}\"" command_background=true pidfile="/run/lima-guestagent.pid" EOF @@ -53,11 +58,29 @@ else # Remove legacy systemd service rm -f "${LIMA_CIDATA_HOME}/.config/systemd/user/lima-guestagent.service" + docker_args="--docker-sockets=${LIMA_CIDATA_PORT_MONITOR_DOCKER}" + containerd_args="--containerd-sockets=${LIMA_CIDATA_PORT_MONITOR_CONTAINERD}" + kubernetes_args="--kubernetes-configs=${LIMA_CIDATA_PORT_MONITOR_KUBERNETES}" + if [ "${LIMA_CIDATA_VSOCK_PORT}" != "0" ]; then - sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd --debug="${LIMA_CIDATA_DEBUG}" --vsock-port "${LIMA_CIDATA_VSOCK_PORT}" - elif [ "${LIMA_CIDATA_VIRTIO_PORT}" != "" ]; then - sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd --debug="${LIMA_CIDATA_DEBUG}" --virtio-port "${LIMA_CIDATA_VIRTIO_PORT}" + sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}/bin/lima-guestagent" install-systemd \ + --debug="${LIMA_CIDATA_DEBUG}" \ + "${docker_args}" \ + "${containerd_args}" \ + "${kubernetes_args}" \ + --vsock-port "${LIMA_CIDATA_VSOCK_PORT}" + elif [ -n "${LIMA_CIDATA_VIRTIO_PORT}" ]; then + sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}/bin/lima-guestagent" install-systemd \ + --debug="${LIMA_CIDATA_DEBUG}" \ + "${docker_args}" \ + "${containerd_args}" \ + "${kubernetes_args}" \ + --virtio-port "${LIMA_CIDATA_VIRTIO_PORT}" else - sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd --debug="${LIMA_CIDATA_DEBUG}" + sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}/bin/lima-guestagent" install-systemd \ + --debug="${LIMA_CIDATA_DEBUG}" \ + "${docker_args}" \ + "${containerd_args}" \ + "${kubernetes_args}" fi fi diff --git a/pkg/cidata/cidata.TEMPLATE.d/lima.env b/pkg/cidata/cidata.TEMPLATE.d/lima.env index e08129b5a22..f435eebe33e 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/lima.env +++ b/pkg/cidata/cidata.TEMPLATE.d/lima.env @@ -60,6 +60,9 @@ LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION= LIMA_CIDATA_VMTYPE={{ .VMType }} LIMA_CIDATA_VSOCK_PORT={{ .VSockPort }} LIMA_CIDATA_VIRTIO_PORT={{ .VirtioPort}} +LIMA_CIDATA_PORT_MONITOR_DOCKER={{ .PortMonitor.Docker }} +LIMA_CIDATA_PORT_MONITOR_CONTAINERD={{ .PortMonitor.Containerd }} +LIMA_CIDATA_PORT_MONITOR_KUBERNETES={{ .PortMonitor.Kubernetes }} {{- if .Plain}} LIMA_CIDATA_PLAIN=1 {{- else}} diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go index 4a38c0c2fbf..a2055fb3df0 100644 --- a/pkg/cidata/cidata.go +++ b/pkg/cidata/cidata.go @@ -144,6 +144,11 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i Plain: *instConfig.Plain, TimeZone: *instConfig.TimeZone, Param: instConfig.Param, + PortMonitor: PortMonitor{ + Docker: strings.Join(instConfig.PortMonitors.Docker.Sockets, ","), + Containerd: strings.Join(instConfig.PortMonitors.Containerd.Sockets, ","), + Kubernetes: strings.Join(instConfig.PortMonitors.Kubernetes.Configs, ","), + }, } if instConfig.VMOpts.VZ.Rosetta.Enabled != nil { diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go index 3d886e7a5e6..8752c3cf1c9 100644 --- a/pkg/cidata/template.go +++ b/pkg/cidata/template.go @@ -73,6 +73,13 @@ type Disk struct { FSType string FSArgs []string } + +type PortMonitor struct { + Docker string + Containerd string + Kubernetes string +} + type TemplateArgs struct { Debug bool Name string // instance name @@ -114,6 +121,7 @@ type TemplateArgs struct { VirtioPort string Plain bool TimeZone string + PortMonitor PortMonitor } func ValidateTemplateArgs(args *TemplateArgs) error { diff --git a/pkg/guestagent/events/containerd_linux.go b/pkg/guestagent/events/containerd_linux.go new file mode 100644 index 00000000000..21c3bf9351d --- /dev/null +++ b/pkg/guestagent/events/containerd_linux.go @@ -0,0 +1,364 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "sync" + "time" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/api/events" + "github.com/containerd/containerd/errdefs" + ctrns "github.com/containerd/containerd/namespaces" + "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" + + "github.com/lima-vm/lima/v2/pkg/guestagent/api" +) + +const ( + stateKey = "nerdctl/state-dir" + portsKey = "nerdctl/ports" + namespaceKey = "nerdctl/namespace" + defaultSocketTimeout = 5 * time.Second +) + +type ipPortMap map[string][]*api.IPPort + +type ContainerdEventMonitor struct { + socketPaths []string +} + +func NewContainerdEventMonitor(socketPaths []string) *ContainerdEventMonitor { + return &ContainerdEventMonitor{ + socketPaths: socketPaths, + } +} + +func (c *ContainerdEventMonitor) MonitorPorts(ctx context.Context, ch chan *api.Event) { + const defaultRetryDelay = 2 + retryDelay := 0 + var wg sync.WaitGroup + for _, socket := range c.socketPaths { + wg.Add(1) + go func(socket string) { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(retryDelay) * time.Second): + logrus.Debugf("attempting to connect to containerd socket %s after: %d", socket, retryDelay) + retryDelay = defaultRetryDelay + } + info, err := os.Stat(socket) + if err != nil { + if os.IsNotExist(err) { + logrus.Warnf("containerd socket %s does not exist", socket) + } else { + logrus.Errorf("failed to stat containerd socket %s: %v", socket, err) + } + retryDelay = defaultRetryDelay + continue + } + if info.IsDir() { + logrus.Errorf("containerd socket path %s is a directory", socket) + retryDelay = 15 + continue + } + client, err := containerd.New(socket, containerd.WithDefaultNamespace(ctrns.Default)) + if err != nil { + logrus.Warnf("failed to create client for socket %s: %v", socket, err) + continue + } + logrus.Debugf("created containerd client for socket %s", socket) + clientCtx, cancel := context.WithTimeout(ctx, defaultSocketTimeout) + serving, serveErr := client.IsServing(clientCtx) + cancel() + if serveErr != nil || !serving { + logrus.Warnf("containerd daemon not serving on socket %s: %v. Retrying in 5s...", socket, serveErr) + client.Close() + retryDelay = defaultRetryDelay + continue + } + logrus.Infof("successfully connected to containerd on socket %s", socket) + if err := runMonitorClient(ctx, client, ch); err != nil { + logrus.Errorf("containerd port monitoring for socket: %s failed: %s", socket, err) + } + client.Close() + } + }(socket) + } + wg.Wait() +} + +func runMonitorClient(ctx context.Context, client *containerd.Client, ch chan *api.Event) error { + runningContainers := make(ipPortMap) + subscribeFilters := []string{ + `topic=="/tasks/start"`, + `topic=="/containers/update"`, + `topic=="/tasks/exit"`, + } + msgCh, errCh := client.Subscribe(ctx, subscribeFilters...) + + if err := initializeRunningContainers(ctx, client, ch, runningContainers); err != nil { + return fmt.Errorf("failed to initialize existing containers published ports: %w", err) + } + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("context cancellation: %w", ctx.Err()) + + case err := <-errCh: + return fmt.Errorf("receiving container event failed: %w", err) + + case envelope := <-msgCh: + logrus.Debugf("received an event: %+v", envelope.Topic) + switch envelope.Topic { + case "/tasks/start": + taskStart := &events.TaskStart{} + err := proto.Unmarshal(envelope.Event.GetValue(), taskStart) + if err != nil { + logrus.Errorf("failed to unmarshal TaskStart event: %v", err) + continue + } + + ipPorts, err := createIPPort(ctx, client, envelope.Namespace, taskStart.ContainerID) + if err != nil { + logrus.Errorf("creating IPPorts for start task ContainerID=%s failed: %s", taskStart.ContainerID, err) + continue + } + + logrus.Debugf("received the following TaskStart: ContainerID=%s ipPorts=%+v", taskStart.ContainerID, ipPorts) + + if len(ipPorts) != 0 { + sendHostAgentEvent(false, ipPorts, ch) + runningContainers[taskStart.ContainerID] = ipPorts + } + + case "/containers/update": + cuEvent := &events.ContainerUpdate{} + err := proto.Unmarshal(envelope.Event.GetValue(), cuEvent) + if err != nil { + logrus.Errorf("failed to unmarshal container update event: %v", err) + continue + } + + ipPorts, err := createIPPort(ctx, client, envelope.Namespace, cuEvent.ID) + if err != nil { + logrus.Errorf("creating IPPorts, for the following exit task: %v failed: %s", cuEvent, err) + continue + } + + logrus.Debugf("received the following updateTask: %v for: %v", cuEvent, ipPorts) + + if existingipPorts, ok := runningContainers[cuEvent.ID]; ok { + if !ipPortsEqual(ipPorts, existingipPorts) { + // first remove the existing entry + sendHostAgentEvent(true, existingipPorts, ch) + // then update with the new entry + sendHostAgentEvent(false, ipPorts, ch) + runningContainers[cuEvent.ID] = ipPorts + } + } + case "/tasks/exit": + exitTask := &events.TaskExit{} + err := proto.Unmarshal(envelope.Event.GetValue(), exitTask) + if err != nil { + logrus.Errorf("failed to unmarshal container's exit task: %v", err) + continue + } + + container, err := client.LoadContainer(ctx, exitTask.ContainerID) + if err != nil { + if errdefs.IsNotFound(err) { + logrus.Debugf("container: %s in namespace: %s not found, deleting port mapping", exitTask.ContainerID, envelope.Namespace) + deleteRunningContainer(exitTask.ContainerID, ch, runningContainers) + continue + } + logrus.Errorf("failed to get the container %s from namespace %s: %s", exitTask.ContainerID, envelope.Namespace, err) + continue + } + + tsk, err := container.Task(ctx, nil) + if err != nil { + if errdefs.IsNotFound(err) { + logrus.Debugf("task for container %s in namespace %s not found, deleting port mapping", exitTask.ContainerID, envelope.Namespace) + deleteRunningContainer(exitTask.ContainerID, ch, runningContainers) + continue + } + logrus.Errorf("failed to get the task for container %s: %s", exitTask.ContainerID, err) + continue + } + status, err := tsk.Status(ctx) + if err != nil { + logrus.Errorf("failed to get the task status for container %s: %s", exitTask.ContainerID, err) + continue + } + + if status.Status == containerd.Running { + logrus.Debugf("container %s is still running, but received exit event with status %d", exitTask.ContainerID, exitTask.ExitStatus) + continue + } + + deleteRunningContainer(exitTask.ContainerID, ch, runningContainers) + } + } + } +} + +func deleteRunningContainer(containerID string, ch chan *api.Event, runningContainers ipPortMap) { + if ipPorts, ok := runningContainers[containerID]; ok { + delete(runningContainers, containerID) + logrus.Debugf("deleted container %s from running containers", containerID) + sendHostAgentEvent(true, ipPorts, ch) + } else { + logrus.Debugf("container %s not found in running containers", containerID) + } +} + +func initializeRunningContainers(ctx context.Context, client *containerd.Client, ch chan *api.Event, runningContainers ipPortMap) error { + containers, err := client.Containers(ctx) + if err != nil { + return err + } + + for _, container := range containers { + task, err := container.Task(ctx, nil) + if err != nil || task == nil { + logrus.Errorf("failed getting container %s task: %s", container.ID(), err) + continue + } + + status, err := task.Status(ctx) + if err != nil || status.Status != containerd.Running { + logrus.Errorf("failed getting container %s task status: %s", container.ID(), err) + continue + } + + labels, err := container.Labels(ctx) + if err != nil { + logrus.Errorf("failed getting container %s labels: %s", container.ID(), err) + continue + } + + namespace, ok := labels[namespaceKey] + if !ok { + logrus.Errorf("container %s does not have a namespace label", container.ID()) + continue + } + ipPorts, err := createIPPort(ctx, client, namespace, container.ID()) + if err != nil { + logrus.Errorf("creating IPPorts, while initializing containers the following: %v failed: %s", container.ID(), err) + continue + } + + sendHostAgentEvent(false, ipPorts, ch) + runningContainers[container.ID()] = ipPorts + } + + return nil +} + +func createIPPort(ctx context.Context, client *containerd.Client, namespace, containerID string) ([]*api.IPPort, error) { + container, err := client.ContainerService().Get( + ctrns.WithNamespace(ctx, namespace), containerID) + if err != nil { + return nil, err + } + + var ipPorts []*api.IPPort + + // For backward compatibility, we first check if the container has the nerdctl/ports label. + // If it does, we parse it and return the IPPorts. + containerPorts, ok := container.Labels[portsKey] + if ok { + ipPorts, err = extractIPPortsFromLabel(containerPorts) + if err != nil { + return nil, fmt.Errorf("extracting IPPorts from container %s ports label failed: %w", containerID, err) + } + return ipPorts, nil + } + // If the label is not present, we check the network config in the following path: + // //containers///network-config.json + stateDir, ok := container.Labels[stateKey] + if !ok { + return nil, fmt.Errorf("container %s does not have a state directory label", containerID) + } + content, err := os.ReadFile(fmt.Sprintf("%s/network-config.json", stateDir)) + if err != nil { + return nil, fmt.Errorf("failed reading network-config.json in dir %s for container %s: %w", stateDir, containerID, err) + } + return extractIPPortsFromNetworkConfig(content) +} + +func extractIPPortsFromLabel(jsonPorts string) ([]*api.IPPort, error) { + var ports []Port + err := json.Unmarshal([]byte(jsonPorts), &ports) + if err != nil { + return nil, err + } + + var ipPorts []*api.IPPort + for _, port := range ports { + ipPorts = append(ipPorts, &api.IPPort{ + Protocol: strings.ToLower(port.Protocol), + Ip: port.HostIP, + Port: int32(port.HostPort), + }) + } + + return ipPorts, nil +} + +func extractIPPortsFromNetworkConfig(jsonStr []byte) ([]*api.IPPort, error) { + var cfg NetworkConfig + if err := json.Unmarshal(jsonStr, &cfg); err != nil { + return nil, err + } + + var ipPorts []*api.IPPort + for _, port := range cfg.PortMappings { + ipPorts = append(ipPorts, &api.IPPort{ + Protocol: strings.ToLower(port.Protocol), + Ip: port.HostIP, + Port: int32(port.HostPort), + }) + } + return ipPorts, nil +} + +func ipPortsEqual(a, b []*api.IPPort) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].Protocol != b[i].Protocol || a[i].Ip != b[i].Ip || a[i].Port != b[i].Port { + return false + } + } + return true +} + +// Port is representing nerdctl/ports entry in the +// event envelope's labels. +type Port struct { + HostPort int `json:"HostPort"` + ContainerPort int `json:"ContainerPort"` + Protocol string `json:"Protocol"` + HostIP string `json:"HostIP"` +} + +// NetworkConfig is representing the network config +// of a container that is found in the following Path: +// //containers///network-config.json. +type NetworkConfig struct { + PortMappings []Port `json:"portMappings"` +} diff --git a/pkg/guestagent/events/containerd_others.go b/pkg/guestagent/events/containerd_others.go new file mode 100644 index 00000000000..a1bd26fee85 --- /dev/null +++ b/pkg/guestagent/events/containerd_others.go @@ -0,0 +1,16 @@ +//go:build !linux +// +build !linux + +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package events + +import "github.com/sirupsen/logrus" + +type ContainerdEventMonitor struct{} + +func NewContainerdEventMonitor(_ []string) (*ContainerdEventMonitor, error) { + logrus.Warn("Containerd event monitoring is not implemented on this platform") + return nil, nil +} diff --git a/pkg/guestagent/events/docker_linux.go b/pkg/guestagent/events/docker_linux.go new file mode 100644 index 00000000000..3a14a29761f --- /dev/null +++ b/pkg/guestagent/events/docker_linux.go @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/events" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/sirupsen/logrus" + + "github.com/lima-vm/lima/v2/pkg/guestagent/api" +) + +type DockerEventMonitor struct { + dockerSocketPaths []string +} + +func NewDockerEventMonitor(dockerSocketPaths []string) *DockerEventMonitor { + return &DockerEventMonitor{ + dockerSocketPaths: dockerSocketPaths, + } +} + +func (d *DockerEventMonitor) MonitorPorts(ctx context.Context, ch chan *api.Event) { + const defaultRetryDelay = 2 + retryDelay := 0 + var wg sync.WaitGroup + for _, socket := range d.dockerSocketPaths { + wg.Add(1) + go func(socket string) { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + case <-time.After(time.Duration(retryDelay) * time.Second): + logrus.Debugf("attempting to connect to docker socket %s after: %d", socket, retryDelay) + retryDelay = defaultRetryDelay + } + + info, err := os.Stat(socket) + if err != nil { + if os.IsNotExist(err) { + logrus.Warnf("Docker socket %s does not exist: %s", socket, err) + } else { + logrus.Errorf("failed to stat docker socket: %s: %s", socket, err) + } + retryDelay = defaultRetryDelay + continue + } + if info.IsDir() { + logrus.Errorf("docker socket path %s is a directory", socket) + retryDelay = 15 + continue + } + + var socketURL string + if !strings.HasPrefix(socket, "unix://") { + if strings.HasPrefix(socket, "/") { + socketURL = "unix://" + strings.Trim(socket, "/") + } else { + socketURL = "unix://" + socket + } + } + + client, err := client.NewClientWithOpts(client.WithHost(socketURL), client.WithAPIVersionNegotiation()) + if err != nil { + logrus.Errorf("failed to create a docker client %s", err) + continue + } + clientCtx, cancel := context.WithTimeout(ctx, defaultSocketTimeout) + _, err = client.Ping(clientCtx) + cancel() + if err != nil { + logrus.Warnf("docker daemon not serving on socket %s: %v. Retrying in 5s...", socket, err) + client.Close() + retryDelay = defaultRetryDelay + continue + } + logrus.Infof("successfully connected to docker on socket %s", socket) + if err := d.runMonitorClient(ctx, client, ch); err != nil { + logrus.Errorf("docker port monitoring for socket: %s failed: %s", socket, err) + } + client.Close() + } + }(socket) + } + wg.Wait() +} + +func (d *DockerEventMonitor) runMonitorClient(ctx context.Context, cli *client.Client, ch chan *api.Event) error { + runningContainers := make(ipPortMap) + defer cli.Close() + + if err := d.initializeRunningContainers(ctx, cli, ch, runningContainers); err != nil { + logrus.Errorf("failed to initialize existing docker container published ports: %s", err) + } + + msgCh, errCh := cli.Events(ctx, events.ListOptions{ + Filters: filters.NewArgs( + filters.Arg("type", string(types.ContainerObject)), + filters.Arg("event", string(events.ActionStart)), + filters.Arg("event", string(events.ActionStop)), + filters.Arg("event", string(events.ActionDie))), + }) + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("context cancellation: %w", ctx.Err()) + + case event := <-msgCh: + container, err := cli.ContainerInspect(ctx, event.Actor.ID) + if err != nil { + logrus.Errorf("inspecting container [%v] failed: %v", event.Actor.ID, err) + continue + } + portMap := container.NetworkSettings.NetworkSettingsBase.Ports + logrus.Debugf("received an event: {Status: %+v ContainerID: %+v Ports: %+v}", + event.Action, + event.Actor.ID, + portMap) + + switch event.Action { + case events.ActionStart: + if len(portMap) != 0 { + validatePortMapping(portMap) + ipPorts, err := convertToIPPort(portMap) + if err != nil { + logrus.Errorf("converting docker's portMapping: %+v to api.IPPort: %v failed: %s", portMap, ipPorts, err) + continue + } + logrus.Infof("successfully converted PortMapping:%+v to IPPorts: %+v", portMap, ipPorts) + runningContainers[event.Actor.ID] = ipPorts + sendHostAgentEvent(false, ipPorts, ch) + } + case events.ActionStop, events.ActionDie: + ipPorts, ok := runningContainers[event.Actor.ID] + if ok { + delete(runningContainers, event.Actor.ID) + } + if ok { + sendHostAgentEvent(true, ipPorts, ch) + } + } + case err := <-errCh: + return fmt.Errorf("receiving container event failed: %w", err) + } + } +} + +func (d *DockerEventMonitor) initializeRunningContainers(ctx context.Context, cli *client.Client, ch chan *api.Event, runningContainers ipPortMap) error { + containers, err := cli.ContainerList(ctx, container.ListOptions{ + Filters: filters.NewArgs(filters.Arg("status", "running")), + }) + if err != nil { + return err + } + + for _, container := range containers { + if len(container.Ports) == 0 { + continue + } + var ipPorts []*api.IPPort + for _, port := range container.Ports { + if port.IP == "" || port.PublicPort == 0 { + continue + } + + ipPorts = append(ipPorts, &api.IPPort{ + Protocol: strings.ToLower(port.Type), + Ip: port.IP, + Port: int32(port.PublicPort), + }) + } + sendHostAgentEvent(false, ipPorts, ch) + runningContainers[container.ID] = ipPorts + } + return nil +} + +func convertToIPPort(portMap nat.PortMap) ([]*api.IPPort, error) { + var ipPorts []*api.IPPort + for key, portBindings := range portMap { + for _, portBinding := range portBindings { + hostPort, err := strconv.ParseInt(portBinding.HostPort, 10, 32) + if err != nil { + return ipPorts, err + } + if portBinding.HostIP == "" || hostPort == 0 { + continue + } + + logrus.Debugf("converted the following PortMapping to IPPort, containerPort:%v HostPort:%v IP:%v Protocol:%v", + key.Port(), portBinding.HostPort, portBinding.HostIP, key.Proto()) + + ipPorts = append(ipPorts, &api.IPPort{ + Protocol: strings.ToLower(key.Proto()), + Ip: portBinding.HostIP, + Port: int32(hostPort), + }) + } + } + return ipPorts, nil +} + +// Removes entries in port mapping that do not hold any values +// for IP and Port e.g 9000/tcp:[]. +func validatePortMapping(portMap nat.PortMap) { + for k, v := range portMap { + if len(v) == 0 { + logrus.Debugf("removing entry: %v from the portmappings: %v", k, portMap) + delete(portMap, k) + } + } +} diff --git a/pkg/guestagent/events/docker_others.go b/pkg/guestagent/events/docker_others.go new file mode 100644 index 00000000000..62351e8c9c7 --- /dev/null +++ b/pkg/guestagent/events/docker_others.go @@ -0,0 +1,16 @@ +//go:build !linux +// +build !linux + +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package events + +import "github.com/sirupsen/logrus" + +type DockerEventMonitor struct{} + +func NewDockerEventMonitor(_ []string) (*DockerEventMonitor, error) { + logrus.Warn("Docker event monitoring is not implemented on this platform") + return nil, nil +} diff --git a/pkg/guestagent/events/eventutils.go b/pkg/guestagent/events/eventutils.go new file mode 100644 index 00000000000..5d1b132425e --- /dev/null +++ b/pkg/guestagent/events/eventutils.go @@ -0,0 +1,38 @@ +//go:build linux +// +build linux + +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "github.com/sirupsen/logrus" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/lima-vm/lima/v2/pkg/guestagent/api" +) + +func sendHostAgentEvent(remove bool, ipPorts []*api.IPPort, ch chan *api.Event) { + ev := &api.Event{ + Time: timestamppb.Now(), + } + if remove { + ev.RemovedLocalPorts = ipPorts + } else { + ev.AddedLocalPorts = ipPorts + } + ch <- ev + logrus.Debugf("sent the following event to hostAgent: %+v", ev) +} + +type NoClientError struct{} + +func (e *NoClientError) Error() string { + return "no available client connected to the event monitor" +} + +func (e *NoClientError) Is(target error) bool { + _, ok := target.(*NoClientError) + return ok +} diff --git a/pkg/guestagent/events/kubernetes_linux.go b/pkg/guestagent/events/kubernetes_linux.go new file mode 100644 index 00000000000..e5754e05f5e --- /dev/null +++ b/pkg/guestagent/events/kubernetes_linux.go @@ -0,0 +1,286 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "context" + "errors" + "fmt" + "net/url" + "os" + "strings" + "time" + + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/clientcmd" + + "github.com/lima-vm/lima/v2/pkg/guestagent/api" +) + +type event struct { + UID types.UID + namespace string + name string + portMapping map[int32]corev1.Protocol + deleted bool +} + +type KubeServiceWatcher struct { + kubeConfigPaths []string + kubeClient kubernetes.Interface + eventCh chan event + errorCh chan error +} + +func NewKubeServiceWatcher(cfgPaths []string) *KubeServiceWatcher { + return &KubeServiceWatcher{ + kubeConfigPaths: cfgPaths, + eventCh: make(chan event), + errorCh: make(chan error), + } +} + +func (k *KubeServiceWatcher) createAndVerifyClient(ctx context.Context) (bool, error) { + kubeClient, err := tryGetKubeClient(k.kubeConfigPaths) + if err != nil { + logrus.Tracef("failed to get kube client: %s", err) + return false, nil + } + + informerFactory := informers.NewSharedInformerFactory(kubeClient, time.Hour) + serviceInformer := informerFactory.Core().V1().Services().Informer() + + _, err = serviceInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj any) { + logrus.Tracef("Service Informer: Add func called with: %+v", obj) + handleUpdate(nil, obj, k.eventCh) + }, + DeleteFunc: func(obj any) { + logrus.Tracef("Service Informer: Del func called with: %+v", obj) + handleUpdate(obj, nil, k.eventCh) + }, + UpdateFunc: func(oldObj, newObj any) { + logrus.Tracef("Service Informer: Update func called with old object %+v and new Object: %+v", oldObj, newObj) + handleUpdate(oldObj, newObj, k.eventCh) + }, + }) + if err != nil { + // this error can not be ignored and must be returned + return false, fmt.Errorf("error setting eventHandler: %w", err) + } + err = serviceInformer.SetWatchErrorHandler(func(_ *cache.Reflector, err error) { + k.errorCh <- fmt.Errorf("kubernetes: error watching service: %w", err) + }) + if err != nil { + // this error can not be ignored and must be returned + return false, fmt.Errorf("error setting errorHandler: %w", err) + } + informerFactory.WaitForCacheSync(ctx.Done()) + informerFactory.Start(ctx.Done()) + k.kubeClient = kubeClient + return true, nil +} + +func (k *KubeServiceWatcher) initializeServices(ctx context.Context) error { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + services, err := k.kubeClient.CoreV1().Services(corev1.NamespaceAll).List(ctx, v1.ListOptions{}) + if err != nil { + logrus.Errorf("Listing services failed: %s", err) + switch { + default: + return err + case isTimeout(err): + case errors.Is(err, unix.ENETUNREACH): + case errors.Is(err, unix.ECONNREFUSED): + case isAPINotReady(err): + } + continue + } + + // List the initial set of services asynchronously, so that we don't have to + // worry about the channel blocking. + go func() { + for _, svc := range services.Items { + handleUpdate(nil, svc, k.eventCh) + } + }() + return nil + + case <-ctx.Done(): + return fmt.Errorf("context cancelled during initialization: %w", ctx.Err()) + } + } +} + +func (k *KubeServiceWatcher) MonitorServices(ctx context.Context, ch chan *api.Event) error { + if err := tryGetClient(ctx, k.createAndVerifyClient); err != nil { + return fmt.Errorf("failed getting kube client: %w", err) + } + + if err := k.initializeServices(ctx); err != nil { + return fmt.Errorf("failed initializing services: %w", err) + } + for { + select { + case <-ctx.Done(): + return fmt.Errorf("context cancellation: %w", ctx.Err()) + case err := <-k.errorCh: + logrus.Errorf("received an error from kube API: %s", err) + case event := <-k.eventCh: + logrus.Debugf("received an event from kube API: %+v", event) + ipPorts := createIPPortFromPortMapping(event.portMapping) + sendHostAgentEvent(event.deleted, ipPorts, ch) + } + } +} + +func createIPPortFromPortMapping(portMapping map[int32]corev1.Protocol) (ipPorts []*api.IPPort) { + for port, proto := range portMapping { + ipPorts = append(ipPorts, &api.IPPort{ + Ip: "0.0.0.0", + Protocol: strings.ToLower(string(proto)), + Port: port, + }) + } + return ipPorts +} + +func handleUpdate(oldObj, newObj any, eventCh chan<- event) { + deleted := make(map[int32]corev1.Protocol) + added := make(map[int32]corev1.Protocol) + oldSvc, _ := oldObj.(*corev1.Service) + newSvc, _ := newObj.(*corev1.Service) + namespace := "" + name := "" + + if oldSvc != nil { + namespace = oldSvc.Namespace + name = oldSvc.Name + + if oldSvc.Spec.Type == corev1.ServiceTypeNodePort { + for _, port := range oldSvc.Spec.Ports { + deleted[port.NodePort] = port.Protocol + } + } + + if oldSvc.Spec.Type == corev1.ServiceTypeLoadBalancer { + for _, port := range oldSvc.Spec.Ports { + deleted[port.Port] = port.Protocol + } + } + } + + if newSvc != nil { + namespace = newSvc.Namespace + name = newSvc.Name + + if newSvc.Spec.Type == corev1.ServiceTypeNodePort { + for _, port := range newSvc.Spec.Ports { + delete(deleted, port.NodePort) + added[port.NodePort] = port.Protocol + } + } + + if newSvc.Spec.Type == corev1.ServiceTypeLoadBalancer { + for _, port := range newSvc.Spec.Ports { + delete(deleted, port.Port) + added[port.Port] = port.Protocol + } + } + } + + if len(deleted) > 0 { + sendEvents(deleted, oldSvc, true, eventCh) + } + + if len(added) > 0 { + sendEvents(added, newSvc, false, eventCh) + } + + logrus.Debugf("kubernetes service update: %s/%s has -%d +%d service port", + namespace, name, len(deleted), len(added)) +} + +func sendEvents(mapping map[int32]corev1.Protocol, svc *corev1.Service, deleted bool, eventCh chan<- event) { + if svc != nil { + eventCh <- event{ + UID: svc.UID, + namespace: svc.Namespace, + name: svc.Name, + portMapping: mapping, + deleted: deleted, + } + } +} + +func tryGetKubeClient(candidateKubeConfigs []string) (kubernetes.Interface, error) { + for _, kubeconfig := range candidateKubeConfigs { + _, err := os.Stat(kubeconfig) + if err != nil { + if os.IsNotExist(err) { + continue + } + + return nil, fmt.Errorf("stat kubeconfig %s failed: %w", kubeconfig, err) + } + + restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, fmt.Errorf("build kubeconfig from %s failed: %w", kubeconfig, err) + } + u, err := url.Parse(restConfig.Host) + if err != nil { + return nil, fmt.Errorf("parse kubeconfig host %s failed: %w", restConfig.Host, err) + } + if u.Hostname() != "127.0.0.1" { // might need to support IPv6 + // ensures the kubeconfig points to local k8s + continue + } + + kubeClient, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return nil, err + } + + return kubeClient, nil + } + + return nil, errors.New("no valid kubeconfig found") +} + +func isTimeout(err error) bool { + type timeout interface { + Timeout() bool + } + + var timeoutError timeout + + return errors.As(err, &timeoutError) && timeoutError.Timeout() +} + +// This is a k3s error that is received over +// the HTTP, Also, it is worth noting that this +// error is wrapped. This is why we are not testing +// against the real error object using errors.Is(). +func isAPINotReady(err error) bool { + return strings.Contains(err.Error(), "apiserver not ready") || strings.Contains(err.Error(), "starting") +} + +func tryGetClient(ctx context.Context, tryConnect func(context.Context) (bool, error)) error { + const retryInterval = 10 * time.Second + const pollImmediately = true + return wait.PollUntilContextCancel(ctx, retryInterval, pollImmediately, tryConnect) +} diff --git a/pkg/guestagent/events/kubernetes_others.go b/pkg/guestagent/events/kubernetes_others.go new file mode 100644 index 00000000000..378e7ba2a7f --- /dev/null +++ b/pkg/guestagent/events/kubernetes_others.go @@ -0,0 +1,16 @@ +//go:build !linux +// +build !linux + +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package events + +import "github.com/sirupsen/logrus" + +type KubeServiceWatcher struct{} + +func NewKubeServiceWatcher(_ []string) *KubeServiceWatcher { + logrus.Warn("NewKubeServiceWatcher is not implemented on this platform") + return nil +} diff --git a/pkg/guestagent/guestagent_linux.go b/pkg/guestagent/guestagent_linux.go index 5b199131f56..e5fc4051313 100644 --- a/pkg/guestagent/guestagent_linux.go +++ b/pkg/guestagent/guestagent_linux.go @@ -18,16 +18,31 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/lima-vm/lima/v2/pkg/guestagent/api" + "github.com/lima-vm/lima/v2/pkg/guestagent/events" "github.com/lima-vm/lima/v2/pkg/guestagent/iptables" - "github.com/lima-vm/lima/v2/pkg/guestagent/kubernetesservice" "github.com/lima-vm/lima/v2/pkg/guestagent/procnettcp" "github.com/lima-vm/lima/v2/pkg/guestagent/timesync" ) -func New(ctx context.Context, newTicker func() (<-chan time.Time, func()), iptablesIdle time.Duration) (Agent, error) { +type Config struct { + Ticker func() (<-chan time.Time, func()) + IptablesIdle time.Duration + DockerSockets []string + ContainerdSockets []string + KubernetesConfigs []string +} + +func New(cfg *Config) (Agent, error) { + dockerEventMonitor := events.NewDockerEventMonitor(cfg.DockerSockets) + containerdEventMonitor := events.NewContainerdEventMonitor(cfg.ContainerdSockets) + kubeServiceWatcher := events.NewKubeServiceWatcher(cfg.KubernetesConfigs) + a := &agent{ - newTicker: newTicker, - kubernetesServiceWatcher: kubernetesservice.NewServiceWatcher(), + newTicker: cfg.Ticker, + IptablesIdle: cfg.IptablesIdle, + dockerEventMonitor: dockerEventMonitor, + containerdEventMonitor: containerdEventMonitor, + kubeServiceWatcher: kubeServiceWatcher, } auditClient, err := libaudit.NewMulticastAuditClient(nil) @@ -39,7 +54,7 @@ func New(ctx context.Context, newTicker func() (<-chan time.Time, func()), iptab return nil, err } logrus.Infof("Auditing is not available: %s", err) - return startGuestAgentRoutines(ctx, a, false), nil + return startGuestAgentRoutines(a, false), nil } auditStatus, err := auditClient.GetStatus() @@ -50,7 +65,7 @@ func New(ctx context.Context, newTicker func() (<-chan time.Time, func()), iptab return nil, err } logrus.Infof("Auditing is not permitted: %s", err) - return startGuestAgentRoutines(ctx, a, false), nil + return startGuestAgentRoutines(a, false), nil } if auditStatus.Enabled == 0 { @@ -67,13 +82,12 @@ func New(ctx context.Context, newTicker func() (<-chan time.Time, func()), iptab return nil, err } } - - go a.setWorthCheckingIPTablesRoutine(auditClient, iptablesIdle) + go a.setWorthCheckingIPTablesRoutine(auditClient) } else { a.worthCheckingIPTables = true } logrus.Infof("Auditing enabled (%d)", auditStatus.Enabled) - return startGuestAgentRoutines(ctx, a, true), nil + return startGuestAgentRoutines(a, true), nil } // startGuestAgentRoutines sets worthCheckingIPTables to true if auditing is not supported, @@ -81,11 +95,10 @@ func New(ctx context.Context, newTicker func() (<-chan time.Time, func()), iptab // // Auditing is not supported in a kernels and is not currently supported outside of the initial namespace, so does not work // from inside a container or WSL2 instance, for example. -func startGuestAgentRoutines(ctx context.Context, a *agent, supportsAuditing bool) *agent { +func startGuestAgentRoutines(a *agent, supportsAuditing bool) *agent { if !supportsAuditing { a.worthCheckingIPTables = true } - go a.kubernetesServiceWatcher.Start(ctx) go a.fixSystemTimeSkew() return a @@ -97,11 +110,14 @@ type agent struct { // reload /proc/net/tcp. newTicker func() (<-chan time.Time, func()) - worthCheckingIPTables bool - worthCheckingIPTablesMu sync.RWMutex - latestIPTables []iptables.Entry - latestIPTablesMu sync.RWMutex - kubernetesServiceWatcher *kubernetesservice.ServiceWatcher + worthCheckingIPTables bool + worthCheckingIPTablesMu sync.RWMutex + IptablesIdle time.Duration + latestIPTables []iptables.Entry + latestIPTablesMu sync.RWMutex + dockerEventMonitor *events.DockerEventMonitor + containerdEventMonitor *events.ContainerdEventMonitor + kubeServiceWatcher *events.KubeServiceWatcher } // setWorthCheckingIPTablesRoutine sets worthCheckingIPTables to be true @@ -109,16 +125,16 @@ type agent struct { // // setWorthCheckingIPTablesRoutine sets worthCheckingIPTables to be false // when no NETFILTER_CFG audit message was received for the iptablesIdle time. -func (a *agent) setWorthCheckingIPTablesRoutine(auditClient *libaudit.AuditClient, iptablesIdle time.Duration) { +func (a *agent) setWorthCheckingIPTablesRoutine(auditClient *libaudit.AuditClient) { logrus.Info("setWorthCheckingIPTablesRoutine(): monitoring netfilter audit events") var latestTrue time.Time go func() { for { - time.Sleep(iptablesIdle) + time.Sleep(a.IptablesIdle) a.worthCheckingIPTablesMu.Lock() // time is monotonic, see https://pkg.go.dev/time#hdr-Monotonic_Clocks elapsedSinceLastTrue := time.Since(latestTrue) - if elapsedSinceLastTrue >= iptablesIdle { + if elapsedSinceLastTrue >= a.IptablesIdle { logrus.Debug("setWorthCheckingIPTablesRoutine(): setting to false") a.worthCheckingIPTables = false } @@ -197,6 +213,22 @@ func isEventEmpty(ev *api.Event) bool { func (a *agent) Events(ctx context.Context, ch chan *api.Event) { defer close(ch) + + errorCh := make(chan error) + if a.kubeServiceWatcher != nil { + go func() { + if err := a.kubeServiceWatcher.MonitorServices(ctx, ch); err != nil { + errorCh <- err + } + }() + } + if a.containerdEventMonitor != nil { + go a.containerdEventMonitor.MonitorPorts(ctx, ch) + } + if a.dockerEventMonitor != nil { + go a.dockerEventMonitor.MonitorPorts(ctx, ch) + } + tickerCh, tickerClose := a.newTicker() defer tickerClose() var st eventState @@ -209,6 +241,8 @@ func (a *agent) Events(ctx context.Context, ch chan *api.Event) { select { case <-ctx.Done(): return + case err := <-errorCh: + logrus.Errorf("event monitoring failed: %s", err) case _, ok := <-tickerCh: if !ok { return @@ -291,25 +325,6 @@ func (a *agent) LocalPorts(ctx context.Context) ([]*api.IPPort, error) { } } - kubernetesEntries := a.kubernetesServiceWatcher.GetPorts() - for _, entry := range kubernetesEntries { - found := false - for _, re := range res { - if re.Port == int32(entry.Port) { - found = true - } - } - - if !found { - res = append(res, - &api.IPPort{ - Ip: entry.IP.String(), - Port: int32(entry.Port), - Protocol: string(entry.Protocol), - }) - } - } - return res, nil } diff --git a/pkg/limatype/lima_yaml.go b/pkg/limatype/lima_yaml.go index 367f5bc9887..24305448111 100644 --- a/pkg/limatype/lima_yaml.go +++ b/pkg/limatype/lima_yaml.go @@ -40,6 +40,7 @@ type LimaYAML struct { GuestInstallPrefix *string `yaml:"guestInstallPrefix,omitempty" json:"guestInstallPrefix,omitempty" jsonschema:"nullable"` Probes []Probe `yaml:"probes,omitempty" json:"probes,omitempty"` PortForwards []PortForward `yaml:"portForwards,omitempty" json:"portForwards,omitempty"` + PortMonitors PortMonitor `yaml:"portMonitors,omitempty" json:"portMonitors,omitempty"` CopyToHost []CopyToHost `yaml:"copyToHost,omitempty" json:"copyToHost,omitempty"` Message string `yaml:"message,omitempty" json:"message,omitempty"` Networks []Network `yaml:"networks,omitempty" json:"networks,omitempty" jsonschema:"nullable"` @@ -400,3 +401,19 @@ func DefaultDriver() VMType { return QEMU } } + +// Engine is a list of container engine connection details. +type Engine struct { + Sockets []string `yaml:"sockets,omitempty" json:"sockets,omitempty"` +} + +type Kubernetes struct { + // Configs is a list of Kubernetes config files, e.g. "/etc/rancher/k3s/k3s.yaml" + Configs []string `yaml:"configs,omitempty" json:"configs,omitempty"` +} + +type PortMonitor struct { + Docker Engine `yaml:"docker,omitempty" json:"docker,omitempty"` + Containerd Engine `yaml:"containerd,omitempty" json:"containerd,omitempty"` + Kubernetes Kubernetes `yaml:"kubernetes,omitempty" json:"kubernetes,omitempty"` +} diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 1c96ff8e40f..e21eeef5a03 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -527,6 +527,54 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin // After defaults processing the singular HostPort and GuestPort values should not be used again. } + // Manage PortMonitors sockets for Docker + y.PortMonitors.Docker.Sockets = slices.Concat( + o.PortMonitors.Docker.Sockets, + y.PortMonitors.Docker.Sockets, + d.PortMonitors.Docker.Sockets) + + for i := range y.PortMonitors.Docker.Sockets { + socket := &y.PortMonitors.Docker.Sockets[i] + if out, err := executeGuestTemplate(*socket, instDir, y.User, y.Param); err == nil { + *socket = out.String() + } else { + logrus.WithError(err).Warnf("Couldn't process Docker socket %q as a template", *socket) + } + } + + // Manage PortMonitors sockets for Containerd + y.PortMonitors.Containerd.Sockets = slices.Concat( + o.PortMonitors.Containerd.Sockets, + y.PortMonitors.Containerd.Sockets, + d.PortMonitors.Containerd.Sockets) + + for i := range y.PortMonitors.Containerd.Sockets { + socket := &y.PortMonitors.Containerd.Sockets[i] + if out, err := executeGuestTemplate(*socket, instDir, y.User, y.Param); err == nil { + *socket = out.String() + } else { + logrus.WithError(err).Warnf("Couldn't process Containerd socket %q as a template", *socket) + } + } + + if y.Containerd.System != nil && *y.Containerd.System { + y.PortMonitors.Containerd.Sockets = unique(append(y.PortMonitors.Containerd.Sockets, "/run/containerd/containerd.sock")) + } + if y.Containerd.User != nil && *y.Containerd.User { + socket := "/run/user/{{.UID}}/containerd/containerd.sock" + if out, err := executeGuestTemplate(socket, instDir, y.User, y.Param); err == nil { + y.PortMonitors.Containerd.Sockets = unique(append(y.PortMonitors.Containerd.Sockets, out.String())) + } else { + logrus.WithError(err).Warnf("Couldn't process Containerd user socket %q as template", socket) + } + } + + // Manage PortMonitors config for kubernetes + y.PortMonitors.Kubernetes.Configs = unique(slices.Concat( + o.PortMonitors.Kubernetes.Configs, + y.PortMonitors.Kubernetes.Configs, + d.PortMonitors.Kubernetes.Configs)) + y.CopyToHost = slices.Concat(o.CopyToHost, y.CopyToHost, d.CopyToHost) for i := range y.CopyToHost { FillCopyToHostDefaults(&y.CopyToHost[i], instDir, y.User, y.Param) diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index 83857f5025e..74cc1f4bd06 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -253,9 +253,6 @@ func TestFillDefault(t *testing.T) { defaultPortForward, defaultPortForward, } - expect.CopyToHost = []limatype.CopyToHost{ - {}, - } // Setting GuestPort and HostPort for DeepEqual(), but they are not supposed to be used // after FillDefault() has been called and the ...PortRange fields have been set. @@ -271,6 +268,14 @@ func TestFillDefault(t *testing.T) { expect.PortForwards[3].GuestSocket = fmt.Sprintf("%s | %s | %s | %s", user.HomeDir, user.Uid, user.Username, y.Param["ONE"]) expect.PortForwards[3].HostSocket = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, currentUser.Uid, currentUser.Username, y.Param["ONE"]) + expect.PortMonitors.Containerd.Sockets = []string{ + fmt.Sprintf("/run/user/%s/containerd/containerd.sock", user.Uid), + } + + expect.CopyToHost = []limatype.CopyToHost{ + {}, + } + expect.CopyToHost[0].GuestFile = fmt.Sprintf("%s | %s | %s | %s", user.HomeDir, user.Uid, user.Username, y.Param["ONE"]) expect.CopyToHost[0].HostFile = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, currentUser.Uid, currentUser.Username, y.Param["ONE"]) @@ -453,6 +458,10 @@ func TestFillDefault(t *testing.T) { expect.Plain = ptr.Of(false) + expect.PortMonitors.Containerd.Sockets = []string{ + "/run/containerd/containerd.sock", + } + y = limatype.LimaYAML{} FillDefault(t.Context(), &y, &d, &limatype.LimaYAML{}, filePath, false) assert.DeepEqual(t, &y, &expect, opts...) @@ -635,6 +644,10 @@ func TestFillDefault(t *testing.T) { expect.Provision = slices.Concat(o.Provision, y.Provision, dExpected.Provision) expect.Probes = slices.Concat(o.Probes, y.Probes, dExpected.Probes) expect.PortForwards = slices.Concat(o.PortForwards, y.PortForwards, dExpected.PortForwards) + expect.PortMonitors.Containerd.Sockets = []string{ + fmt.Sprintf("/run/user/%s/containerd/containerd.sock", user.Uid), + "/run/containerd/containerd.sock", + } expect.CopyToHost = slices.Concat(o.CopyToHost, y.CopyToHost, dExpected.CopyToHost) expect.Containerd.Archives = slices.Concat(o.Containerd.Archives, y.Containerd.Archives, dExpected.Containerd.Archives) expect.Containerd.Archives[3].Arch = *expect.Arch diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index d38d55e1768..d4bfaad1df9 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net" + "net/url" "os" "path" "path/filepath" @@ -362,6 +363,21 @@ func Validate(y *limatype.LimaYAML, warn bool) error { // Not validating that the various GuestPortRanges and HostPortRanges are not overlapping. Rules will be // processed sequentially and the first matching rule for a guest port determines forwarding behavior. } + + for i, socket := range y.PortMonitors.Containerd.Sockets { + field := fmt.Sprintf("portMonitor.containerd.sockets[%d]", i) + if err := validateSocket(field, socket); err != nil { + errs = errors.Join(errs, err) + } + } + + for i, socket := range y.PortMonitors.Docker.Sockets { + field := fmt.Sprintf("portMonitor.docker.sockets[%d]", i) + if err := validateSocket(field, socket); err != nil { + errs = errors.Join(errs, err) + } + } + for i, rule := range y.CopyToHost { field := fmt.Sprintf("CopyToHost[%d]", i) if rule.GuestFile != "" { @@ -644,3 +660,39 @@ func ValidateAgainstLatestConfig(ctx context.Context, yNew, yLatest []byte) erro return errs } + +func validateSocket(field, socket string) error { + if socket == "" { + return fmt.Errorf("%s socket path must not be empty", field) + } + + u, err := url.Parse(socket) + if err != nil { + return fmt.Errorf("%s socket path %q is not a valid URL: %w", field, socket, err) + } + // Treat empty scheme as a unix socket path + if u.Scheme == "" { + return validateUnixSocket(field, socket) + } + switch u.Scheme { + case "unix", "file": + if u.Path == "" { + return fmt.Errorf("%s socket path %q is not a valid URL: missing path", field, socket) + } + return validateUnixSocket(field, u.Path) + case "tcp": + if u.Host == "" { + return fmt.Errorf("%s socket path %q is not a valid URL: missing host", field, socket) + } + return nil + default: + return fmt.Errorf("%s socket path %q is not a valid URL: unsupported scheme %q", field, socket, u.Scheme) + } +} + +func validateUnixSocket(field, socketPath string) error { + if !path.IsAbs(socketPath) { + return fmt.Errorf("field `%s` must be absolute, got %q", field, socketPath) + } + return nil +} diff --git a/pkg/portfwd/listener.go b/pkg/portfwd/listener.go index d3c3dadebe7..7ee4a343fb7 100644 --- a/pkg/portfwd/listener.go +++ b/pkg/portfwd/listener.go @@ -71,7 +71,6 @@ func (p *ClosableListeners) Remove(_ context.Context, protocol, hostAddress, gue func (p *ClosableListeners) forwardTCP(ctx context.Context, client *guestagentclient.GuestAgentClient, hostAddress, guestAddress string) { key := key("tcp", hostAddress, guestAddress) - p.listenersRW.Lock() _, ok := p.listeners[key] if ok { @@ -84,9 +83,11 @@ func (p *ClosableListeners) forwardTCP(ctx context.Context, client *guestagentcl p.listenersRW.Unlock() return } + defer p.Remove(ctx, "tcp", hostAddress, guestAddress) p.listeners[key] = tcpLis p.listenersRW.Unlock() + for { conn, err := tcpLis.Accept() if err != nil { diff --git a/templates/default.yaml b/templates/default.yaml index d1a0061696b..6df60d735b1 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -513,6 +513,20 @@ networks: # hostPortRange: [1, 65535] # # Any port still not matched by a rule will not be forwarded (ignored) +# portMonitors configures monitoring of container engine ports. +# These are used to detect forwarded ports from Docker, containerd, or Kubernetes workloads. +portMonitors: + # docker specifies a list of Unix sockets to monitor for Docker API port forwards. + docker: + sockets: [] + # containerd specifies a list of Unix sockets to monitor for containerd workloads. If no sockets are specified, the default socket will be used + # using the containerd settings. If containerd user is enabled a user socket will be used, otherwise a system socket. + containerd: + sockets: [] + # kubernetes specifies paths to kubeconfig files to watch for Kubernetes port-forwarded services. + kubernetes: + configs: [] + # Copy files from the guest to the host. Copied after provisioning scripts have been completed. # copyToHost: # - guest: "/etc/myconfig.cfg" diff --git a/templates/docker-rootful.yaml b/templates/docker-rootful.yaml index 53477203efc..f7b0fc9fd8d 100644 --- a/templates/docker-rootful.yaml +++ b/templates/docker-rootful.yaml @@ -62,6 +62,10 @@ hostResolver: portForwards: - guestSocket: "/var/run/docker.sock" hostSocket: "{{.Dir}}/sock/docker.sock" +portMonitors: + docker: + sockets: + - "/var/run/docker.sock" message: | To run `docker` on the host (assumes docker-cli is installed), run the following commands: ------ diff --git a/templates/docker.yaml b/templates/docker.yaml index 4d5c0e7a0e2..f70d3d7350f 100644 --- a/templates/docker.yaml +++ b/templates/docker.yaml @@ -69,6 +69,10 @@ hostResolver: portForwards: - guestSocket: "/run/user/{{.UID}}/docker.sock" hostSocket: "{{.Dir}}/sock/docker.sock" +portMonitors: + docker: + sockets: + - "/run/user/{{.UID}}/docker.sock" message: | To run `docker` on the host (assumes docker-cli is installed), run the following commands: ------ diff --git a/templates/k0s.yaml b/templates/k0s.yaml index 00fa61811e5..d644b226735 100644 --- a/templates/k0s.yaml +++ b/templates/k0s.yaml @@ -67,3 +67,7 @@ message: | export KUBECONFIG="{{.Dir}}/copied-from-guest/kubeconfig.yaml" kubectl ... ------ +portMonitors: + kubernetes: + configs: + - "/var/lib/k0s/pki/admin.conf" diff --git a/templates/k3s.yaml b/templates/k3s.yaml index 9b84d591114..98d8a38783d 100644 --- a/templates/k3s.yaml +++ b/templates/k3s.yaml @@ -49,3 +49,7 @@ message: | export KUBECONFIG="{{.Dir}}/copied-from-guest/kubeconfig.yaml" kubectl ... ------ +portMonitors: + kubernetes: + configs: + - "/etc/rancher/k3s/k3s.yaml" diff --git a/templates/k8s.yaml b/templates/k8s.yaml index 3bcc6c9afc6..9e0309144cf 100644 --- a/templates/k8s.yaml +++ b/templates/k8s.yaml @@ -176,3 +176,7 @@ message: | export KUBECONFIG="{{.Dir}}/copied-from-guest/kubeconfig.yaml" kubectl ... ------ +portMonitors: + kubernetes: + configs: + - "/etc/kubernetes/admin.conf"