From 739f328ee55ea2fddf565e5c09dad4b5d8d568d0 Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Tue, 14 Jun 2022 09:13:14 +0200 Subject: [PATCH] feat(http): optional query parameter to update only containers of a specified image (#1289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(http): optional query parameter to update only containers of a specified image * fix style issues * comma separated image parameter * Support comma-separated query parameter as well as specifying it multiple times Co-authored-by: nils måsén * fixed compile error * fixed FilterByImageTag Not sure what changed in my testing setup, but Docker reports image names including the tag name now. * consistent use of image/tag (use image) * fixed multiple image queries * assuming I'm right here, only block on lock when any images are specified. * add unit tests for image filter. didn't add tests for update api because they didn't already exist * whoops. * use ImageName instead, add unit test for empty ImageName filter. Co-authored-by: nils måsén --- cmd/root.go | 2 +- pkg/api/update/update.go | 32 +++++++++++++++---- pkg/container/mocks/FilterableContainer.go | 14 ++++++++ pkg/filters/filters.go | 18 +++++++++++ pkg/filters/filters_test.go | 37 ++++++++++++++++++++++ pkg/types/filterable_container.go | 1 + 6 files changed, 96 insertions(+), 8 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 9041157b0..a35041075 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -188,7 +188,7 @@ func Run(c *cobra.Command, names []string) { httpAPI := api.New(apiToken) if enableUpdateAPI { - updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }, updateLock) + updateHandler := update.New(func(images []string) { runUpdatesWithNotifications(filters.FilterByImage(images, filter)) }, updateLock) httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle) // If polling isn't enabled the scheduler is never started and // we need to trigger the startup messages manually. diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index 4721e3ef2..ba044ab50 100644 --- a/pkg/api/update/update.go +++ b/pkg/api/update/update.go @@ -4,6 +4,7 @@ import ( "io" "net/http" "os" + "strings" log "github.com/sirupsen/logrus" ) @@ -13,7 +14,7 @@ var ( ) // New is a factory function creating a new Handler instance -func New(updateFn func(), updateLock chan bool) *Handler { +func New(updateFn func(images []string), updateLock chan bool) *Handler { if updateLock != nil { lock = updateLock } else { @@ -29,7 +30,7 @@ func New(updateFn func(), updateLock chan bool) *Handler { // Handler is an API handler used for triggering container update scans type Handler struct { - fn func() + fn func(images []string) Path string } @@ -43,12 +44,29 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { return } - select { - case chanValue := <-lock: + var images []string + imageQueries, found := r.URL.Query()["image"] + if found { + for _, image := range imageQueries { + images = append(images, strings.Split(image, ",")...) + } + + } else { + images = nil + } + + if len(images) > 0 { + chanValue := <-lock defer func() { lock <- chanValue }() - handle.fn() - default: - log.Debug("Skipped. Another update already running.") + handle.fn(images) + } else { + select { + case chanValue := <-lock: + defer func() { lock <- chanValue }() + handle.fn(images) + default: + log.Debug("Skipped. Another update already running.") + } } } diff --git a/pkg/container/mocks/FilterableContainer.go b/pkg/container/mocks/FilterableContainer.go index 1ae812535..fa863b51a 100644 --- a/pkg/container/mocks/FilterableContainer.go +++ b/pkg/container/mocks/FilterableContainer.go @@ -78,3 +78,17 @@ func (_m *FilterableContainer) Scope() (string, bool) { return r0, r1 } + +// ImageName provides a mock function with given fields: +func (_m *FilterableContainer) ImageName() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go index 18f39c257..a283301da 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -70,6 +70,24 @@ func FilterByScope(scope string, baseFilter t.Filter) t.Filter { } } +// FilterByImage returns all containers that have a specific image +func FilterByImage(images []string, baseFilter t.Filter) t.Filter { + if images == nil { + return baseFilter + } + + return func(c t.FilterableContainer) bool { + image := strings.Split(c.ImageName(), ":")[0] + for _, targetImage := range images { + if image == targetImage { + return baseFilter(c) + } + } + + return false + } +} + // BuildFilter creates the needed filter of containers func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, string) { sb := strings.Builder{} diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go index 3b52b5ee5..c07b1812f 100644 --- a/pkg/filters/filters_test.go +++ b/pkg/filters/filters_test.go @@ -110,6 +110,43 @@ func TestFilterByDisabledLabel(t *testing.T) { container.AssertExpectations(t) } +func TestFilterByImage(t *testing.T) { + filterEmpty := FilterByImage(nil, NoFilter) + filterSingle := FilterByImage([]string{"registry"}, NoFilter) + filterMultiple := FilterByImage([]string{"registry", "bla"}, NoFilter) + assert.NotNil(t, filterSingle) + assert.NotNil(t, filterMultiple) + + container := new(mocks.FilterableContainer) + container.On("ImageName").Return("registry:2") + assert.True(t, filterEmpty(container)) + assert.True(t, filterSingle(container)) + assert.True(t, filterMultiple(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("ImageName").Return("registry:latest") + assert.True(t, filterEmpty(container)) + assert.True(t, filterSingle(container)) + assert.True(t, filterMultiple(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("ImageName").Return("abcdef1234") + assert.True(t, filterEmpty(container)) + assert.False(t, filterSingle(container)) + assert.False(t, filterMultiple(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("ImageName").Return("bla:latest") + assert.True(t, filterEmpty(container)) + assert.False(t, filterSingle(container)) + assert.True(t, filterMultiple(container)) + container.AssertExpectations(t) + +} + func TestBuildFilter(t *testing.T) { var names []string names = append(names, "test") diff --git a/pkg/types/filterable_container.go b/pkg/types/filterable_container.go index 3c462954a..b410b1cbc 100644 --- a/pkg/types/filterable_container.go +++ b/pkg/types/filterable_container.go @@ -7,4 +7,5 @@ type FilterableContainer interface { IsWatchtower() bool Enabled() (bool, bool) Scope() (string, bool) + ImageName() string }