From a047c7447c06853506a20c8e450ef34bc07bcc19 Mon Sep 17 00:00:00 2001 From: thediveo Date: Tue, 17 Oct 2023 14:00:26 +0000 Subject: [PATCH] refactor: import carved-out turtlefinder module Signed-off-by: thediveo --- api/v1/netns.go | 9 +- api/v1/package_test.go | 7 +- api/v1/targets.go | 7 +- cmd/gostdump/main.go | 5 +- cmd/gostwire/cmdroot.go | 2 +- cmd/lsallnifs/rootcmd.go | 5 +- decorator/dockernet/dockernet_test.go | 7 +- decorator/nerdctlnet/nerdctlnet_test.go | 4 +- discover.go | 2 +- example_test.go | 2 +- go.mod | 2 + go.sum | 4 + internal/discover/discover.go | 3 +- metadata/engines/engines_test.go | 6 +- metadata/iecore/iecore_test.go | 7 +- mobydig/mobydig_test.go | 7 +- mobydig/neighborhood_test.go | 7 +- test/kind/kind_test.go | 27 +- turtlefinder/detector/all/all.go | 10 - turtlefinder/detector/all/doc.go | 4 - turtlefinder/detector/all/package_test.go | 17 - turtlefinder/detector/all/plugin_test.go | 29 -- .../detector/containerd/containerd.go | 73 ---- .../detector/containerd/containerd_test.go | 81 ----- turtlefinder/detector/containerd/doc.go | 4 - .../detector/containerd/package_test.go | 17 - turtlefinder/detector/doc.go | 10 - turtlefinder/detector/moby/doc.go | 4 - turtlefinder/detector/moby/moby.go | 68 ---- turtlefinder/detector/moby/moby_test.go | 84 ----- turtlefinder/detector/moby/package_test.go | 17 - turtlefinder/detector/plugin.go | 25 -- turtlefinder/doc.go | 29 -- turtlefinder/engine.go | 104 ------ turtlefinder/engine_test.go | 81 ----- turtlefinder/package_test.go | 17 - turtlefinder/sockfinder.go | 137 ------- turtlefinder/sockfinder_test.go | 63 ---- turtlefinder/stacker.go | 129 ------- turtlefinder/stacker_matcher.go | 29 -- turtlefinder/stacker_test.go | 120 ------ turtlefinder/test/cind/Dockerfile | 16 - turtlefinder/test/cind/common.sh | 12 - .../files/etc/systemd/system/testing.service | 11 - turtlefinder/test/cind/files/testing/setup.sh | 19 - turtlefinder/test/cind/setup.sh | 38 -- turtlefinder/test/cind/teardown.sh | 7 - turtlefinder/test/fdscan/proc/1234/fd/0 | 0 turtlefinder/test/fdscan/proc/5678/fd/3 | 1 - turtlefinder/test/fdscan/proc/5678/fd/junk | 0 turtlefinder/turtlefinder.go | 342 ------------------ turtlefinder/turtlefinder_test.go | 60 --- webui/src/models/gw/model.ts | 2 + 53 files changed, 62 insertions(+), 1711 deletions(-) delete mode 100644 turtlefinder/detector/all/all.go delete mode 100644 turtlefinder/detector/all/doc.go delete mode 100644 turtlefinder/detector/all/package_test.go delete mode 100644 turtlefinder/detector/all/plugin_test.go delete mode 100644 turtlefinder/detector/containerd/containerd.go delete mode 100644 turtlefinder/detector/containerd/containerd_test.go delete mode 100644 turtlefinder/detector/containerd/doc.go delete mode 100644 turtlefinder/detector/containerd/package_test.go delete mode 100644 turtlefinder/detector/doc.go delete mode 100644 turtlefinder/detector/moby/doc.go delete mode 100644 turtlefinder/detector/moby/moby.go delete mode 100644 turtlefinder/detector/moby/moby_test.go delete mode 100644 turtlefinder/detector/moby/package_test.go delete mode 100644 turtlefinder/detector/plugin.go delete mode 100644 turtlefinder/doc.go delete mode 100644 turtlefinder/engine.go delete mode 100644 turtlefinder/engine_test.go delete mode 100644 turtlefinder/package_test.go delete mode 100644 turtlefinder/sockfinder.go delete mode 100644 turtlefinder/sockfinder_test.go delete mode 100644 turtlefinder/stacker.go delete mode 100644 turtlefinder/stacker_matcher.go delete mode 100644 turtlefinder/stacker_test.go delete mode 100644 turtlefinder/test/cind/Dockerfile delete mode 100644 turtlefinder/test/cind/common.sh delete mode 100644 turtlefinder/test/cind/files/etc/systemd/system/testing.service delete mode 100755 turtlefinder/test/cind/files/testing/setup.sh delete mode 100755 turtlefinder/test/cind/setup.sh delete mode 100755 turtlefinder/test/cind/teardown.sh delete mode 100644 turtlefinder/test/fdscan/proc/1234/fd/0 delete mode 100755 turtlefinder/test/fdscan/proc/5678/fd/3 delete mode 100644 turtlefinder/test/fdscan/proc/5678/fd/junk delete mode 100644 turtlefinder/turtlefinder.go delete mode 100644 turtlefinder/turtlefinder_test.go diff --git a/api/v1/netns.go b/api/v1/netns.go index d29051a..f04d63e 100644 --- a/api/v1/netns.go +++ b/api/v1/netns.go @@ -13,13 +13,12 @@ import ( "strings" "github.com/siemens/ghostwire/v2/network" - "github.com/siemens/ghostwire/v2/turtlefinder" - "golang.org/x/text/cases" - "golang.org/x/text/language" - + "github.com/siemens/turtlefinder" "github.com/thediveo/lxkns/decorator/kuhbernetes" "github.com/thediveo/lxkns/model" "github.com/thediveo/whalewatcher/engineclient/moby" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) var titler = cases.Title(language.Und) @@ -185,7 +184,7 @@ func (n *networkNamespace) marshal(allnetns *networkNamespaces) ([]byte, error) GroupID: gid, ID: cntrID(tenant.Process), Name: c.Name, - Prefix: c.Labels[turtlefinder.GostwireContainerPrefixLabelName], + Prefix: c.Labels[turtlefinder.TurtlefinderContainerPrefixLabelName], Labels: c.Labels, API: c.Engine.API, PID: c.PID, diff --git a/api/v1/package_test.go b/api/v1/package_test.go index e805832..38254e1 100644 --- a/api/v1/package_test.go +++ b/api/v1/package_test.go @@ -11,13 +11,12 @@ import ( "time" "github.com/getkin/kin-openapi/openapi3" - "github.com/thediveo/lxkns/decorator/kuhbernetes" - gostwire "github.com/siemens/ghostwire/v2" "github.com/siemens/ghostwire/v2/decorator/ieappicon" "github.com/siemens/ghostwire/v2/test/nerdctl" - "github.com/siemens/ghostwire/v2/turtlefinder" "github.com/siemens/ghostwire/v2/util" + "github.com/siemens/turtlefinder" + "github.com/thediveo/lxkns/decorator/kuhbernetes" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -106,7 +105,7 @@ var _ = BeforeSuite(func() { ) for _, c := range disco.Lxkns.Containers { if c.Name == bareName { - c.Labels[turtlefinder.GostwireContainerPrefixLabelName] = barePrefix + c.Labels[turtlefinder.TurtlefinderContainerPrefixLabelName] = barePrefix break } } diff --git a/api/v1/targets.go b/api/v1/targets.go index 4d30bc6..1947ecf 100644 --- a/api/v1/targets.go +++ b/api/v1/targets.go @@ -7,10 +7,9 @@ package v1 import ( "strings" - "github.com/siemens/ghostwire/v2/network" - "github.com/siemens/ghostwire/v2/turtlefinder" - gostwire "github.com/siemens/ghostwire/v2" + "github.com/siemens/ghostwire/v2/network" + "github.com/siemens/turtlefinder" "github.com/thediveo/lxkns/decorator/kuhbernetes" "github.com/thediveo/lxkns/model" ) @@ -108,7 +107,7 @@ func newCaptureTargets(dr gostwire.DiscoveryResult) captureTargets { NifNames: nifNames, PID: tenant.Process.PID, Starttime: tenant.Process.Starttime, - Prefix: container.Labels[turtlefinder.GostwireContainerPrefixLabelName], + Prefix: container.Labels[turtlefinder.TurtlefinderContainerPrefixLabelName], }) } } diff --git a/cmd/gostdump/main.go b/cmd/gostdump/main.go index 9c5ccb6..428db00 100644 --- a/cmd/gostdump/main.go +++ b/cmd/gostdump/main.go @@ -9,10 +9,9 @@ import ( "encoding/json" "fmt" - apiv1 "github.com/siemens/ghostwire/v2/api/v1" - "github.com/siemens/ghostwire/v2/turtlefinder" - gostwire "github.com/siemens/ghostwire/v2" + apiv1 "github.com/siemens/ghostwire/v2/api/v1" + "github.com/siemens/turtlefinder" ) func main() { diff --git a/cmd/gostwire/cmdroot.go b/cmd/gostwire/cmdroot.go index f8d462d..e3923c0 100644 --- a/cmd/gostwire/cmdroot.go +++ b/cmd/gostwire/cmdroot.go @@ -15,7 +15,7 @@ import ( "time" "unsafe" - "github.com/siemens/ghostwire/v2/turtlefinder" + "github.com/siemens/turtlefinder" gostwire "github.com/siemens/ghostwire/v2" diff --git a/cmd/lsallnifs/rootcmd.go b/cmd/lsallnifs/rootcmd.go index 0206b7b..4a0a3fd 100644 --- a/cmd/lsallnifs/rootcmd.go +++ b/cmd/lsallnifs/rootcmd.go @@ -9,10 +9,9 @@ import ( "fmt" "strings" - "github.com/siemens/ghostwire/v2/network" - "github.com/siemens/ghostwire/v2/turtlefinder" - gostwire "github.com/siemens/ghostwire/v2" + "github.com/siemens/ghostwire/v2/network" + "github.com/siemens/turtlefinder" "github.com/spf13/cobra" "github.com/thediveo/lxkns/log" diff --git a/decorator/dockernet/dockernet_test.go b/decorator/dockernet/dockernet_test.go index 83cf12b..476cd51 100644 --- a/decorator/dockernet/dockernet_test.go +++ b/decorator/dockernet/dockernet_test.go @@ -10,13 +10,12 @@ import ( "os" "time" + "github.com/ory/dockertest/v3" + dtclient "github.com/ory/dockertest/v3/docker" "github.com/siemens/ghostwire/v2/decorator" "github.com/siemens/ghostwire/v2/internal/discover" "github.com/siemens/ghostwire/v2/network" - "github.com/siemens/ghostwire/v2/turtlefinder" - - "github.com/ory/dockertest/v3" - dtclient "github.com/ory/dockertest/v3/docker" + "github.com/siemens/turtlefinder" "github.com/thediveo/go-plugger/v3" lxknsdiscover "github.com/thediveo/lxkns/discover" "github.com/thediveo/lxkns/model" diff --git a/decorator/nerdctlnet/nerdctlnet_test.go b/decorator/nerdctlnet/nerdctlnet_test.go index 27404b5..448fc19 100644 --- a/decorator/nerdctlnet/nerdctlnet_test.go +++ b/decorator/nerdctlnet/nerdctlnet_test.go @@ -10,19 +10,19 @@ import ( "os" "time" - "github.com/onsi/gomega/gexec" "github.com/siemens/ghostwire/v2/decorator" "github.com/siemens/ghostwire/v2/internal/discover" "github.com/siemens/ghostwire/v2/network" "github.com/siemens/ghostwire/v2/test/nerdctl" - "github.com/siemens/ghostwire/v2/turtlefinder" "github.com/siemens/ghostwire/v2/util" + "github.com/siemens/turtlefinder" "github.com/thediveo/go-plugger/v3" "github.com/thediveo/lxkns/model" "github.com/thediveo/whalewatcher/watcher/containerd" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" . "github.com/onsi/gomega/gleak" . "github.com/thediveo/fdooze" ) diff --git a/discover.go b/discover.go index e5c3a6f..ada9c67 100644 --- a/discover.go +++ b/discover.go @@ -10,7 +10,7 @@ import ( _ "github.com/siemens/ghostwire/v2/decorator/all" // activate all Gostwire-specific decorators. "github.com/siemens/ghostwire/v2/internal/discover" "github.com/siemens/ghostwire/v2/network" - "github.com/siemens/ghostwire/v2/turtlefinder" + "github.com/siemens/turtlefinder" "github.com/thediveo/lxkns/containerizer" lxknsdiscover "github.com/thediveo/lxkns/discover" "github.com/thediveo/lxkns/model" diff --git a/example_test.go b/example_test.go index 7c83bc0..cccef15 100644 --- a/example_test.go +++ b/example_test.go @@ -8,7 +8,7 @@ import ( "context" "fmt" - "github.com/siemens/ghostwire/v2/turtlefinder" + "github.com/siemens/turtlefinder" ) func Example_discovery() { diff --git a/go.mod b/go.mod index bd86b92..7c7a9d5 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( require ( github.com/containerd/log v0.1.0 // indirect gotest.tools v2.2.0+incompatible // indirect + k8s.io/cri-api v0.28.2 // indirect ) require ( @@ -115,6 +116,7 @@ require ( github.com/pelletier/go-toml v1.9.5 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/siemens/turtlefinder v1.0.1 github.com/spf13/pflag v1.0.5 // indirect github.com/thediveo/go-mntinfo v1.0.1 // indirect github.com/thediveo/ioctl v0.9.2 diff --git a/go.sum b/go.sum index 6b16e81..e5f931d 100644 --- a/go.sum +++ b/go.sum @@ -323,6 +323,8 @@ github.com/siemens/ieddata v1.0.0 h1:jS4w5G/XBZ28s48IQfFmocNYkXrTQvMVzCgaWKSXqmg github.com/siemens/ieddata v1.0.0/go.mod h1:klA6Gx4K55NrSp8re+rZb7XuCIL8vI5jWgRYfoghiE4= github.com/siemens/mobydig v1.0.0 h1:kOZ0QYwXGxAuhT+A3Dz1QGoaovxyywVvwEvLC9yETkc= github.com/siemens/mobydig v1.0.0/go.mod h1:TtROWS4S7mfaAFUGmbhaZ9jnUFsdfLWYnLPTi+44g4Q= +github.com/siemens/turtlefinder v1.0.1 h1:CMReXXC5dWu1/JJTLyxZ6ja20GleyVuRD9gcxUNpjIo= +github.com/siemens/turtlefinder v1.0.1/go.mod h1:GCWkpEhDxcOyYN5ZqXpss1uxc70Lbg3TsZdcyQEMlw4= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -645,6 +647,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= honnef.co/go/tools v0.2.2 h1:MNh1AVMyVX23VUHE2O27jm6lNj3vjO5DexS4A1xvnzk= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= +k8s.io/cri-api v0.28.2 h1:RzDo9YY9tkWhAx9/UZEcn6ug1WcvDhU3eA1YLevFreI= +k8s.io/cri-api v0.28.2/go.mod h1:xXygwvSOGcT/2KXg8sMYTHns2xFem3949kCQn5IS1k4= sigs.k8s.io/kind v0.20.0 h1:f0sc3v9mQbGnjBUaqSFST1dwIuiikKVGgoTwpoP33a8= sigs.k8s.io/kind v0.20.0/go.mod h1:aBlbxg08cauDgZ612shr017/rZwqd7AS563FvpWKPVs= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/internal/discover/discover.go b/internal/discover/discover.go index 17d4f93..f24eca9 100644 --- a/internal/discover/discover.go +++ b/internal/discover/discover.go @@ -9,8 +9,7 @@ import ( "github.com/siemens/ghostwire/v2/decorator" "github.com/siemens/ghostwire/v2/network" - "github.com/siemens/ghostwire/v2/turtlefinder" - + "github.com/siemens/turtlefinder" "github.com/thediveo/go-plugger/v3" "github.com/thediveo/lxkns/containerizer" lxknsdiscover "github.com/thediveo/lxkns/discover" diff --git a/metadata/engines/engines_test.go b/metadata/engines/engines_test.go index 8c83d85..2184cc1 100644 --- a/metadata/engines/engines_test.go +++ b/metadata/engines/engines_test.go @@ -9,11 +9,9 @@ import ( "os" "time" - "github.com/siemens/ghostwire/v2/turtlefinder" - - gostwire "github.com/siemens/ghostwire/v2" - "github.com/ory/dockertest" + gostwire "github.com/siemens/ghostwire/v2" + "github.com/siemens/turtlefinder" "github.com/thediveo/lxkns/containerizer" "github.com/thediveo/lxkns/model" "github.com/thediveo/whalewatcher/engineclient/moby" diff --git a/metadata/iecore/iecore_test.go b/metadata/iecore/iecore_test.go index 3a6da75..7d74799 100644 --- a/metadata/iecore/iecore_test.go +++ b/metadata/iecore/iecore_test.go @@ -11,13 +11,12 @@ import ( "sync" "time" - gostwire "github.com/siemens/ghostwire/v2" - "github.com/siemens/ghostwire/v2/turtlefinder" - "github.com/siemens/ieddata" - "github.com/cenkalti/backoff/v4" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" + gostwire "github.com/siemens/ghostwire/v2" + "github.com/siemens/ieddata" + "github.com/siemens/turtlefinder" "github.com/thediveo/lxkns/containerizer" "github.com/thediveo/lxkns/model" "github.com/thediveo/lxkns/species" diff --git a/mobydig/mobydig_test.go b/mobydig/mobydig_test.go index f4e4ee2..7736dac 100644 --- a/mobydig/mobydig_test.go +++ b/mobydig/mobydig_test.go @@ -11,13 +11,12 @@ import ( "os" "time" - lxknsdiscover "github.com/thediveo/lxkns/discover" - "github.com/thediveo/lxkns/model" - "github.com/siemens/ghostwire/v2/internal/discover" "github.com/siemens/ghostwire/v2/network" - "github.com/siemens/ghostwire/v2/turtlefinder" "github.com/siemens/mobydig/messymoby" + "github.com/siemens/turtlefinder" + lxknsdiscover "github.com/thediveo/lxkns/discover" + "github.com/thediveo/lxkns/model" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" diff --git a/mobydig/neighborhood_test.go b/mobydig/neighborhood_test.go index a74fe4f..f863649 100644 --- a/mobydig/neighborhood_test.go +++ b/mobydig/neighborhood_test.go @@ -10,14 +10,13 @@ import ( "os" "time" - lxknsdiscover "github.com/thediveo/lxkns/discover" - "github.com/thediveo/lxkns/model" - "github.com/siemens/ghostwire/v2/decorator/dockernet" "github.com/siemens/ghostwire/v2/internal/discover" "github.com/siemens/ghostwire/v2/network" - "github.com/siemens/ghostwire/v2/turtlefinder" "github.com/siemens/mobydig/messymoby" + "github.com/siemens/turtlefinder" + lxknsdiscover "github.com/thediveo/lxkns/discover" + "github.com/thediveo/lxkns/model" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" diff --git a/test/kind/kind_test.go b/test/kind/kind_test.go index c99e820..5ed872d 100644 --- a/test/kind/kind_test.go +++ b/test/kind/kind_test.go @@ -13,8 +13,8 @@ import ( "os" "github.com/siemens/ghostwire/v2/internal/discover" - "github.com/siemens/ghostwire/v2/turtlefinder" "github.com/siemens/ghostwire/v2/util" + "github.com/siemens/turtlefinder" "github.com/thediveo/lxkns/containerizer" "github.com/thediveo/lxkns/model" "github.com/thediveo/whalewatcher/watcher/moby" @@ -22,6 +22,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" . "github.com/thediveo/success" ) @@ -35,6 +36,18 @@ nodes: const kindTestClusterName = "gw-kind-test" +func withPrefix(prefix string) types.GomegaMatcher { + return WithTransform(func(actual any) (string, error) { + switch container := actual.(type) { + case *model.Container: + return container.Labels[turtlefinder.TurtlefinderContainerPrefixLabelName], nil + case model.Container: + return container.Labels[turtlefinder.TurtlefinderContainerPrefixLabelName], nil + } + return "", fmt.Errorf("withPrefix expects a model.Container or *model.Container, but got %T", actual) + }, Equal(prefix)) +} + var _ = Describe("kind", func() { var prov *cluster.Provider @@ -85,10 +98,10 @@ var _ = Describe("kind", func() { }, "10s", "500ms").Should(ContainElements( SatisfyAll( util.HaveContainer(kindTestClusterName+"-control-plane", moby.Type), - turtlefinder.WithPrefix("")), + withPrefix("")), SatisfyAll( util.HaveContainer(kindTestClusterName+"-worker", moby.Type), - turtlefinder.WithPrefix("")), + withPrefix("")), )) }) @@ -101,16 +114,16 @@ var _ = Describe("kind", func() { }, "10s", "500ms").Should(ContainElements( SatisfyAll( util.FromPod(MatchRegexp(`^kube-system/etcd-%s-control-plane$`, kindTestClusterName)), - turtlefinder.WithPrefix(kindTestClusterName+"-control-plane")), + withPrefix(kindTestClusterName+"-control-plane")), SatisfyAll( util.FromPod(MatchRegexp(`^kube-system/kube-proxy-\w+`)), - turtlefinder.WithPrefix(kindTestClusterName+"-control-plane")), + withPrefix(kindTestClusterName+"-control-plane")), SatisfyAll( util.FromPod(MatchRegexp(`^kube-system/coredns-\w+-\w+$`)), - turtlefinder.WithPrefix(kindTestClusterName+"-control-plane")), + withPrefix(kindTestClusterName+"-control-plane")), SatisfyAll( util.FromPod(MatchRegexp(`^kube-system/kube-proxy-\w+`)), - turtlefinder.WithPrefix(kindTestClusterName+"-worker")), + withPrefix(kindTestClusterName+"-worker")), ), func() string { return fmt.Sprintf("current pod list: %v", util.AllPods(containers)) }) diff --git a/turtlefinder/detector/all/all.go b/turtlefinder/detector/all/all.go deleted file mode 100644 index 61d2213..0000000 --- a/turtlefinder/detector/all/all.go +++ /dev/null @@ -1,10 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package all - -import ( - _ "github.com/siemens/ghostwire/v2/turtlefinder/detector/containerd" // pull in default plugin - _ "github.com/siemens/ghostwire/v2/turtlefinder/detector/moby" // pull in default plugin -) diff --git a/turtlefinder/detector/all/doc.go b/turtlefinder/detector/all/doc.go deleted file mode 100644 index 02136a8..0000000 --- a/turtlefinder/detector/all/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package all pulls in all Gostwire engine detectors. -*/ -package all diff --git a/turtlefinder/detector/all/package_test.go b/turtlefinder/detector/all/package_test.go deleted file mode 100644 index 2a1243a..0000000 --- a/turtlefinder/detector/all/package_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package all - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestGostwireTurtlefinderDetectorAll(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "ghostwire/turtlefinder/detector/all package") -} diff --git a/turtlefinder/detector/all/plugin_test.go b/turtlefinder/detector/all/plugin_test.go deleted file mode 100644 index 152110f..0000000 --- a/turtlefinder/detector/all/plugin_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package all - -import ( - "github.com/siemens/ghostwire/v2/turtlefinder/detector" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/thediveo/go-plugger/v3" -) - -var _ = Describe("detector plugins", func() { - - It("all register the Detector plugin interface and return engine process names", func() { - namers := plugger.Group[detector.Detector]().Symbols() - Expect(namers).To(HaveLen(2)) - names := []string{} - for _, namer := range namers { - names = append(names, namer.EngineNames()...) - } - Expect(names).To(ConsistOf( - "containerd", "dockerd", - )) - }) - -}) diff --git a/turtlefinder/detector/containerd/containerd.go b/turtlefinder/detector/containerd/containerd.go deleted file mode 100644 index 8e2c726..0000000 --- a/turtlefinder/detector/containerd/containerd.go +++ /dev/null @@ -1,73 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package containerd - -import ( - "context" - "sort" - "strings" - "time" - - detect "github.com/siemens/ghostwire/v2/turtlefinder/detector" - - cdclient "github.com/containerd/containerd" - "github.com/thediveo/go-plugger/v3" - "github.com/thediveo/lxkns/log" - "github.com/thediveo/lxkns/model" - cdengine "github.com/thediveo/whalewatcher/engineclient/containerd" - "github.com/thediveo/whalewatcher/watcher" - "github.com/thediveo/whalewatcher/watcher/containerd" -) - -// Register this Docker container (engine) discovery plugin. This statically -// ensures that the Detector interface is fully implemented. -func init() { - plugger.Group[detect.Detector]().Register( - &Detector{}, plugger.WithPlugin("containerd")) -} - -// Detector implements the detect.Detector interface; naming it the same as the -// interface makes the plugin symbol registration autodetect the correct alias -// name. -type Detector struct{} - -// EngineNames returns the process name of the containerd engine process. -func (d *Detector) EngineNames() []string { - return []string{"containerd"} -} - -// NewWatcher returns a watcher for tracking alive containerd containers. -func (d *Detector) NewWatcher(ctx context.Context, pid model.PIDType, apis []string) watcher.Watcher { - sort.Strings(apis) // in-place - for _, apipathname := range apis { - if strings.HasSuffix(apipathname, ".ttrpc") { - continue - } - // As containerd's go client will accept more or less any API pathname - // we throw at it and throw up only when actually trying to communicate - // with the engine and only after some time, it's not sufficient to just - // create the watcher, we also need to check that we actually can - // successfully talk with the daemon. Querying the daemon's version - // information sufficies and ensures that a partiular API path is - // useful. - log.Debugf("dialing containerd endpoint '%s'", apipathname) - w, err := containerd.New(apipathname, nil, cdengine.WithPID(int(pid))) - if err == nil { - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - _, err := w.Client().(*cdclient.Client).Version(ctx) - if err := ctx.Err(); err != nil { - log.Debugf("containerd API Info call context hit deadline: %s", err.Error()) - } - cancel() - if err == nil { - return w - } - w.Close() - } - log.Debugf("containerd API endpoint '%s' failed: %s", apipathname, err.Error()) - } - log.Errorf("no working containerd API endpoint found.") - return nil -} diff --git a/turtlefinder/detector/containerd/containerd_test.go b/turtlefinder/detector/containerd/containerd_test.go deleted file mode 100644 index 50d283c..0000000 --- a/turtlefinder/detector/containerd/containerd_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package containerd - -import ( - "context" - "os" - "time" - - "github.com/siemens/ghostwire/v2/test/nerdctl" - detect "github.com/siemens/ghostwire/v2/turtlefinder/detector" - - "github.com/thediveo/go-plugger/v3" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gleak" - . "github.com/thediveo/fdooze" -) - -// testWorkloadName specifies the name of a Docker container test workload, so -// we're sure that there is a well-defined container to be found. -const testWorkloadName = "gw-turtles-containerd-watch-workload" - -var _ = Describe("containerd turtle watcher", func() { - - Context("with test workload", Ordered, func() { - - BeforeAll(func() { - if os.Getuid() != 0 { - Skip("needs root") - } - nerdctl.SkipWithout() - - nerdctl.NerdctlIgnore("rm", "-f", testWorkloadName) - nerdctl.Nerdctl( - "run", "-d", - "--name", testWorkloadName, - "busybox", "/bin/sleep", "120s") - - goodfds := Filedescriptors() - goodgos := Goroutines() // avoid other failed goroutine tests to spill over - DeferCleanup(func() { - nerdctl.NerdctlIgnore("rm", "-f", testWorkloadName) - Eventually(Goroutines).WithTimeout(5 * time.Second).WithPolling(250 * time.Millisecond). - ShouldNot(HaveLeaked(goodgos)) - Expect(Filedescriptors()).NotTo(HaveLeakedFds(goodfds)) - }) - }) - - It("registers correctly", func() { - Expect(plugger.Group[detect.Detector]().Plugins()).To( - ContainElement("containerd")) - }) - - It("tries unsuccessfully", NodeTimeout(30*time.Second), func(ctx context.Context) { - d := &Detector{} - Expect(d.NewWatcher(ctx, 0, []string{"/etc/rumpelpumpel"})).To(BeNil()) - }) - - It("watches successfully", NodeTimeout(30*time.Second), func(ctx context.Context) { - d := &Detector{} - w := d.NewWatcher(ctx, 0, []string{ - "/etc/rumpelpumpel", - "/var/run/containerd/containerd.sock.ttrpc", - "/var/run/containerd/containerd.sock", - }) - Expect(w).NotTo(BeNil()) - defer w.Close() - go func() { // ...will be ended by cancelling the context - _ = w.Watch(ctx) - }() - Eventually(w.Portfolio().Project("").ContainerNames, - "5s", "250ms").Should(ContainElement(testWorkloadName)) - }) - - }) - -}) diff --git a/turtlefinder/detector/containerd/doc.go b/turtlefinder/detector/containerd/doc.go deleted file mode 100644 index fdd0b49..0000000 --- a/turtlefinder/detector/containerd/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package containerd implements the engine detector for containerd processes. -*/ -package containerd diff --git a/turtlefinder/detector/containerd/package_test.go b/turtlefinder/detector/containerd/package_test.go deleted file mode 100644 index 36893ad..0000000 --- a/turtlefinder/detector/containerd/package_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package containerd - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestGostwireTurtlefinderDetectorContainerd(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "ghostwire/turtlefinder/detector/containerd package") -} diff --git a/turtlefinder/detector/doc.go b/turtlefinder/detector/doc.go deleted file mode 100644 index d971879..0000000 --- a/turtlefinder/detector/doc.go +++ /dev/null @@ -1,10 +0,0 @@ -/* -Package detector defines the plugin interface between the TurtleFinder and its -container engine detector plugins. - -The sub-package “all” pulls in all supported engine detector plugins, that are -supported out-of-the-box. The individual engine-specific detector plugins are -then implemented in the other sub-packages: for instance, the “containerd” and -“moby” sub-packages. -*/ -package detector diff --git a/turtlefinder/detector/moby/doc.go b/turtlefinder/detector/moby/doc.go deleted file mode 100644 index f626916..0000000 --- a/turtlefinder/detector/moby/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package moby implements the engine detector for Docker “dockerd” processes. -*/ -package moby diff --git a/turtlefinder/detector/moby/moby.go b/turtlefinder/detector/moby/moby.go deleted file mode 100644 index 9efe5b5..0000000 --- a/turtlefinder/detector/moby/moby.go +++ /dev/null @@ -1,68 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package moby - -import ( - "context" - "sort" - "time" - - detect "github.com/siemens/ghostwire/v2/turtlefinder/detector" - - "github.com/docker/docker/client" - "github.com/thediveo/go-plugger/v3" - "github.com/thediveo/lxkns/log" - "github.com/thediveo/lxkns/model" - mobyengine "github.com/thediveo/whalewatcher/engineclient/moby" - "github.com/thediveo/whalewatcher/watcher" - "github.com/thediveo/whalewatcher/watcher/moby" -) - -// Register this Docker container (engine) discovery plugin. This statically -// ensures that the Detector interface is fully implemented. -func init() { - plugger.Group[detect.Detector]().Register( - &Detector{}, plugger.WithPlugin("dockerd")) -} - -// Detector implements the detect.Detector interface; naming it the same as the -// interface makes the plugin symbol registration autodetect the correct alias -// name. -type Detector struct{} - -// EngineNames returns the process name of the Docker/moby engine process. -func (d *Detector) EngineNames() []string { - return []string{"dockerd"} -} - -// NewWatcher returns a watcher for tracking alive Docker containers. -func (d *Detector) NewWatcher(ctx context.Context, pid model.PIDType, apis []string) watcher.Watcher { - sort.Strings(apis) // in-place - for _, apipathname := range apis { - // As Docker's go client will accept any API pathname we throw at it and - // throw up only when actually trying to communicate with the engine, - // it's not sufficient to just create the watcher, we also need to check - // that we actually can successfully talk with the daemon. Querying the - // daemon's info sufficies and ensures that a partiular API path is - // useful. - log.Debugf("dialing Docker endpoint 'unix://%s'", apipathname) - w, err := moby.New("unix://"+apipathname, nil, mobyengine.WithPID(int(pid))) - if err == nil { - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - _, err = w.Client().(*client.Client).Info(ctx) - if ctxerr := ctx.Err(); ctxerr != nil { - log.Debugf("Docker API Info call context hit deadline: %s", ctxerr.Error()) - } - cancel() - if err == nil { - return w - } - w.Close() - } - log.Debugf("Docker API endpoint 'unix://%s' failed: %s", apipathname, err.Error()) - } - log.Errorf("no working Docker API endpoint found.") - return nil -} diff --git a/turtlefinder/detector/moby/moby_test.go b/turtlefinder/detector/moby/moby_test.go deleted file mode 100644 index 9b5eb00..0000000 --- a/turtlefinder/detector/moby/moby_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package moby - -import ( - "context" - "os" - "time" - - detect "github.com/siemens/ghostwire/v2/turtlefinder/detector" - - "github.com/ory/dockertest/v3" - "github.com/thediveo/go-plugger/v3" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gleak" - . "github.com/thediveo/fdooze" -) - -// testWorkloadName specifies the name of a Docker container test workload, so -// we're sure that there is a well-defined container to be found. -const testWorkloadName = "gw-turtles-docker-watch-workload" - -var _ = Describe("Docker turtle watcher", func() { - - var pool *dockertest.Pool - - BeforeEach(NodeTimeout(30*time.Second), func(_ context.Context) { - if os.Getuid() != 0 { - Skip("needs root") - } - - var err error - pool, err = dockertest.NewPool("") - Expect(err).NotTo(HaveOccurred()) - _ = pool.RemoveContainerByName(testWorkloadName) - _, err = pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "busybox", - Tag: "latest", - Name: testWorkloadName, - Cmd: []string{"/bin/sleep", "120s"}, - }) - Expect(err).NotTo(HaveOccurred(), "creating container %s", testWorkloadName) - - goodfds := Filedescriptors() - goodgos := Goroutines() // avoid other failed goroutine tests to spill over - DeferCleanup(NodeTimeout(30*time.Second), func(_ context.Context) { - _ = pool.RemoveContainerByName(testWorkloadName) - Eventually(Goroutines).WithTimeout(5 * time.Second).WithPolling(250 * time.Millisecond). - ShouldNot(HaveLeaked(goodgos)) - Expect(Filedescriptors()).NotTo(HaveLeakedFds(goodfds)) - }) - }) - - Context("with test workload", func() { - - It("registers correctly", func() { - Expect(plugger.Group[detect.Detector]().Plugins()).To( - ContainElement("dockerd")) - }) - - It("tries unsuccessfully", NodeTimeout(30*time.Second), func(ctx context.Context) { - d := &Detector{} - Expect(d.NewWatcher(ctx, 0, []string{"/etc/rumpelpumpel"})).To(BeNil()) - }) - - It("watches successfully", NodeTimeout(30*time.Second), func(ctx context.Context) { - d := &Detector{} - w := d.NewWatcher(ctx, 0, []string{"/etc/rumpelpumpel", "/var/run/docker/metrics.sock", "/var/run/docker.sock"}) - Expect(w).NotTo(BeNil()) - defer w.Close() - go func() { // ...will be ended by cancelling the context - _ = w.Watch(ctx) - }() - Eventually(w.Portfolio().Project("").ContainerNames, - "5s", "250ms").Should(ContainElement(testWorkloadName)) - }) - - }) - -}) diff --git a/turtlefinder/detector/moby/package_test.go b/turtlefinder/detector/moby/package_test.go deleted file mode 100644 index ce97b88..0000000 --- a/turtlefinder/detector/moby/package_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package moby - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestGostwireTurtlefinderDetectorMoby(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "ghostwire/turtlefinder/detector/moby package") -} diff --git a/turtlefinder/detector/plugin.go b/turtlefinder/detector/plugin.go deleted file mode 100644 index 8d33dc4..0000000 --- a/turtlefinder/detector/plugin.go +++ /dev/null @@ -1,25 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package detector - -import ( - "context" - - "github.com/thediveo/lxkns/model" - "github.com/thediveo/whalewatcher/watcher" -) - -// Detector allows specialized container engine detector plugins to interface -// with the generic engine discovery mechanism. -type Detector interface { - // EngineNames returns one or more process name(s) of a specific type of - // container engine. - EngineNames() []string - - // NewWatcher returns a watcher for tracking alive containers of the - // container engine accessible by at least one of the specified API - // pathnames. - NewWatcher(ctx context.Context, pid model.PIDType, apis []string) watcher.Watcher -} diff --git a/turtlefinder/doc.go b/turtlefinder/doc.go deleted file mode 100644 index 9fb66ec..0000000 --- a/turtlefinder/doc.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Package turtlefinder provides a Containerizer that auto-detects container -engines and their containers. The turtlefinder containerizer can be used by -concurrent discoveries. - -All that is necessary: - - containerizer := turtlefinders.New() - -Boringly simple, right? - -Basically, upon a container query the turtlefinder containerizer first looks for -any newly seen container engines, based on container engine process names. The -engine discovery can be extended by pluging in new engine detectors (and -adaptors). Additionally, the turtlefinder determines the hierarchy of container -engines, such as when a container engine is hosted inside a container managed by -a (parent) container engine. This hierarchy later gets propagated to the -individual containers in form of a so-called “prefix”, attached in form of a -special container label. - -The turtlefinder then spins up background watchers as required that synchronize -with the workload states. Old engine watchers get retired as their engine -processes die. This workload state information is then returned as the list of -discovered containers. - -The decoration of the discovered containers is then done as usual by the -(extensible) lxkns decorator mechanism as part of the overall discovery. -*/ -package turtlefinder diff --git a/turtlefinder/engine.go b/turtlefinder/engine.go deleted file mode 100644 index 1d60e65..0000000 --- a/turtlefinder/engine.go +++ /dev/null @@ -1,104 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package turtlefinder - -import ( - "context" - "time" - - "github.com/thediveo/lxkns/log" - "github.com/thediveo/lxkns/model" - "github.com/thediveo/whalewatcher/watcher" -) - -// Engine watches a container engine for signs of container life, using a -// supplied "whale watcher". Engine objects then can be queried for a list of -// currently alive (running/paused) containers they manage. -type Engine struct { - watcher.Watcher // engine watcher (doubles as engine adapter). - ID string // engine ID. - Version string // engine version. - Done chan struct{} // closed when watch is done/has terminated. -} - -// NewEngine returns a new Engine given the specified watcher. The Engine is -// already "warming up" and has started watching (using the given context). -func NewEngine(ctx context.Context, watch watcher.Watcher) *Engine { - idctx, cancel := context.WithTimeout(ctx, 2*time.Second) - e := &Engine{ - Watcher: watch, - ID: watch.ID(idctx), - Version: watch.Version(idctx), - Done: make(chan struct{}, 1), // might never be picked up in some situations - } - cancel() // ensure to quickly release cancel, silence linter - log.Infof("watching %s container engine (PID %d) with ID '%s', version '%s'", - watch.Type(), watch.PID(), e.ID, e.Version) - go func() { - err := e.Watcher.Watch(ctx) - log.Infof("stopped watching container engine (PID %d), reason: %s", - watch.PID(), err.Error()) - close(e.Done) - e.Close() - }() - return e -} - -// Containers returns the alive containers managed by this engine, using the -// associated watcher. -func (e *Engine) Containers(ctx context.Context) []*model.Container { - eng := &model.ContainerEngine{ - ID: e.ID, - Type: e.Watcher.Type(), - Version: e.Version, - API: e.Watcher.API(), - PID: model.PIDType(e.Watcher.PID()), - } - // Adapt the whalewatcher container model to the lxkns container model, - // where the latter takes container engines and groups into account of its - // information model. We only need to set the container engine, as groups - // will be handled separately by the various (lxkns) decorators. - for _, projname := range append(e.Watcher.Portfolio().Names(), "") { - project := e.Watcher.Portfolio().Project(projname) - if project == nil { - continue - } - for _, container := range project.Containers() { - // Ouch! Make sure to clone the Labels map and not simply pass it - // directly on to our ontainer objects. Otherwise decorators adding - // labels would modify the labels shared through the underlying - // container label source. So, clone the labels (top-level only) and - // then happy decorating. - clonedLabels := model.Labels{} - for k, v := range container.Labels { - clonedLabels[k] = v - } - cntr := &model.Container{ - ID: container.ID, - Name: container.Name, - Type: eng.Type, - Flavor: eng.Type, - PID: model.PIDType(container.PID), - Paused: container.Paused, - Labels: clonedLabels, - Engine: eng, - } - eng.AddContainer(cntr) - } - } - return eng.Containers -} - -// IsAlive returns true as long as the engine watcher is operational and hasn't -// permanently failed/terminated. -func (e *Engine) IsAlive() bool { - select { - case <-e.Done: - return false - default: - // nothing to see, move on! - } - return true -} diff --git a/turtlefinder/engine_test.go b/turtlefinder/engine_test.go deleted file mode 100644 index d5b39f4..0000000 --- a/turtlefinder/engine_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package turtlefinder - -import ( - "context" - "time" - - "github.com/siemens/ghostwire/v2/util" - - "github.com/onsi/gomega/types" - "github.com/ory/dockertest/v3" - "github.com/thediveo/lxkns/model" - "github.com/thediveo/whalewatcher/watcher/moby" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gleak" - . "github.com/thediveo/fdooze" -) - -// testEngineWorkloadName specifies the name of a Docker container test -// workload, so we're sure that there is a well-defined container to be found. -const testEngineWorkloadName = "gw-turtles-testengine-workload" - -func HaveEngine(typ string, apiregex string) types.GomegaMatcher { - return And( - HaveField("Type", typ), - HaveField("API", MatchRegexp(apiregex))) -} - -var _ = Describe("container engine", func() { - - BeforeEach(func() { - goodfds := Filedescriptors() - goodgos := Goroutines() // avoid other failed goroutine tests to spill over - DeferCleanup(func() { - Eventually(Goroutines).WithTimeout(2 * time.Second).WithPolling(250 * time.Millisecond). - ShouldNot(HaveLeaked(goodgos)) - Expect(Filedescriptors()).NotTo(HaveLeakedFds(goodfds)) - }) - }) - - It("tracks an engine", NodeTimeout(30*time.Second), func(ctx context.Context) { - w, err := moby.New("", nil) - Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(ctx) - defer cancel() - e := NewEngine(ctx, w) - Expect(e.ID).NotTo(BeZero()) - - Consistently(e.IsAlive).Should(BeTrue()) - - pool, err := dockertest.NewPool("") - Expect(err).NotTo(HaveOccurred()) - _ = pool.RemoveContainerByName(testEngineWorkloadName) - _, err = pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "busybox", - Tag: "latest", - Name: testEngineWorkloadName, - Cmd: []string{"/bin/sleep", "120s"}, - }) - Expect(err).NotTo(HaveOccurred(), "creating container %s", testEngineWorkloadName) - defer func() { _ = pool.RemoveContainerByName(testEngineWorkloadName) }() - - // Give leeway for the container workload discovery to reflect the - // correct situation even under heavy system load. And remember to pass - // a function to Eventually, not a result ;) - Eventually(func() []*model.Container { - return e.Containers(ctx) - }, "10s", "500ms").Should( - ContainElement(util.HaveContainerNameID(testEngineWorkloadName)), - "missing container %s", testEngineWorkloadName) - - cancel() - Eventually(e.IsAlive).Should(BeFalse()) - }) - -}) diff --git a/turtlefinder/package_test.go b/turtlefinder/package_test.go deleted file mode 100644 index 6f7a909..0000000 --- a/turtlefinder/package_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package turtlefinder - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestGostwireTurtlefinder(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "ghostwire/turtlefinder package") -} diff --git a/turtlefinder/sockfinder.go b/turtlefinder/sockfinder.go deleted file mode 100644 index 4eb8909..0000000 --- a/turtlefinder/sockfinder.go +++ /dev/null @@ -1,137 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package turtlefinder - -import ( - "bufio" - "io/ioutil" - "os" - "strconv" - "strings" - - "github.com/thediveo/lxkns/model" -) - -// soAcceptCon is the state bit mask to identify listening unix domain sockets. -const soAcceptCon = 1 << 16 - -// soStream is the type of a connection-oriented/streaming unix domain socket. -const soStream = 1 - -// discoverAPISockets returns a list of (potential) listening API unix domain -// sockets for a specific process. The PID of the process must be valid in the -// current mount namespace and a correct proc filesystem must have been -// (re)mounted in this mount namespace, otherwise only an empty list will be -// returned. The easiest way is to do this with a PID valid in the initial PID -// namespace and with a correct proc in the current mount namespace that has -// full "host:pid" view. -func discoverAPISockets(pid model.PIDType) []string { - var listeningSox = discoverListeningSox(pid) - return matchProcSox(pid, listeningSox) -} - -// socketPathsByIno maps the inode numbers of (unix domain) sockets to their -// corresponding path names. This map will not contain unix domain sockets from -// Linux' "abstract namespace" (see also: -// http://man7.org/linux/man-pages/man7/unix.7.html). -type socketPathsByIno map[uint64]string - -// matchProcSox scans the open file descriptors ("fd") of the specified process -// for known listening sockets, passed in by listeningSox. The process is -// identified by its PID that needs to be usable with the proc file system -// mounted in the current mount namespace. -func matchProcSox(pid model.PIDType, listeningSox socketPathsByIno) (socketpaths []string) { - fdbase := "/proc/" + strconv.FormatUint(uint64(pid), 10) + "/fd" - fdentries, err := ioutil.ReadDir(fdbase) - if err != nil { - return - } - fdbase += "/" - // Scan all directory entries below the process' /proc/[PID]/fd directory: - // these represent the individual open file descriptors of this process. - // They are links (rather: pseudo-symbolic links) to their corresponding - // resources, such as file names, sockets, et cetera. For sockets, we can - // only learn a socket's inode number, but neither its type, nor state. - // Thus we need the sockets-by-inode dictionary to check whether a fd - // references something of interest to us and the filesystem path it - // points to (as usual, subject to the current mount namespace). - for _, fdentry := range fdentries { - fdlink, err := os.Readlink(fdbase + fdentry.Name()) - if err == nil && strings.HasPrefix(fdlink, "socket:[") { - ino, err := strconv.ParseUint(fdlink[8:len(fdlink)-1], 10, 64) - if err == nil { - if soxpath, ok := listeningSox[ino]; ok { - socketpaths = append(socketpaths, soxpath) - } - } - } - } - return -} - -// discoverListeningSox returns a map of (named) unix domain sockets in -// listening state in the mount namspace to which the specified process is -// attached to.. The map specifies for each listening unix domain socket both -// its inode number as the key and path as value. -func discoverListeningSox(pid model.PIDType) socketPathsByIno { - sox := socketPathsByIno{} - // Try to open the list of unix domain sockets currently present in the - // system. - // - // Note 1: please note that this list is subject to mount namespace this - // process is joined to. Some documentation and blog posts erroneously - // indicate that this list is controlled by the current network namespace - // (as "/proc/net/" might suggest), but without ever checking. However, when - // thinking about it, this doesn't make any sense at all, as unix domain - // sockets have names that are filesystem paths, so it does make sense that - // the mount namespace gets control, but not the network namespace. /rant - // - // Note 2: lesser known, files in a different mount namespaces can be - // directly accessed via the proc filesystem if there's a process attached - // to the mount namespace. These wormholes are "/proc/[PID]/root/" and - // predate Linux mount namespaces by quite some eons, dating back to - // "chroot". The wormholes save us from needing to re-execute in order to - // access a container engine API endpoint in a different mount namespace. - // This improves performance, as we can keep in-process and even - // aggressively parallelize talking to engines. - // - // It's "incontinentainers", after all. - uf, err := os.Open("/proc/" + strconv.FormatUint(uint64(pid), 10) + - "/net/unix") - if err != nil { - return nil - } - defer func() { _ = uf.Close() }() // otherwise gosec goes berserk over this, oh my! - // Each line from /proc/[PID]/net/unix lists one socket with its state - // ("flags"), type, etc. For precise field semantics, please see: - // https://elixir.bootlin.com/linux/v5.0.3/source/net/unix/af_unix.c#L2831 - // -- this line of code generates a single line in /proc/net/unix. - soxscan := bufio.NewScanner(uf) - for soxscan.Scan() { - fields := strings.Split(soxscan.Text(), " ") - if len(fields) < 8 { - continue - } - flags, err := strconv.ParseUint(fields[3], 16, 32) - if err != nil { - continue - } - soxtype, err := strconv.ParseUint(fields[4], 16, 16) - if err != nil { - continue - } - path := fields[7] - // If this ain't ;) a unix socket in listening mode, then skip it. - if soxtype != soStream || flags != soAcceptCon { - continue - } - ino, err := strconv.ParseUint(fields[6], 10, 64) - if err != nil { - continue - } - sox[ino] = path // finally map the socket's inode number to its path. - } - return sox -} diff --git a/turtlefinder/sockfinder_test.go b/turtlefinder/sockfinder_test.go deleted file mode 100644 index ab71a0c..0000000 --- a/turtlefinder/sockfinder_test.go +++ /dev/null @@ -1,63 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package turtlefinder - -import ( - "net" - "os" - "syscall" - "time" - - "github.com/thediveo/lxkns/model" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gleak" - . "github.com/thediveo/fdooze" -) - -const testListeningUnixSocketPath = "/tmp/gw-turtlefinder-test.sock" - -var _ = Describe("socket finder", func() { - - BeforeEach(func() { - goodfds := Filedescriptors() - goodgos := Goroutines() // avoid other failed goroutine tests to spill over - DeferCleanup(func() { - Eventually(Goroutines).WithTimeout(2 * time.Second).WithPolling(250 * time.Millisecond). - ShouldNot(HaveLeaked(goodgos)) - Expect(Filedescriptors()).NotTo(HaveLeakedFds(goodfds)) - }) - }) - - It("finds Docker API unix socket", func() { - sox := discoverListeningSox(model.PIDType(os.Getpid())) - Expect(sox).To(ContainElement("/run/docker.sock")) - }) - - It("doesn't find non-existing canary listening unix socket", func() { - soxpaths := matchProcSox( - model.PIDType(os.Getpid()), - discoverListeningSox(model.PIDType(os.Getpid()))) - Expect(soxpaths).NotTo(ContainElement(testListeningUnixSocketPath)) - }) - - It("finds listening canary unix socket", func() { - _ = syscall.Unlink(testListeningUnixSocketPath) - - lsock, err := net.Listen("unix", testListeningUnixSocketPath) - Expect(err).NotTo(HaveOccurred()) - defer func() { - _ = lsock.Close() - _ = syscall.Unlink(testListeningUnixSocketPath) - }() - - soxpaths := matchProcSox( - model.PIDType(os.Getpid()), - discoverListeningSox(model.PIDType(os.Getpid()))) - Expect(soxpaths).To(ContainElement(testListeningUnixSocketPath)) - }) - -}) diff --git a/turtlefinder/stacker.go b/turtlefinder/stacker.go deleted file mode 100644 index e44cefc..0000000 --- a/turtlefinder/stacker.go +++ /dev/null @@ -1,129 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package turtlefinder - -import ( - "github.com/thediveo/lxkns/model" -) - -// GostwireContainerPrefixLabelName defines the label name for attaching prefix -// information about the engine-hierarchy to containers. Discovery client can -// use these container labels to find out the hierarchy of containers. For -// instance, if container "A" is managed by a container engine hosted inside -// container "B", then container "A" is labelled with prefix "B". -const GostwireContainerPrefixLabelName = "gostwire/container/prefix" - -// PrefixSeparator is the separator used in hierarchical prefixes. -const PrefixSeparator = "/" - -// stackedEngine temporarily stores additional details about a container engine -// while we figure out if and how engines have been stacked, or rather, put into -// each other. -type stackedEngine struct { - EncloserName string // name derived from enclosing container, if any, otherwise "". - EncloserEnginePID model.PIDType // PID of engine PID managing the enclosing container, if any, otherwise 0. - Prefix string // hierarchical prefix for this engine, or "". - Parent *stackedEngine // parent container engine, if any. - Children []*stackedEngine // child container engines, if any. -} - -// Add a stackedEngine as the child of this stackedEngine, at the same time also -// setting this stackedEngine to be the parent of the added stackedEngine. -func (e *stackedEngine) Add(child *stackedEngine) { - child.Parent = e - e.Children = append(e.Children, child) -} - -// stackEngines discovers the hierarchical relationships (if any) between -// container engines, that is, when one engine is running inside a container -// managed by another container engine. -func stackEngines(containers []*model.Container, engines []*Engine, proctable model.ProcessTable) { - // Let's build an index for mapping the PIDs of the containers' initial - // processes to their containers. - containersByPID := map[model.PIDType]*model.Container{} - for _, container := range containers { - containersByPID[container.PID] = container - } - // Index the list of engines we were told, in order to quickly look up the - // additional information we need to associate with the engines during the - // stacking process. The index key is an engine's PID, as this is the only - // correct link between a model.ContainerEngine and a turtlefinder.Engine. - // We also use this chance to see if an engine is inside a container and in - // which one in particular. - stackedEngines := map[model.PIDType]*stackedEngine{} - for _, engine := range engines { - // Climb up the process tree until we either hit a container PID or we - // fall off the ... root? Okay, another +1 on the eternal counter of - // really bad metaphors. - var ( - name string - outerEnginePID model.PIDType - container *model.Container - ) - proc := proctable[model.PIDType(engine.PID())] - for proc != nil { - var ok bool - if container, ok = containersByPID[proc.PID]; ok { - name = container.Name - outerEnginePID = container.Engine.PID - break - } - // rinse and repeat until container PID hit or falling off root. - proc = proc.Parent - } - stackedEngines[model.PIDType(engine.PID())] = &stackedEngine{ - EncloserName: name, - EncloserEnginePID: outerEnginePID, - } - } - // Now that we know which engines are containerized, set these engines to be - // children of the container engines managing the engine-enclosing - // containers. Hopefully, we end up with some hierarchy. While it is not - // strictly necessary to explicitly build this engine hierarchy, it helps - // with detecting sibling engines in the same context, such as side-by-side - // engines in the host or in some container. - var nullEngine = &stackedEngine{} // acts as "fake" root - for _, engine := range stackedEngines { - if pid := engine.EncloserEnginePID; pid != 0 { - if parentEngine := stackedEngines[pid]; parentEngine != nil { - parentEngine.Add(engine) - continue - } - } - nullEngine.Add(engine) - } - // Next, we can finally determine the engine prefixes ("turtle paths") based - // on the discovered engine hierarchy. Looks like a recursion allergy ;) - for _, engine := range stackedEngines { - prefix := "" - eng := engine - for eng != nil { - if eng.EncloserName != "" { - if prefix == "" { - prefix = eng.EncloserName - } else { - prefix = eng.EncloserName + PrefixSeparator + prefix - } - } - eng = eng.Parent - } - engine.Prefix = prefix - } - // Finally distribute the per-engine prefixes to the individual containers; - // the prefixes are attached as Gostwire-specific container labels. - var engine *model.ContainerEngine - var cachedEnginePrefix string - for _, container := range containers { - if container.Engine != engine { - engine = container.Engine - if steng, ok := stackedEngines[engine.PID]; ok { - cachedEnginePrefix = steng.Prefix - } else { - cachedEnginePrefix = "" - } - } - container.Labels[GostwireContainerPrefixLabelName] = cachedEnginePrefix - } -} diff --git a/turtlefinder/stacker_matcher.go b/turtlefinder/stacker_matcher.go deleted file mode 100644 index bf9ad46..0000000 --- a/turtlefinder/stacker_matcher.go +++ /dev/null @@ -1,29 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -//go:build matchers -// +build matchers - -package turtlefinder - -import ( - "fmt" - - "github.com/onsi/gomega/types" - "github.com/thediveo/lxkns/model" - - . "github.com/onsi/gomega" -) - -func WithPrefix(prefix string) types.GomegaMatcher { - return WithTransform(func(actual interface{}) (string, error) { - switch container := actual.(type) { - case *model.Container: - return container.Labels[GostwireContainerPrefixLabelName], nil - case model.Container: - return container.Labels[GostwireContainerPrefixLabelName], nil - } - return "", fmt.Errorf("WithPrefix expects a model.Container or *model.Container, but got %T", actual) - }, Equal(prefix)) -} diff --git a/turtlefinder/stacker_test.go b/turtlefinder/stacker_test.go deleted file mode 100644 index 6db81dd..0000000 --- a/turtlefinder/stacker_test.go +++ /dev/null @@ -1,120 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package turtlefinder - -import ( - "context" - "os" - "time" - - "github.com/siemens/ghostwire/v2/util" - - "os/exec" - - "github.com/onsi/gomega/gexec" - lxknsdiscover "github.com/thediveo/lxkns/discover" - "github.com/thediveo/lxkns/model" - "github.com/thediveo/lxkns/species" - "github.com/thediveo/whalewatcher/watcher/containerd" - "github.com/thediveo/whalewatcher/watcher/moby" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - . "github.com/onsi/gomega/gleak" - . "github.com/thediveo/fdooze" -) - -var _ = Describe("turtles and elephants", func() { - - BeforeEach(func() { - goodfds := Filedescriptors() - goodgos := Goroutines() // avoid other failed goroutine tests to spill over - DeferCleanup(func() { - Eventually(Goroutines).WithTimeout(2 * time.Second).WithPolling(250 * time.Millisecond). - ShouldNot(HaveLeaked(goodgos)) - Expect(Filedescriptors()).NotTo(HaveLeakedFds(goodfds)) - }) - }) - - It("get prefixed and stacked, not stucked", NodeTimeout(60*time.Second), func(ctx context.Context) { - if os.Getuid() != 0 { - Skip("needs root") - } - - By("creating a turtle finder") - watcherctx, watchercancel := context.WithCancel(ctx) - tf := New(func() context.Context { return watcherctx }) - Expect(tf).NotTo(BeNil()) - defer watchercancel() - defer tf.Close() - - By("stopping any old left-over containerized container engine") - session, err := gexec.Start(exec.Command("./test/cind/teardown.sh"), - GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - Eventually(ctx, session).Should(gexec.Exit()) - - By("discovering engines") - // to get out of an import cycle we need to do the discover ourselves - // ... but we can trim this down to only those steps we need here. - discover := func() *lxknsdiscover.Result { - return lxknsdiscover.Namespaces( - lxknsdiscover.FromProcs(), - lxknsdiscover.FromBindmounts(), - lxknsdiscover.WithNamespaceTypes( - species.CLONE_NEWNET|species.CLONE_NEWPID|species.CLONE_NEWNS|species.CLONE_NEWUTS), - lxknsdiscover.WithHierarchy(), - lxknsdiscover.WithContainerizer(tf), - lxknsdiscover.WithPIDMapper(), - ) - } - - _ = discover() - Expect(tf.Engines()).To(ContainElements( - HaveEngine(moby.Type, `^unix:///proc/\d+/root/run/docker.sock$`), - HaveEngine(containerd.Type, `^unix:///proc/\d+/root/run/containerd/containerd.sock$`), - )) - - By("starting an additional container engine in a container") - session, err = gexec.Start(exec.Command("./test/cind/setup.sh"), - GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - Eventually(ctx, session).Should(gexec.Exit(0)) - defer func() { // safety net - session, err := gexec.Start(exec.Command("./test/cind/teardown.sh"), - GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - Eventually(session).Within(20 * time.Second).ProbeEvery(100 * time.Millisecond).Should(gexec.Exit()) - }() - - By("turtle finder catching up") - Eventually(ctx, func() int { - _ = discover() - return tf.EngineCount() - }, "5s", "250ms").Should(Equal(3)) - - containers := discover().Containers - Expect(containers).To(ContainElement( - SatisfyAll( - util.HaveContainerNameID("cind-sleepy"), - WithTransform( - func(actual *model.Container) model.Labels { return actual.Labels }, - HaveKeyWithValue(GostwireContainerPrefixLabelName, "containerd-in-docker"))))) - - By("stopping the containerized container engine") - session, err = gexec.Start(exec.Command("./test/cind/teardown.sh"), - GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - Eventually(ctx, session).Should(gexec.Exit()) - - By("running a final engine discovery") - Eventually(ctx, func() int { - _ = discover() - return tf.EngineCount() - }, "5s", "250ms").Should(Equal(2)) - }) - -}) diff --git a/turtlefinder/test/cind/Dockerfile b/turtlefinder/test/cind/Dockerfile deleted file mode 100644 index 997b509..0000000 --- a/turtlefinder/test/cind/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -# Our test harness image is based off the kindest/base Kubernetes-in-Docker base -# image, all built locally, so we can also build for architectures where there -# are no pre-built kindest/base images available. - -# Thanks to https://github.com/moby/moby/pull/31352, it is possible to -# parameterize the FROM statement, yay! -ARG KINDBASE_IMAGE -FROM ${KINDBASE_IMAGE} -COPY files/ / -RUN mkdir -p /kind \ - && echo "Installing packages..." \ - && apt-get update \ - && apt-get install -y socat \ - && echo "Enabling systemd testing service..." \ - && systemctl enable testing -ENTRYPOINT [ "/usr/local/bin/entrypoint", "/sbin/init" ] diff --git a/turtlefinder/test/cind/common.sh b/turtlefinder/test/cind/common.sh deleted file mode 100644 index 40c451d..0000000 --- a/turtlefinder/test/cind/common.sh +++ /dev/null @@ -1,12 +0,0 @@ -# Work from kindest/base image for linux/amd64 and linux/arm64. -KINDBASE_IMAGE="kindest/base:v20230512-4855a7f1" - -# Our image name:tag for a containerd-in-Docker testing image. -CIND_IMAGE="lxkns/cind:inception" - -# Name of test container instance. -CNTR_NAME="${CNTR_NAME:-containerd-in-docker}" - -# While waiting for the containerd docker container to boot and start its -# inner testing container, print a "." every n lines of container log output. -LINESPERDOT=25 diff --git a/turtlefinder/test/cind/files/etc/systemd/system/testing.service b/turtlefinder/test/cind/files/etc/systemd/system/testing.service deleted file mode 100644 index f6ef542..0000000 --- a/turtlefinder/test/cind/files/etc/systemd/system/testing.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=containerd testing setup -After=containerd.service - -[Service] -Type=oneshot -ExecStart=/testing/setup.sh -StandardOutput=journal+console - -[Install] -WantedBy=multi-user.target diff --git a/turtlefinder/test/cind/files/testing/setup.sh b/turtlefinder/test/cind/files/testing/setup.sh deleted file mode 100755 index 0d36796..0000000 --- a/turtlefinder/test/cind/files/testing/setup.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -CNTR_IMG="docker.io/library/busybox:latest" - -# Wait for containerd's API socket to become useable... -until socat -u OPEN:/dev/null UNIX-CONNECT:/var/run/containerd/containerd.sock; do - echo "waiting for containerd..." - sleep 0.5 -done - -# Spin up a test container... -ctr image pull "${CNTR_IMG}" -ctr run \ - --label nerdctl/name=cind-sleepy \ - --read-only \ - --snapshotter=native "${CNTR_IMG}" \ - sleepy /bin/sh -c 'echo "SLEEPY READY"; sleep 1000000' - -exit 0 diff --git a/turtlefinder/test/cind/setup.sh b/turtlefinder/test/cind/setup.sh deleted file mode 100755 index 3754585..0000000 --- a/turtlefinder/test/cind/setup.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -TESTDIR=$(dirname "$0") -. "${TESTDIR}/common.sh" - -# Stop and get rid of any old running instance, as we want to start anew. -docker rm -f ${CNTR_NAME} - -# Build the testing image, if not already cached. -docker build --build-arg "KINDBASE_IMAGE=${KINDBASE_IMAGE}" -t ${CIND_IMAGE} ${TESTDIR} - -# Fire it up... -docker run -d -it --rm --name ${CNTR_NAME} --privileged ${CIND_IMAGE} || exit 1 - -# Now wait for the test container to have spun up and be ready... -exec 3< <(docker logs -f ${CNTR_NAME} /dev/null) -DOCKERLOGS_PID=$! -NEXTDOT=0 -echo -n "waiting for test container" -while IFS= read -r <&3 LINE; do - if echo "$LINE" | grep -q "SLEEPY READY"; then - break - fi - ((NEXTDOT--)) - if (( ${NEXTDOT} <= 0 )); then - echo -n "." - NEXTDOT=${LINESPERDOT} - fi -done -exec 3<&- -echo -if [[ $(docker inspect -f '{{.State.Running}}' "${CNTR_NAME}") == "true" ]]; then - kill -9 $DOCKERLOGS_PID - echo "container in container spun up" -else - echo "ERROR: failed to correctly start testing container" - exit 1 -fi -exit 0 diff --git a/turtlefinder/test/cind/teardown.sh b/turtlefinder/test/cind/teardown.sh deleted file mode 100755 index ef413ae..0000000 --- a/turtlefinder/test/cind/teardown.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -TESTDIR=$(dirname "$0") -. "${TESTDIR}/common.sh" - -docker rm -f ${CNTR_NAME} - -exit 0 diff --git a/turtlefinder/test/fdscan/proc/1234/fd/0 b/turtlefinder/test/fdscan/proc/1234/fd/0 deleted file mode 100644 index e69de29..0000000 diff --git a/turtlefinder/test/fdscan/proc/5678/fd/3 b/turtlefinder/test/fdscan/proc/5678/fd/3 deleted file mode 100755 index f2c6628..0000000 --- a/turtlefinder/test/fdscan/proc/5678/fd/3 +++ /dev/null @@ -1 +0,0 @@ -net:[12345678] \ No newline at end of file diff --git a/turtlefinder/test/fdscan/proc/5678/fd/junk b/turtlefinder/test/fdscan/proc/5678/fd/junk deleted file mode 100644 index e69de29..0000000 diff --git a/turtlefinder/turtlefinder.go b/turtlefinder/turtlefinder.go deleted file mode 100644 index 8eeed59..0000000 --- a/turtlefinder/turtlefinder.go +++ /dev/null @@ -1,342 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package turtlefinder - -import ( - "context" - "strconv" - "strings" - "sync" - "time" - - "github.com/siemens/ghostwire/v2/turtlefinder/detector" - _ "github.com/siemens/ghostwire/v2/turtlefinder/detector/all" // pull in engine detector plugins - - "github.com/thediveo/go-plugger/v3" - "github.com/thediveo/lxkns/containerizer" - "github.com/thediveo/lxkns/log" - "github.com/thediveo/lxkns/model" - "github.com/thediveo/procfsroot" - "github.com/thediveo/whalewatcher/watcher" -) - -// Overseer gives access to information about container engines currently monitored. -type Overseer interface { - Engines() []*model.ContainerEngine -} - -// Contexter supplies a TurtleFinder with a suitable context for long-running -// container engine workload watching. -type Contexter func() context.Context - -// TurtleFinder implements the lxkns Containerizer interface to discover alive -// containers from one or more container engines. It can be safely used from -// multiple goroutines. -// -// On demand, a TurtleFinder scans a process list for signs of container engines -// and then tries to contact the potential engines in order to watch their -// containers. -type TurtleFinder struct { - contexter Contexter // contexts for workload watching. - engineplugins []engineplugin // static list of engine plugins. - - mux sync.Mutex // protects the following fields. - engines map[model.PIDType]*Engine // engines by PID; may be failed. -} - -// TurtleFinder implements the lxkns Containerizer interface. -var _ containerizer.Containerizer = (*TurtleFinder)(nil) - -// engineplugin represents the process names of a container engine discovery -// plugin, as well as the plugin's Discover function. -type engineplugin struct { - names []string // process names of interest - detector detector.Detector // detector plugin interface - pluginname string // for housekeeping and logging -} - -// engineprocess represents an individual container engine process and the -// container engine discovery plugin responsible for it. -type engineprocess struct { - proc *model.Process - engine *engineplugin -} - -// New returns a TurtleFinder object for further use. The supplied contexter is -// called whenever a new container engine has been found and its workload is to -// be watched: the contexter then has to return a suitable (long-running) -// context, it proferably has control over in order to properly shut down the -// background goroutine resources (indirectly) used by a TurtleFinder. -func New(contexter Contexter) *TurtleFinder { - f := &TurtleFinder{ - contexter: contexter, - engines: map[model.PIDType]*Engine{}, - } - // Query the available turtle finder plugins for the names of processes to - // look for, in order to later optimize searching the processes; as we're - // working only with a static set of plugins we only need to query the basic - // information once. - namegivers := plugger.Group[detector.Detector]().PluginsSymbols() - engineplugins := make([]engineplugin, 0, len(namegivers)) - for _, namegiver := range namegivers { - engineplugins = append(engineplugins, engineplugin{ - names: namegiver.S.EngineNames(), - detector: namegiver.S, - pluginname: namegiver.Plugin, - }) - } - f.engineplugins = engineplugins - log.Infof("available engine detector plugins: %s", - strings.Join(plugger.Group[detector.Detector]().Plugins(), ", ")) - return f -} - -// Containers returns the current container state of (alive) containers from all -// discovered container engines. -func (f *TurtleFinder) Containers( - ctx context.Context, procs model.ProcessTable, pidmap model.PIDMapper, -) []*model.Container { - // Do some quick housekeeping first: remove engines whose processes have - // vanished. - if !f.prune(procs) { - return nil // sorry, we're closed. - } - // Then look for new engine processes. - f.update(ctx, procs) - // Now query the available engines for containers that are alive... - f.mux.Lock() - engines := make([]*Engine, 0, len(f.engines)) - for _, engine := range f.engines { - // create copies of the engine objects in order to not trash the - // original engine objects. - engine := *engine - engines = append(engines, &engine) - } - f.mux.Unlock() - // Feel the heat and query the engines in parallel; to collect the results - // we use a buffered channel of the size equal the number of engines to - // query. - // - // TODO: bounded worker model - log.Infof("consulting %d container engines ... in parallel", len(engines)) - enginecontainers := make(chan []*model.Container, len(engines)) - var wg sync.WaitGroup - wg.Add(len(engines)) - for _, engine := range engines { - go func(engine *Engine) { - defer wg.Done() - containers := engine.Containers(ctx) - enginecontainers <- containers - }(engine) - } - // Wait for all query workers to complete and push their results into the - // buffered channel; only then we close the channel and then pull off the - // buffered results. - wg.Wait() - close(enginecontainers) - containers := []*model.Container{} - for conts := range enginecontainers { - containers = append(containers, conts...) - } - // Fill in the engine hierarchy, if necessary: note that we can't use this - // without knowing the containers and especially their names. - stackEngines(containers, engines, procs) - - return containers -} - -// Close closes all resources associated with this turtle finder. This is an -// asynchronous process. Make sure to also cancel or have already cancelled the -// context -func (f *TurtleFinder) Close() { - f.mux.Lock() - defer f.mux.Unlock() - for _, engine := range f.engines { - engine.Close() - } - f.engines = nil -} - -// Engines returns information about the container engines currently being -// monitored. -func (f *TurtleFinder) Engines() []*model.ContainerEngine { - f.mux.Lock() - defer f.mux.Unlock() - engines := make([]*model.ContainerEngine, 0, len(f.engines)) - for _, engine := range f.engines { - select { - case <-engine.Done: - continue // already Done, so ignore this engine. - default: - // not Done, so let's move on and add it to the list of available - // engines. - } - engines = append(engines, &model.ContainerEngine{ - ID: engine.ID, - Type: engine.Type(), - Version: engine.Version, - API: engine.API(), - PID: model.PIDType(engine.PID()), - }) - } - return engines -} - -// EngineCount returns the number of container engines currently under watch. -// Callers might want to use the Engines method instead as EngineCount bases on -// it (because we don't store an explicit engine count anywhere). -func (f *TurtleFinder) EngineCount() int { - f.mux.Lock() - defer f.mux.Unlock() - return len(f.engines) -} - -// prune any terminated watchers, either because the watcher terminated itself -// or we can't find the associated engine process anymore. -func (f *TurtleFinder) prune(procs model.ProcessTable) bool { - f.mux.Lock() - defer f.mux.Unlock() - if f.engines == nil { - return false - } - for pid, engine := range f.engines { - if procs[pid] == nil && !engine.IsAlive() { - delete(f.engines, pid) - engine.Close() // ...if not already done so. - } - } - return true -} - -// update our knowledge about container engines if necessary, given the current -// process table and by asking engine discovery plugins for any signs of engine -// life. -func (f *TurtleFinder) update(ctx context.Context, procs model.ProcessTable) { - // Look for potential signs of engine life, based on process names... - engineprocs := []engineprocess{} -NextProcess: - for _, proc := range procs { - procname := proc.Name - for engidx, engine := range f.engineplugins { - for _, enginename := range engine.names { - if procname == enginename { - engineprocs = append(engineprocs, engineprocess{ - proc: proc, - engine: &f.engineplugins[engidx], // ...we really don't want to address the loop variable here - }) - continue NextProcess - } - } - } - } - // Next, throw out all engine processes we already know of and keep only the - // new ones to look into them further. This way we keep the lock as short as - // possible. - newengineprocs := make([]engineprocess, 0, len(engineprocs)) - f.mux.Lock() - for _, engineproc := range engineprocs { - // Is this an engine PID we already know and watch? - if _, ok := f.engines[engineproc.proc.PID]; ok { - continue - } - newengineprocs = append(newengineprocs, engineproc) - } - f.mux.Unlock() - if len(newengineprocs) == 0 { - return - } - // Finally look into each new engine process: try to figure out its - // potential API socket endpoint pathname and then try to contact the engine - // via this (these) pathname(s)... Again, we aggressively go parallel in - // contacting new engines. - var wg sync.WaitGroup - wg.Add(len(newengineprocs)) - for _, engineproc := range newengineprocs { - go func(engineproc engineprocess) { - defer wg.Done() - log.Debugf("scanning new potential engine process %s (%d) for API endpoints...", - engineproc.proc.Name, engineproc.proc.PID) - // Does this process have any listening unix sockets that might act as - // API endpoints? - apisox := discoverAPISockets(engineproc.proc.PID) - if apisox == nil { - log.Debugf("process %d no API endpoint found", engineproc.proc.PID) - return - } - // Translate the API pathnames so that we can access them from our - // namespace via procfs wormholes; to make this reliably work we need to - // evaluate paths for symbolic links... - for idx, apipath := range apisox { - root := "/proc/" + strconv.FormatUint(uint64(engineproc.proc.PID), 10) + - "/root" - if p, err := procfsroot.EvalSymlinks(apipath, root, procfsroot.EvalFullPath); err == nil { - apisox[idx] = root + p - } else { - log.Warnf("invalid API endpoint at %s", apipath) - apisox[idx] = "" - } - } - // Ask the contexter to give us a long-living engine workload - // watching context; just using the background context (or even a - // request's context) will be a bad idea as it doesn't give the - // users of a Turtlefinder the means to properly spin down workload - // watchers when retiring a Turtlefinder. - enginectx := f.contexter() - if w := engineproc.engine.detector.NewWatcher(enginectx, engineproc.proc.PID, apisox); w != nil { - // We've got a new watcher! - startWatch(enginectx, w) - eng := NewEngine(enginectx, w) - f.mux.Lock() - f.engines[engineproc.proc.PID] = eng - f.mux.Unlock() - } - }(engineproc) - } - wg.Wait() -} - -// startWatch starts the watch and then shortly waits for a watcher to -// synchronize and then watches in the background (spinning off a separate go -// routine) the watcher synchronizing to its engine state, logging begin and end -// as informational messages. -func startWatch(ctx context.Context, w watcher.Watcher) { - log.Infof("beginning synchronization to %s engine (PID %d) at API %s", - w.Type(), w.PID(), w.API()) - // Start the watch including the initial synchronization... - errch := make(chan error, 1) - go func() { - errch <- w.Watch(ctx) - close(errch) - }() - // Wait in the background for the synchronization to complete and then - // report the engine ID. - go func() { - <-w.Ready() - // Getting the engine ID should be carried out swiftly, so we timebox - // it. - idctx, cancel := context.WithTimeout(ctx, 2*time.Second) - log.Infof("synchronized to %s engine (PID %d) with ID %s", - w.Type(), w.PID(), w.ID(idctx)) - cancel() // ensure to quickly release cancel - }() - // Give the watcher a (short) chance to get in sync, but do not hang around - // for too long... - // - // Oh, well: time.After is kind of hard to use without small leaks. - // Now, a 5s timer will be GC'ed after 5s anyway, but let's do it - // properly for once and all, to get the proper habit. For more - // background information, please see, for instance: - // https://www.arangodb.com/2020/09/a-story-of-a-memory-leak-in-go-how-to-properly-use-time-after/ - wecker := time.NewTimer(2 * time.Second) - select { - case <-w.Ready(): - if !wecker.Stop() { // drain the timer, if necessary. - <-wecker.C - } - case <-wecker.C: - log.Warnf("%s engine (PID %d) not yet synchronized ... continuing in background", - w.Type(), w.PID()) - } -} diff --git a/turtlefinder/turtlefinder_test.go b/turtlefinder/turtlefinder_test.go deleted file mode 100644 index 37db7c6..0000000 --- a/turtlefinder/turtlefinder_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// (c) Siemens AG 2023 -// -// SPDX-License-Identifier: MIT - -package turtlefinder - -import ( - "context" - "os" - "time" - - "github.com/thediveo/lxkns/discover" - "github.com/thediveo/whalewatcher/watcher/containerd" - "github.com/thediveo/whalewatcher/watcher/moby" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gleak" - . "github.com/thediveo/fdooze" -) - -var _ = Describe("turtle finder", func() { - - BeforeEach(func() { - goodfds := Filedescriptors() - goodgos := Goroutines() // avoid other failed goroutine tests to spill over - DeferCleanup(func() { - Eventually(Goroutines).WithTimeout(2 * time.Second).WithPolling(250 * time.Millisecond). - ShouldNot(HaveLeaked(goodgos)) - Expect(Filedescriptors()).NotTo(HaveLeakedFds(goodfds)) - }) - }) - - // This is an ugly test with respect to goroutine leakage, as it runs a - // discovery and then very quickly cancels the context, so watchers might - // still be in their spin-up phase. - It("finds docker and containerd", func() { - if os.Getuid() != 0 { - Skip("needs root") - } - - watcherctx, watchercancel := context.WithCancel(context.Background()) - tf := New(func() context.Context { return watcherctx }) - Expect(tf).NotTo(BeNil()) - defer watchercancel() - defer tf.Close() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - lxdisco := discover.Namespaces(discover.WithFullDiscovery()) - _ = tf.Containers(ctx, lxdisco.Processes, lxdisco.PIDMap) - engines := tf.Engines() - Expect(engines).To(ContainElements( - HaveEngine(moby.Type, `^unix:///proc/\d+/root/run/docker.sock$`), - HaveEngine(containerd.Type, `^unix:///proc/\d+/root/run/containerd/containerd.sock$`), - )) - }) - -}) diff --git a/webui/src/models/gw/model.ts b/webui/src/models/gw/model.ts index 7e2d04f..e582b75 100644 --- a/webui/src/models/gw/model.ts +++ b/webui/src/models/gw/model.ts @@ -16,10 +16,12 @@ import { ForwardedPort } from './forwardedports' /* Ghostwire's engine v2 own label namespace for passing additional information. */ export const GHOSTWIRE_LABEL_ROOT = 'gostwire/' +export const TURTLEFINDER_LABEL_ROOT = "turtlefinder/" export const hiddenLabel = (key: string) => [ GHOSTWIRE_LABEL_ROOT, + TURTLEFINDER_LABEL_ROOT, "github.com/thediveo/whalewatcher/", "github.com/thediveo/lxkns/", ].find(root => key.startsWith(root)) !== undefined