diff --git a/engine.go b/engine.go index 6c4172c..c57f099 100644 --- a/engine.go +++ b/engine.go @@ -27,18 +27,29 @@ type Engine struct { ID string // engine ID. Version string // engine version. Done chan struct{} // closed when watch is done/has terminated. - Cleanerfn func(*Engine) // if non-nil, called when the Engine is getting removed. + PPIDHint model.PIDType // PID of engine's process; for container PID translation. } -// 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, w watcher.Watcher) *Engine { +// NewEngine returns a new Engine given the specified watcher. As NewEngine +// returns, the Engine is already "warming up" and has started watching (using +// the given context). +// +// ppidhint optionally specifies a container engine's immediate parent process. +// This information is later necessary for lxkns to correctly translate +// container PIDs. When activating a socket-activated engine, the process tree +// scan does never include the engine, as this is only activated after the scan. +// In order to still allow lxkns to translate container PIDs related to newly +// socket-activated engines, we assume that the engine's parent process PID is +// in the same PID namespace, so we can also use that for correct PID +// translation. +func NewEngine(ctx context.Context, w watcher.Watcher, ppidhint model.PIDType) *Engine { idctx, cancel := context.WithTimeout(ctx, 2*time.Second) e := &Engine{ - Watcher: w, - ID: w.ID(idctx), - Version: w.Version(idctx), - Done: make(chan struct{}, 1), // might never be picked up in some situations + Watcher: w, + ID: w.ID(idctx), + Version: w.Version(idctx), + Done: make(chan struct{}, 1), // might never be picked up in some situations + PPIDHint: ppidhint, } cancel() // ensure to quickly release cancel, silence linter log.Infof("watching %s container engine (PID %d) with ID '%s', version '%s'", @@ -55,13 +66,17 @@ func NewEngine(ctx context.Context, w watcher.Watcher) *Engine { // Containers returns the alive containers managed by this engine, using the // associated watcher. +// +// The containers returned will reference a model.ContainerEngine and thus are +// decoupled from a turtlefinder's (container) Engine object. 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()), + ID: e.ID, + Type: e.Watcher.Type(), + Version: e.Version, + API: e.Watcher.API(), + PID: model.PIDType(e.Watcher.PID()), + PPIDHint: e.PPIDHint, } // Adapt the whalewatcher container model to the lxkns container model, // where the latter takes container engines and groups into account of its diff --git a/engine_test.go b/engine_test.go index 5396631..43a083c 100644 --- a/engine_test.go +++ b/engine_test.go @@ -50,7 +50,7 @@ var _ = Describe("container engine", Serial, Ordered, func() { Expect(err).NotTo(HaveOccurred()) ctx, cancel := context.WithCancel(ctx) defer cancel() - engine := NewEngine(ctx, w) + engine := NewEngine(ctx, w, 0) Expect(engine.ID).NotTo(BeZero()) Consistently(engine.IsAlive).Should(BeTrue()) diff --git a/go.mod b/go.mod index b912857..bc616c3 100644 --- a/go.mod +++ b/go.mod @@ -84,10 +84,10 @@ require ( github.com/ory/dockertest/v3 v3.10.0 github.com/thediveo/fdooze v0.3.1 github.com/thediveo/go-plugger/v3 v3.1.0 - github.com/thediveo/lxkns v0.30.0 + github.com/thediveo/lxkns v0.31.0 github.com/thediveo/success v1.0.2 github.com/thediveo/whalewatcher v0.11.0 golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.16.0 + golang.org/x/sys v0.16.0 // indirect golang.org/x/tools v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index 98035a8..301f7c0 100644 --- a/go.sum +++ b/go.sum @@ -152,7 +152,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= 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= @@ -174,14 +174,12 @@ github.com/thediveo/fdooze v0.3.1 h1:T5lARTBZXdDIwdsMNgiwpEY3NT40k1WTRlkoKgk8K+0 github.com/thediveo/fdooze v0.3.1/go.mod h1:wf5DDE9ch9MqqoS5ofU5+tOOsZyvp5qrJzQjVIGXUTk= github.com/thediveo/go-mntinfo v1.0.2 h1:PVzVhve7Hhi9cEnW7tLv+6V1K0L14LyrFkoRNIhL7e0= github.com/thediveo/go-mntinfo v1.0.2/go.mod h1:R0OctrQ+AVz+aEbofJah3/8Hrpn9N22mc0Dym8Mv2qM= -github.com/thediveo/go-plugger/v3 v3.0.1 h1:GIlzqJa1stXgF2ouey69hyWaS+iaxLWJT37N8DMtQP8= -github.com/thediveo/go-plugger/v3 v3.0.1/go.mod h1:UYHIk8tys2lerZjpMhbWjrORb6/mXlb2bGZxCPTuR8k= github.com/thediveo/go-plugger/v3 v3.1.0 h1:aqtzFkP7gBU/MlL/TyMOTY0MUYixebZn8JVhX/13yLo= github.com/thediveo/go-plugger/v3 v3.1.0/go.mod h1:bED6ehF6GQUW9NDDgJG6QS/GL1J8L8hT3RUI7GTtAWo= github.com/thediveo/ioctl v0.9.3 h1:DCxyUUY15z/Zezz+wf2nlbVf3yFh0nvfM7i7KnfgG8s= github.com/thediveo/ioctl v0.9.3/go.mod h1:Ro3WW0UuPDh1QByEwNb/alva3ODM+GbRlb80u/LZU9o= -github.com/thediveo/lxkns v0.30.0 h1:shbrIHMX7kEgCIR1VgJO7OB6eVtJMDppaC9PM2hvXsU= -github.com/thediveo/lxkns v0.30.0/go.mod h1:36NPgrJPxQrlAQLy2GS0dO+EHQB1Gu2ijlPy5KRgWic= +github.com/thediveo/lxkns v0.31.0 h1:vjUaMfYG+Xafep6L/QUDfU+xK+jpzrZQyG0OKQAnfhg= +github.com/thediveo/lxkns v0.31.0/go.mod h1:agBgLaQTIIRC0+GzEzqdRxK4sihsZkvpp4Nsp10+Rl4= github.com/thediveo/namspill v0.1.6 h1:eD8puqhwIkBS78vrzJtY46eurHX0o6JIAqzgkRmMLl0= github.com/thediveo/notwork v1.3.1 h1:KG1Jh7pWU+QTl+7yMzs2Zugl8JZsP0x4OwNFt0rItL0= github.com/thediveo/once v0.9.1 h1:gk/8dYOto5cVEBH0LK1vYyOFH9OvugcZ12e+UnJpmTo= @@ -244,8 +242,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -261,8 +257,6 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/package_dockerenginefinder_test.go b/package_dockerenginefinder_test.go index 381e026..1eae0e0 100644 --- a/package_dockerenginefinder_test.go +++ b/package_dockerenginefinder_test.go @@ -29,6 +29,7 @@ func dockerEngineFinderOnly() { backup := g.Backup() DeferCleanup(func() { g.Restore(backup) + clearCachedDetectorPlugins() // ...and don't forget to clean up after all }) g.Clear() g.Register(&dockerdEngineFinder{}, plugger.WithPlugin("test-dockerd")) diff --git a/socketactivator_test.go b/socketactivator_test.go index 57513aa..c21e6a8 100644 --- a/socketactivator_test.go +++ b/socketactivator_test.go @@ -23,7 +23,7 @@ import ( const sockactivatorSyncWait = 5 * time.Second -func resetActivatorPlugins() { +func clearCachedDetectorPlugins() { muDaemonDetectorPlugins.Lock() defer muDaemonDetectorPlugins.Unlock() demonDetectorPlugins = nil @@ -35,7 +35,7 @@ var _ = Describe("socket activator", Serial, Ordered, func() { BeforeEach(test.LogToGinkgo) - BeforeEach(resetActivatorPlugins) + BeforeEach(clearCachedDetectorPlugins) BeforeEach(func() { goodgos := Goroutines() // avoid other failed goroutine tests to spill over diff --git a/stacker.go b/stacker.go index c9079e0..041c0c7 100644 --- a/stacker.go +++ b/stacker.go @@ -78,6 +78,7 @@ func stackEngines(containers []*model.Container, engines []*Engine, proctable mo // parent->children: we thus only need to modify the newly created // process object and the shallow clone of the map, but we neither touch // the original process map nor the original process objects. + proctable[proc.PID] = proc proc.Parent = proctable[proc.PPID] } for _, engine := range engines { diff --git a/stacker_test.go b/stacker_test.go index aa3637c..464377c 100644 --- a/stacker_test.go +++ b/stacker_test.go @@ -8,6 +8,7 @@ import ( "context" "io" "os" + "strings" "time" "github.com/ory/dockertest/v3" @@ -16,10 +17,12 @@ import ( "github.com/thediveo/lxkns/model" "github.com/thediveo/lxkns/species" "github.com/thediveo/whalewatcher/engineclient/containerd/test/ctr" + "github.com/thediveo/whalewatcher/engineclient/cri" "github.com/thediveo/whalewatcher/engineclient/cri/test/img" "github.com/thediveo/whalewatcher/test" "github.com/thediveo/whalewatcher/watcher/containerd" "github.com/thediveo/whalewatcher/watcher/moby" + "golang.org/x/exp/slices" testlog "github.com/siemens/turtlefinder/internal/test" @@ -157,9 +160,18 @@ var _ = Describe("turtles and elephants", Serial, Ordered, func() { By("waiting for turtle finder to catch up") Eventually(ctx, func() []*model.ContainerEngine { _ = discover() - return finder.Engines() + engines := slices.DeleteFunc(finder.Engines(), isStackerTestEngineTyp) + slices.SortFunc(engines, func(a, b *model.ContainerEngine) int { + return strings.Compare(a.Type, b.Type) + }) + return engines }).Within(10 * time.Second).ProbeEvery(250 * time.Millisecond). - Should(HaveLen(len(engines) + 2 /* containerd native and CRI "twins"*/)) + Should(HaveExactElements( + HaveField("Type", containerd.Type), + HaveField("Type", containerd.Type), + HaveField("Type", moby.Type), + HaveField("Type", cri.Type), + )) By("pulling a busybox image (if necessary)") ctr.Successfully(providerCntr, @@ -190,9 +202,29 @@ var _ = Describe("turtles and elephants", Serial, Ordered, func() { By("waiting for the containerized containerd engine to vanish") Eventually(ctx, func() []*model.ContainerEngine { _ = discover() - return finder.Engines() + engines := slices.DeleteFunc(finder.Engines(), isStackerTestEngineTyp) + slices.SortFunc(engines, func(a, b *model.ContainerEngine) int { + return strings.Compare(a.Type, b.Type) + }) + return engines }).Within(10 * time.Second).ProbeEvery(250 * time.Millisecond). - Should(HaveLen(len(engines))) + Should(HaveExactElements( + HaveField("Type", containerd.Type), // ...only one left + HaveField("Type", moby.Type), + )) }) }) + +// filter for the engine types guaranteed to be present; please note that we +// filter out podman here, in order to be independent of host podman +// installations. We cover podman explicitly in the overall turtlefinder +// test(s). +func isStackerTestEngineTyp(e *model.ContainerEngine) bool { + switch e.Type { + case containerd.Type, moby.Type, cri.Type: + return false + default: + return true + } +} diff --git a/turtlefinder.go b/turtlefinder.go index 9f1f722..9d7bef2 100644 --- a/turtlefinder.go +++ b/turtlefinder.go @@ -15,7 +15,6 @@ import ( "github.com/siemens/turtlefinder/activator" "github.com/siemens/turtlefinder/detector" - "golang.org/x/exp/slices" "golang.org/x/sync/semaphore" _ "github.com/siemens/turtlefinder/activator/all" // pull in activator and socket-activated engine detector plugins @@ -162,11 +161,9 @@ func (f *TurtleFinder) Containers( f.update(ctx, procs) // Now query the available engines for containers that are alive... f.mux.Lock() - allEngines := make([]*Engine, 0, len(f.engines)) + allEngines := make([]*Engine, 0, len(f.engines) /* lucky guess */) for _, engines := range f.engines { - // create copies of the engine objects in order to not trash the - // original engine objects. - allEngines = append(allEngines, slices.Clone(engines)...) + allEngines = append(allEngines, engines...) } f.mux.Unlock() allcontainers := []*model.Container{} @@ -408,9 +405,9 @@ NextProcess: // watchers when retiring a Turtlefinder. enginectx := f.contexter() for _, w := range engineproc.engine.detector.NewWatchers(enginectx, engineproc.proc.PID, apisox) { - // We've got a new watcher! Or two *snicker* + // We've got a new watcher! Or two... *snicker* ...so many demons! startWatch(enginectx, w, f.initialsyncwait) - eng := NewEngine(enginectx, w) + eng := NewEngine(enginectx, w, engineproc.proc.PPID) f.mux.Lock() f.engines[engineproc.proc.PID] = append(f.engines[engineproc.proc.PID], eng) f.mux.Unlock() @@ -449,8 +446,19 @@ NextProcess: // need to make sure that we're not trashing our engine map. f.mux.Lock() defer f.mux.Unlock() + // Freshly socket-activated engines won't yet be in the process + // tree we're working on. In order to allow downstream users of + // turtlefinders – lxkns in particular – to still do correct + // container PID translation, we get an engine's parent PID that + // we assume serves as well for PID translation between PID + // namespaces. So pay a quick visit to the proc filesystem and + // pick up this engine's PPID. + var ppidhint model.PIDType + if engproc := model.NewProcess(pid, false); engproc != nil { + ppidhint = engproc.PPID + } f.engines[pid] = []*Engine{ - NewEngine(f.contexter(), w), + NewEngine(f.contexter(), w, ppidhint), } }, ) diff --git a/turtlefinder_test.go b/turtlefinder_test.go index 83c3a2c..db14f41 100644 --- a/turtlefinder_test.go +++ b/turtlefinder_test.go @@ -119,7 +119,7 @@ var _ = Describe("turtle finder", Ordered, Serial, func() { }) - BeforeEach(resetActivatorPlugins) + BeforeEach(clearCachedDetectorPlugins) BeforeEach(test.LogToGinkgo) diff --git a/watch_test.go b/watch_test.go index 1ef1c2b..29b48e0 100644 --- a/watch_test.go +++ b/watch_test.go @@ -56,7 +56,7 @@ var _ = Describe("watch", Serial, func() { Eventually(GinkgoWriter.(fmt.Stringer).String).Should(MatchRegexp( `beginning synchronization to 'docker.com' engine .*\n` + `.*synchronized to 'docker.com' container engine .* with ID ` + - `'(?:[A-Z0-9]{4}(?:[A-Z0-9]{4}){11})|(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})'`)) + `'(?:[A-Z0-9]{4}(?::[A-Z0-9]{4}){11})|(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})'`)) cancel() Eventually(GinkgoWriter.(fmt.Stringer).String).Should(ContainSubstring("terminated watch")) })