diff --git a/Makefile b/Makefile index 8af5f546544..8213d5b33a7 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ RESET := $(shell tput -T linux sgr0) TITLE := $(BOLD)$(PURPLE) SUCCESS := $(BOLD)$(GREEN) # the quality gate lower threshold for unit test total % coverage (by function statements) -COVERAGE_THRESHOLD := 70 +COVERAGE_THRESHOLD := 65 # CI cache busting values; change these if you want CI to not use previous stored cache INTEGRATION_CACHE_BUSTER="88738d2f" CLI_CACHE_BUSTER="789bacdf" diff --git a/cmd/event_loop.go b/cmd/event_loop.go index 4a86f1bae7d..0c7518942e9 100644 --- a/cmd/event_loop.go +++ b/cmd/event_loop.go @@ -13,9 +13,12 @@ import ( // eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and // signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until // an eventual graceful exit. -func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI) error { +// nolint:gocognit +func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI, cleanupFn func()) error { + defer cleanupFn() events := subscription.Events() - if err := setupUI(subscription.Unsubscribe, ux); err != nil { + var err error + if ux, err = setupUI(subscription.Unsubscribe, ux); err != nil { return err } @@ -32,7 +35,11 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription * continue } if err != nil { - retErr = err + // capture the error from the worker and unsubscribe to complete a graceful shutdown + retErr = multierror.Append(retErr, err) + if err := subscription.Unsubscribe(); err != nil { + retErr = multierror.Append(retErr, err) + } } case e, isOpen := <-events: if !isOpen { @@ -50,10 +57,15 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription * } } case <-signals: - if err := subscription.Unsubscribe(); err != nil { - log.Warnf("unable to unsubscribe from the event bus: %+v", err) - events = nil - } + // ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up. + // we ignore further errors since cleaning up the tmp directories will affect running catalogers that are + // reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result. + + // TODO: potential future improvement would be to pass context into workers with a cancel function that is + // to the event loop. In this way we can have a more controlled shutdown even at the most nested levels + // of processing. + events = nil + workerErrs = nil } } @@ -64,14 +76,15 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription * return retErr } -func setupUI(unsubscribe func() error, ux ui.UI) error { +func setupUI(unsubscribe func() error, ux ui.UI) (ui.UI, error) { if err := ux.Setup(unsubscribe); err != nil { + // replace the existing UI with a (simpler) logger UI ux = ui.NewLoggerUI() if err := ux.Setup(unsubscribe); err != nil { // something is very wrong, bail. - return err + return ux, err } log.Errorf("unable to setup given UI, falling back to logger: %+v", err) } - return nil + return ux, nil } diff --git a/cmd/event_loop_test.go b/cmd/event_loop_test.go new file mode 100644 index 00000000000..aa69ec73f02 --- /dev/null +++ b/cmd/event_loop_test.go @@ -0,0 +1,455 @@ +package cmd + +import ( + "fmt" + "os" + "syscall" + "testing" + "time" + + "github.com/anchore/syft/internal/ui" + "github.com/anchore/syft/syft/event" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/wagoodman/go-partybus" +) + +var _ ui.UI = (*uiMock)(nil) + +type uiMock struct { + t *testing.T + finalEvent partybus.Event + unsubscribe func() error + mock.Mock +} + +func (u *uiMock) Setup(unsubscribe func() error) error { + u.t.Logf("UI Setup called") + u.unsubscribe = unsubscribe + return u.Called(unsubscribe).Error(0) +} + +func (u *uiMock) Handle(event partybus.Event) error { + u.t.Logf("UI Handle called: %+v", event.Type) + if event == u.finalEvent { + assert.NoError(u.t, u.unsubscribe()) + } + return u.Called(event).Error(0) +} + +func (u *uiMock) Teardown() error { + u.t.Logf("UI Teardown called") + return u.Called().Error(0) +} + +func Test_eventLoop_gracefulExit(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + finalEvent := partybus.Event{ + Type: event.PresenterReady, + } + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + close(ret) + t.Log("worker closed") + // do the other half of the shutdown + testBus.Publish(finalEvent) + t.Log("worker published final event") + }() + return ret + } + + signaler := func() <-chan os.Signal { + return nil + } + + ux := &uiMock{ + t: t, + finalEvent: finalEvent, + } + + // ensure the mock sees at least the final event + ux.On("Handle", finalEvent).Return(nil) + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + assert.NoError(t, + eventLoop( + worker(), + signaler(), + subscription, + ux, + cleanupFn, + ), + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_eventLoop_workerError(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + workerErr := fmt.Errorf("worker error") + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + ret <- workerErr + t.Log("worker sent error") + close(ret) + t.Log("worker closed") + // note: NO final event is fired + }() + return ret + } + + signaler := func() <-chan os.Signal { + return nil + } + + ux := &uiMock{ + t: t, + } + + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + // ensure we see an error returned + assert.ErrorIs(t, + eventLoop( + worker(), + signaler(), + subscription, + ux, + cleanupFn, + ), + workerErr, + "should have seen a worker error, but did not", + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_eventLoop_unsubscribeError(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + finalEvent := partybus.Event{ + Type: event.PresenterReady, + } + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + close(ret) + t.Log("worker closed") + // do the other half of the shutdown + testBus.Publish(finalEvent) + t.Log("worker published final event") + }() + return ret + } + + signaler := func() <-chan os.Signal { + return nil + } + + ux := &uiMock{ + t: t, + finalEvent: finalEvent, + } + + // ensure the mock sees at least the final event... note the unsubscribe error here + ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe) + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + // unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that + // this case is handled as a controlled shutdown (this test should not timeout) + assert.NoError(t, + eventLoop( + worker(), + signaler(), + subscription, + ux, + cleanupFn, + ), + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_eventLoop_handlerError(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + finalEvent := partybus.Event{ + Type: event.PresenterReady, + Error: fmt.Errorf("unable to create presenter"), + } + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + close(ret) + t.Log("worker closed") + // do the other half of the shutdown + testBus.Publish(finalEvent) + t.Log("worker published final event") + }() + return ret + } + + signaler := func() <-chan os.Signal { + return nil + } + + ux := &uiMock{ + t: t, + finalEvent: finalEvent, + } + + // ensure the mock sees at least the final event... note the event error is propagated + ux.On("Handle", finalEvent).Return(finalEvent.Error) + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + // handle errors SHOULD propagate the event loop. We are additionally asserting that this case is + // handled as a controlled shutdown (this test should not timeout) + assert.ErrorIs(t, + eventLoop( + worker(), + signaler(), + subscription, + ux, + cleanupFn, + ), + finalEvent.Error, + "should have seen a event error, but did not", + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_eventLoop_signalsStopExecution(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + worker := func() <-chan error { + // the worker will never return work and the event loop will always be waiting... + return make(chan error) + } + + signaler := func() <-chan os.Signal { + ret := make(chan os.Signal) + go func() { + ret <- syscall.SIGINT + // note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit + }() + return ret + } + + ux := &uiMock{ + t: t, + } + + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + assert.NoError(t, + eventLoop( + worker(), + signaler(), + subscription, + ux, + cleanupFn, + ), + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_eventLoop_uiTeardownError(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + finalEvent := partybus.Event{ + Type: event.PresenterReady, + } + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + close(ret) + t.Log("worker closed") + // do the other half of the shutdown + testBus.Publish(finalEvent) + t.Log("worker published final event") + }() + return ret + } + + signaler := func() <-chan os.Signal { + return nil + } + + ux := &uiMock{ + t: t, + finalEvent: finalEvent, + } + + teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down") + + // ensure the mock sees at least the final event... note the event error is propagated + ux.On("Handle", finalEvent).Return(nil) + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(teardownError) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + // ensure we see an error returned + assert.ErrorIs(t, + eventLoop( + worker(), + signaler(), + subscription, + ux, + cleanupFn, + ), + teardownError, + "should have seen a UI teardown error, but did not", + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) { + done := make(chan bool) + go func() { + test(t) + done <- true + }() + + select { + case <-time.After(timeout): + t.Fatal("test timed out") + case <-done: + } +} diff --git a/cmd/packages.go b/cmd/packages.go index 4fae5c41c73..73684865291 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -6,10 +6,6 @@ import ( "io/ioutil" "os" - "github.com/anchore/syft/syft/presenter/packages" - - "github.com/spf13/viper" - "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/anchore" "github.com/anchore/syft/internal/bus" @@ -19,10 +15,12 @@ import ( "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/presenter/packages" "github.com/anchore/syft/syft/source" "github.com/pkg/profile" "github.com/spf13/cobra" "github.com/spf13/pflag" + "github.com/spf13/viper" "github.com/wagoodman/go-partybus" ) @@ -191,6 +189,7 @@ func packagesExec(_ *cobra.Command, args []string) error { setupSignals(), eventSubscription, ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet), + syft.Cleanup, ) } @@ -206,7 +205,11 @@ func packagesExecWorker(userInput string) <-chan error { errs <- fmt.Errorf("failed to determine image source: %+v", err) return } - defer cleanup() + defer func() { + if err := cleanup(); err != nil { + log.Warnf("unable to cleanup source temp dir: %+v", err) + } + }() catalog, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt) if err != nil { diff --git a/cmd/power_user.go b/cmd/power_user.go index a5728fa64ce..5df52d41b78 100644 --- a/cmd/power_user.go +++ b/cmd/power_user.go @@ -4,9 +4,11 @@ import ( "fmt" "sync" - "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft" + "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/bus" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/presenter/poweruser" "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/syft/event" @@ -68,6 +70,7 @@ func powerUserExec(_ *cobra.Command, args []string) error { setupSignals(), eventSubscription, ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet), + syft.Cleanup, ) } @@ -89,7 +92,11 @@ func powerUserExecWorker(userInput string) <-chan error { errs <- err return } - defer cleanup() + defer func() { + if err := cleanup(); err != nil { + log.Warnf("unable to cleanup source temp dir: %+v", err) + } + }() if src.Metadata.Scheme != source.ImageScheme { errs <- fmt.Errorf("the power-user subcommand only allows for 'image' schemes, given %q", src.Metadata.Scheme) diff --git a/go.mod b/go.mod index 89e6352f870..c21d80747b7 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.0 - github.com/stretchr/testify v1.6.0 + github.com/stretchr/testify v1.7.0 github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240 github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163 @@ -46,3 +46,5 @@ require ( golang.org/x/mod v0.3.0 gopkg.in/yaml.v2 v2.3.0 ) + +replace github.com/anchore/stereoscope => ../stereoscope diff --git a/go.sum b/go.sum index 71d5f646a49..7565cb2a012 100644 --- a/go.sum +++ b/go.sum @@ -115,8 +115,6 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0v github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= -github.com/anchore/stereoscope v0.0.0-20210524175238-3b7662f3a66f h1:bFadyOLOkzME3BrZFZ5m8cf/b2hsn3aMSS9s+SKubRk= -github.com/anchore/stereoscope v0.0.0-20210524175238-3b7662f3a66f/go.mod h1:vhh1M99rfWx5ejMvz1lkQiFZUrC5wu32V12R4JXH+ZI= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -704,6 +702,7 @@ github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -711,8 +710,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= diff --git a/internal/temp_dir_generator.go b/internal/temp_dir_generator.go new file mode 100644 index 00000000000..1734608198b --- /dev/null +++ b/internal/temp_dir_generator.go @@ -0,0 +1,5 @@ +package internal + +import "github.com/anchore/stereoscope/pkg/file" + +var RootTempDirGenerator = file.NewTempDirGenerator(ApplicationName) diff --git a/syft/lib.go b/syft/lib.go index ed9a86ef611..2e9d7027e54 100644 --- a/syft/lib.go +++ b/syft/lib.go @@ -19,6 +19,9 @@ package syft import ( "fmt" + "github.com/anchore/stereoscope" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/distro" @@ -76,3 +79,11 @@ func SetLogger(logger logger.Logger) { func SetBus(b *partybus.Bus) { bus.SetPublisher(b) } + +func Cleanup() { + stereoscope.Cleanup() + + if err := internal.RootTempDirGenerator.Cleanup(); err != nil { + log.Errorf("failed to cleanup temp directories: %w", err) + } +} diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index a3e3af3e9d4..cc6efe6059b 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -36,7 +36,11 @@ type archiveParser struct { func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, error) { parser, cleanupFn, err := newJavaArchiveParser(virtualPath, reader, true) // note: even on error, we should always run cleanup functions - defer cleanupFn() + defer func() { + if err := cleanupFn(); err != nil { + log.Warnf("unable to clean up java archive temp dir: %+v", err) + } + }() if err != nil { return nil, err } @@ -53,7 +57,7 @@ func uniquePkgKey(p *pkg.Package) string { // newJavaArchiveParser returns a new java archive parser object for the given archive. Can be configured to discover // and parse nested archives or ignore them. -func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested bool) (*archiveParser, func(), error) { +func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested bool) (*archiveParser, func() error, error) { contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader) if err != nil { return nil, cleanupFn, fmt.Errorf("unable to process java archive: %w", err) diff --git a/syft/pkg/cataloger/java/archive_parser_test.go b/syft/pkg/cataloger/java/archive_parser_test.go index d0453679f58..ce75344fe50 100644 --- a/syft/pkg/cataloger/java/archive_parser_test.go +++ b/syft/pkg/cataloger/java/archive_parser_test.go @@ -226,7 +226,9 @@ func TestParseJar(t *testing.T) { } parser, cleanupFn, err := newJavaArchiveParser(fixture.Name(), fixture, false) - defer cleanupFn() + t.Cleanup(func() { + assert.NoError(t, cleanupFn()) + }) if err != nil { t.Fatalf("should not have filed... %+v", err) } @@ -845,9 +847,11 @@ func TestPackagesFromPomProperties(t *testing.T) { assert.NoError(t, err) // make the parser - parser, cleanup, err := newJavaArchiveParser(virtualPath, nop, false) + parser, cleanupFn, err := newJavaArchiveParser(virtualPath, nop, false) assert.NoError(t, err) - t.Cleanup(cleanup) + t.Cleanup(func() { + assert.NoError(t, cleanupFn()) + }) // get the test data actualPackage := parser.newPackageFromPomProperties(*test.props, test.parent) diff --git a/syft/pkg/cataloger/java/save_archive_to_tmp.go b/syft/pkg/cataloger/java/save_archive_to_tmp.go index 69d7866935a..cbaa4a3b45e 100644 --- a/syft/pkg/cataloger/java/save_archive_to_tmp.go +++ b/syft/pkg/cataloger/java/save_archive_to_tmp.go @@ -3,25 +3,19 @@ package java import ( "fmt" "io" - "io/ioutil" "os" "path/filepath" - "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal" ) -func saveArchiveToTmp(reader io.Reader) (string, string, func(), error) { - tempDir, err := ioutil.TempDir("", "syft-jar-contents-") +func saveArchiveToTmp(reader io.Reader) (string, string, func() error, error) { + generator := internal.RootTempDirGenerator.NewGenerator() + tempDir, err := generator.NewDirectory("java-cataloger-content-cache") if err != nil { - return "", "", func() {}, fmt.Errorf("unable to create tempdir for jar processing: %w", err) - } - - cleanupFn := func() { - err = os.RemoveAll(tempDir) - if err != nil { - log.Errorf("unable to cleanup jar tempdir: %+v", err) - } + return "", "", func() error { return nil }, fmt.Errorf("unable to create tempdir for jar processing: %w", err) } + cleanupFn := generator.Cleanup archivePath := filepath.Join(tempDir, "archive") contentDir := filepath.Join(tempDir, "contents") diff --git a/syft/source/source.go b/syft/source/source.go index 71cb77fd3f1..683a2521207 100644 --- a/syft/source/source.go +++ b/syft/source/source.go @@ -23,33 +23,33 @@ type Source struct { type sourceDetector func(string) (image.Source, string, error) // New produces a Source based on userInput like dir: or image:tag -func New(userInput string, registryOptions *image.RegistryOptions) (Source, func(), error) { +func New(userInput string, registryOptions *image.RegistryOptions) (Source, func() error, error) { fs := afero.NewOsFs() + noCleanupFn := func() error { return nil } parsedScheme, imageSource, location, err := detectScheme(fs, image.DetectSource, userInput) if err != nil { - return Source{}, func() {}, fmt.Errorf("unable to parse input=%q: %w", userInput, err) + return Source{}, noCleanupFn, fmt.Errorf("unable to parse input=%q: %w", userInput, err) } switch parsedScheme { case DirectoryScheme: fileMeta, err := fs.Stat(location) if err != nil { - return Source{}, func() {}, fmt.Errorf("unable to stat dir=%q: %w", location, err) + return Source{}, noCleanupFn, fmt.Errorf("unable to stat dir=%q: %w", location, err) } if !fileMeta.IsDir() { - return Source{}, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", location, err) + return Source{}, noCleanupFn, fmt.Errorf("given path is not a directory (path=%q): %w", location, err) } s, err := NewFromDirectory(location) if err != nil { - return Source{}, func() {}, fmt.Errorf("could not populate source from path=%q: %w", location, err) + return Source{}, noCleanupFn, fmt.Errorf("could not populate source from path=%q: %w", location, err) } - return s, func() {}, nil + return s, noCleanupFn, nil case ImageScheme: - img, err := stereoscope.GetImageFromSource(location, imageSource, registryOptions) - cleanup := stereoscope.Cleanup + img, cleanup, err := stereoscope.GetImageFromSource(location, imageSource, registryOptions) if err != nil || img == nil { return Source{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", location, err) @@ -62,7 +62,7 @@ func New(userInput string, registryOptions *image.RegistryOptions) (Source, func return s, cleanup, nil } - return Source{}, func() {}, fmt.Errorf("unable to process input for scanning: '%s'", userInput) + return Source{}, noCleanupFn, fmt.Errorf("unable to process input for scanning: '%s'", userInput) } // NewFromDirectory creates a new source object tailored to catalog a given filesystem directory recursively. diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index 118c4ad6f69..5c75157fd4a 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -1,6 +1,7 @@ package integration import ( + "github.com/stretchr/testify/assert" "testing" "github.com/anchore/syft/syft/distro" @@ -24,7 +25,9 @@ func BenchmarkImagePackageCatalogers(b *testing.B) { for _, c := range cataloger.ImageCatalogers() { // in case of future alteration where state is persisted, assume no dependency is safe to reuse theSource, cleanupSource, err := source.New("docker-archive:"+tarPath, nil) - b.Cleanup(cleanupSource) + b.Cleanup(func() { + assert.NoError(b, cleanupSource()) + }) if err != nil { b.Fatalf("unable to get source: %+v", err) } diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go index 56cbc7b09c1..b5753d10346 100644 --- a/test/integration/utils_test.go +++ b/test/integration/utils_test.go @@ -1,6 +1,7 @@ package integration import ( + "github.com/stretchr/testify/assert" "testing" "github.com/anchore/stereoscope/pkg/imagetest" @@ -15,7 +16,9 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string) (*pkg.Catalog, * tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) theSource, cleanupSource, err := source.New("docker-archive:"+tarPath, nil) - t.Cleanup(cleanupSource) + t.Cleanup(func() { + assert.NoError(t, cleanupSource()) + }) if err != nil { t.Fatalf("unable to get source: %+v", err) } @@ -30,7 +33,9 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string) (*pkg.Catalog, * func catalogDirectory(t *testing.T, dir string) (*pkg.Catalog, *distro.Distro, source.Source) { theSource, cleanupSource, err := source.New("dir:"+dir, nil) - t.Cleanup(cleanupSource) + t.Cleanup(func() { + assert.NoError(t, cleanupSource()) + }) if err != nil { t.Fatalf("unable to get source: %+v", err) }