From 922a9ec126e77fe0f982e863649cb94f6be65499 Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Thu, 12 May 2022 18:38:31 +0200 Subject: [PATCH 01/12] feat(http): optional query parameter to update only containers of a specified image --- cmd/root.go | 4 ++-- pkg/api/update/update.go | 19 +++++++++++-------- pkg/container/container.go | 4 ++++ pkg/container/mocks/FilterableContainer.go | 13 +++++++++++++ pkg/filters/filters.go | 17 +++++++++++++++++ pkg/types/filterable_container.go | 1 + 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 1be52a8ed..01e9f547e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -189,7 +189,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(tags []string) { runUpdatesWithNotifications(filters.FilterByImageTag(tags, 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. @@ -293,7 +293,7 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) { startupLog.Info("Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST")) startupLog.Info("Note that the first check will be performed in " + until) } else if runOnce, _ := c.PersistentFlags().GetBool("run-once"); runOnce { - startupLog.Info("Running a one time update.") + startupLog.Info("Running a one time update.") } else { startupLog.Info("Periodic runs are not enabled.") } diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index 4721e3ef2..912df43ee 100644 --- a/pkg/api/update/update.go +++ b/pkg/api/update/update.go @@ -13,7 +13,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 +29,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 +43,15 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { return } - select { - case chanValue := <-lock: - defer func() { lock <- chanValue }() - handle.fn() - default: - log.Debug("Skipped. Another update already running.") + var images []string + if r.URL.Query().Has("image") { + images = []string{r.URL.Query().Get("image")} + } else { + images = nil } + chanValue := <-lock + defer func() { lock <- chanValue }() + handle.fn(images) + } diff --git a/pkg/container/container.go b/pkg/container/container.go index 82ae20572..433fb546e 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -326,3 +326,7 @@ func (c Container) VerifyConfiguration() error { return nil } + +func (c Container) Image() string { + return c.runtimeConfig().Image +} diff --git a/pkg/container/mocks/FilterableContainer.go b/pkg/container/mocks/FilterableContainer.go index 1ae812535..22c16073e 100644 --- a/pkg/container/mocks/FilterableContainer.go +++ b/pkg/container/mocks/FilterableContainer.go @@ -78,3 +78,16 @@ func (_m *FilterableContainer) Scope() (string, bool) { return r0, r1 } + +func (_m *FilterableContainer) Image() 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..58d4bc596 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -70,6 +70,23 @@ func FilterByScope(scope string, baseFilter t.Filter) t.Filter { } } +func FilterByImageTag(tags []string, baseFilter t.Filter) t.Filter { + if tags == nil { + return baseFilter + } + + return func(c t.FilterableContainer) bool { + image := c.Image() + for _, targetTag := range tags { + if image == targetTag { + 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/types/filterable_container.go b/pkg/types/filterable_container.go index 3c462954a..efd9ba694 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) + Image() string } From 4b798c209e0a907ddd1b33a5495b38a52a50f01c Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Thu, 12 May 2022 19:50:07 +0200 Subject: [PATCH 02/12] fix style issues --- pkg/container/container.go | 1 + pkg/container/mocks/FilterableContainer.go | 1 + pkg/filters/filters.go | 1 + 3 files changed, 3 insertions(+) diff --git a/pkg/container/container.go b/pkg/container/container.go index 433fb546e..fe5cc9958 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -327,6 +327,7 @@ func (c Container) VerifyConfiguration() error { return nil } +// Image returns the image tag of the container as reported by the runtime config func (c Container) Image() string { return c.runtimeConfig().Image } diff --git a/pkg/container/mocks/FilterableContainer.go b/pkg/container/mocks/FilterableContainer.go index 22c16073e..29df3cf25 100644 --- a/pkg/container/mocks/FilterableContainer.go +++ b/pkg/container/mocks/FilterableContainer.go @@ -79,6 +79,7 @@ func (_m *FilterableContainer) Scope() (string, bool) { return r0, r1 } +// Image provides a mock function with given fields: func (_m *FilterableContainer) Image() string { ret := _m.Called() diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go index 58d4bc596..fcd8a191a 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -70,6 +70,7 @@ func FilterByScope(scope string, baseFilter t.Filter) t.Filter { } } +// FilterByImageTag returns all containers that have a specific image func FilterByImageTag(tags []string, baseFilter t.Filter) t.Filter { if tags == nil { return baseFilter From 76407f36a73c6d461a5d45939477a6dd1eb28e1f Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Mon, 16 May 2022 16:35:39 +0200 Subject: [PATCH 03/12] comma separated image parameter --- pkg/api/update/update.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index 912df43ee..7769a3f8d 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" ) @@ -45,7 +46,7 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { var images []string if r.URL.Query().Has("image") { - images = []string{r.URL.Query().Get("image")} + images = strings.Split(r.URL.Query().Get("image"), ",") } else { images = nil } From 5c682d777dc2059c9f7fba4a56cd89c2eeb1758a Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Tue, 17 May 2022 18:16:16 +0200 Subject: [PATCH 04/12] Support comma-separated query parameter as well as specifying it multiple times MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: nils måsén --- pkg/api/update/update.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index 7769a3f8d..72463af43 100644 --- a/pkg/api/update/update.go +++ b/pkg/api/update/update.go @@ -45,8 +45,10 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { } var images []string - if r.URL.Query().Has("image") { - images = strings.Split(r.URL.Query().Get("image"), ",") + if imageQueries, found := r.URL.Query()["image"] { + for _, image := range imageQueries { + images = append(images, strings.Split(image, ",")) + } } else { images = nil } From ac9f5f00e9abb356aa06f010119192b611192ced Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Tue, 17 May 2022 18:57:00 +0200 Subject: [PATCH 05/12] fixed compile error --- pkg/api/update/update.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index 72463af43..122d67188 100644 --- a/pkg/api/update/update.go +++ b/pkg/api/update/update.go @@ -45,9 +45,10 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { } var images []string - if imageQueries, found := r.URL.Query()["image"] { + imageQueries, found := r.URL.Query()["image"] + if found { for _, image := range imageQueries { - images = append(images, strings.Split(image, ",")) + images = strings.Split(image, ",") } } else { images = nil From 1e2d9ebb81219647e9bd7b7d0be7cedc5350b30c Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Tue, 17 May 2022 18:57:08 +0200 Subject: [PATCH 06/12] fixed FilterByImageTag Not sure what changed in my testing setup, but Docker reports image names including the tag name now. --- pkg/filters/filters.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go index fcd8a191a..4bc779666 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -77,7 +77,7 @@ func FilterByImageTag(tags []string, baseFilter t.Filter) t.Filter { } return func(c t.FilterableContainer) bool { - image := c.Image() + image := strings.Split(c.Image(), ":")[0] for _, targetTag := range tags { if image == targetTag { return baseFilter(c) From 3b6ba5bf0f33e987d18cd8112cf916404235f2bc Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Tue, 17 May 2022 18:59:38 +0200 Subject: [PATCH 07/12] consistent use of image/tag (use image) --- cmd/root.go | 2 +- pkg/filters/filters.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 01e9f547e..57abdb55e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -189,7 +189,7 @@ func Run(c *cobra.Command, names []string) { httpAPI := api.New(apiToken) if enableUpdateAPI { - updateHandler := update.New(func(tags []string) { runUpdatesWithNotifications(filters.FilterByImageTag(tags, 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/filters/filters.go b/pkg/filters/filters.go index 4bc779666..e5b811158 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -70,16 +70,16 @@ func FilterByScope(scope string, baseFilter t.Filter) t.Filter { } } -// FilterByImageTag returns all containers that have a specific image -func FilterByImageTag(tags []string, baseFilter t.Filter) t.Filter { - if tags == nil { +// 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.Image(), ":")[0] - for _, targetTag := range tags { - if image == targetTag { + for _, targetImage := range images { + if image == targetImage { return baseFilter(c) } } From ec79075cde6d23b63a1d242e951b9d872a8bc7a9 Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Tue, 7 Jun 2022 17:14:18 +0200 Subject: [PATCH 08/12] fixed multiple image queries --- pkg/api/update/update.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index 122d67188..8f098b476 100644 --- a/pkg/api/update/update.go +++ b/pkg/api/update/update.go @@ -48,8 +48,9 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { imageQueries, found := r.URL.Query()["image"] if found { for _, image := range imageQueries { - images = strings.Split(image, ",") + images = append(images, strings.Split(image, ",")...) } + } else { images = nil } From c307cb7647fff2e81ce2f8f58981f3330be2bc12 Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Tue, 7 Jun 2022 17:50:10 +0200 Subject: [PATCH 09/12] assuming I'm right here, only block on lock when any images are specified. --- pkg/api/update/update.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index 8f098b476..ba044ab50 100644 --- a/pkg/api/update/update.go +++ b/pkg/api/update/update.go @@ -55,8 +55,18 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { images = nil } - chanValue := <-lock - defer func() { lock <- chanValue }() - handle.fn(images) + if len(images) > 0 { + chanValue := <-lock + defer func() { lock <- chanValue }() + handle.fn(images) + } else { + select { + case chanValue := <-lock: + defer func() { lock <- chanValue }() + handle.fn(images) + default: + log.Debug("Skipped. Another update already running.") + } + } } From 5a634d2e8dc3d6eeefa43fb643a9c4bf5e5b41f5 Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Thu, 9 Jun 2022 14:18:50 +0200 Subject: [PATCH 10/12] add unit tests for image filter. didn't add tests for update api because they didn't already exist --- pkg/filters/filters_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go index 3b52b5ee5..8350660ed 100644 --- a/pkg/filters/filters_test.go +++ b/pkg/filters/filters_test.go @@ -110,6 +110,41 @@ func TestFilterByDisabledLabel(t *testing.T) { container.AssertExpectations(t) } +func TestFilterByImage(t *testing.T) { + filterSingle := FilterByImage([]string{"registry"}, NoFilter) + filterMultiple := FilterByImage([]string{"registry", "bla"}, NoFilter) + assert.NotNil(t, filterSingle) + assert.NotNil(t, filterMultiple) + + filterNil := FilterByImage(nil, NoFilter) + assert.Same(t, filterNil, NoFilter) + + container := new(mocks.FilterableContainer) + container.On("Image").Return("registry:2") + assert.True(t, filterSingle(container)) + assert.True(t, filterMultiple(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Image").Return("registry:latest") + assert.True(t, filterSingle(container)) + assert.True(t, filterMultiple(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Image").Return("abcdef1234") + assert.False(t, filterSingle(container)) + assert.False(t, filterMultiple(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Image").Return("bla:latest") + 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") From a66f39dba14c6163390f5c920101da9da2f69e06 Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Thu, 9 Jun 2022 15:58:21 +0200 Subject: [PATCH 11/12] whoops. --- pkg/filters/filters_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go index 8350660ed..90e52f161 100644 --- a/pkg/filters/filters_test.go +++ b/pkg/filters/filters_test.go @@ -116,9 +116,6 @@ func TestFilterByImage(t *testing.T) { assert.NotNil(t, filterSingle) assert.NotNil(t, filterMultiple) - filterNil := FilterByImage(nil, NoFilter) - assert.Same(t, filterNil, NoFilter) - container := new(mocks.FilterableContainer) container.On("Image").Return("registry:2") assert.True(t, filterSingle(container)) From a4ba4733231980bfb910beefde395080e9e39c2e Mon Sep 17 00:00:00 2001 From: Dirk Kok Date: Sat, 11 Jun 2022 16:09:54 +0200 Subject: [PATCH 12/12] use ImageName instead, add unit test for empty ImageName filter. --- pkg/container/container.go | 5 ----- pkg/container/mocks/FilterableContainer.go | 4 ++-- pkg/filters/filters.go | 2 +- pkg/filters/filters_test.go | 13 +++++++++---- pkg/types/filterable_container.go | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/container/container.go b/pkg/container/container.go index fe5cc9958..82ae20572 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -326,8 +326,3 @@ func (c Container) VerifyConfiguration() error { return nil } - -// Image returns the image tag of the container as reported by the runtime config -func (c Container) Image() string { - return c.runtimeConfig().Image -} diff --git a/pkg/container/mocks/FilterableContainer.go b/pkg/container/mocks/FilterableContainer.go index 29df3cf25..fa863b51a 100644 --- a/pkg/container/mocks/FilterableContainer.go +++ b/pkg/container/mocks/FilterableContainer.go @@ -79,8 +79,8 @@ func (_m *FilterableContainer) Scope() (string, bool) { return r0, r1 } -// Image provides a mock function with given fields: -func (_m *FilterableContainer) Image() string { +// ImageName provides a mock function with given fields: +func (_m *FilterableContainer) ImageName() string { ret := _m.Called() var r0 string diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go index e5b811158..a283301da 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -77,7 +77,7 @@ func FilterByImage(images []string, baseFilter t.Filter) t.Filter { } return func(c t.FilterableContainer) bool { - image := strings.Split(c.Image(), ":")[0] + image := strings.Split(c.ImageName(), ":")[0] for _, targetImage := range images { if image == targetImage { return baseFilter(c) diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go index 90e52f161..c07b1812f 100644 --- a/pkg/filters/filters_test.go +++ b/pkg/filters/filters_test.go @@ -111,31 +111,36 @@ func TestFilterByDisabledLabel(t *testing.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("Image").Return("registry:2") + 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("Image").Return("registry:latest") + 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("Image").Return("abcdef1234") + 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("Image").Return("bla:latest") + container.On("ImageName").Return("bla:latest") + assert.True(t, filterEmpty(container)) assert.False(t, filterSingle(container)) assert.True(t, filterMultiple(container)) container.AssertExpectations(t) diff --git a/pkg/types/filterable_container.go b/pkg/types/filterable_container.go index efd9ba694..b410b1cbc 100644 --- a/pkg/types/filterable_container.go +++ b/pkg/types/filterable_container.go @@ -7,5 +7,5 @@ type FilterableContainer interface { IsWatchtower() bool Enabled() (bool, bool) Scope() (string, bool) - Image() string + ImageName() string }