diff --git a/docs/faq.md b/docs/faq.md index 332bfbb39..6230d46db 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -203,7 +203,7 @@ regopts: Or you can tweak the [`schedule` setting](config/watch.md#schedule) with something like `0 */6 * * *` (every 6 hours). !!! warning - Also be careful with the `watch_repo` setting as it will fetch manifest for **ALL** tags available for the image. + Also be careful with the `watch_repo` setting as it will fetch manifest for **ALL** tags available for the image if set to true. If using semver sorting, you can set `watch_repo` to semver and it will instead only watch images that are newer versions than the current image. ## Tags sorting when using `watch_repo` diff --git a/internal/app/job.go b/internal/app/job.go index 447df8a39..8f2ae64b3 100644 --- a/internal/app/job.go +++ b/internal/app/job.go @@ -63,7 +63,7 @@ func (di *Diun) createJob(job model.Job) { // Set defaults if err := mergo.Merge(&job.Image, model.Image{ Platform: model.ImagePlatform{}, - WatchRepo: utl.NewFalse(), + WatchRepo: model.WatchRepoNo, MaxTags: 0, }); err != nil { sublog.Error().Err(err).Msg("Cannot set default values") @@ -118,7 +118,7 @@ func (di *Diun) createJob(job model.Job) { sublog.Error().Err(err).Msgf("Invoking job") } - if !*job.Image.WatchRepo || len(job.RegImage.Domain) == 0 { + if job.Image.WatchRepo == model.WatchRepoNo || len(job.RegImage.Domain) == 0 { return } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 1feedcfe3..f524971c0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -60,7 +60,7 @@ func TestLoadFile(t *testing.T) { }, }, Defaults: &model.Defaults{ - WatchRepo: utl.NewFalse(), + WatchRepo: model.WatchRepoNo, NotifyOn: []model.NotifyOn{model.NotifyOnNew}, MaxTags: 5, SortTags: registry.SortTagReverse, diff --git a/internal/model/defaults.go b/internal/model/defaults.go index 88a78a904..83789cccc 100644 --- a/internal/model/defaults.go +++ b/internal/model/defaults.go @@ -2,12 +2,11 @@ package model import ( "github.com/crazy-max/diun/v4/pkg/registry" - "github.com/crazy-max/diun/v4/pkg/utl" ) // Defaults holds data necessary for image defaults configuration type Defaults struct { - WatchRepo *bool `yaml:"watchRepo,omitempty" json:"watchRepo,omitempty"` + WatchRepo WatchRepo `yaml:"watchRepo,omitempty" json:"watchRepo,omitempty"` NotifyOn []NotifyOn `yaml:"notifyOn,omitempty" json:"notifyOn,omitempty"` MaxTags int `yaml:"maxTags,omitempty" json:"maxTags,omitempty"` SortTags registry.SortTag `yaml:"sortTags,omitempty" json:"sortTags,omitempty"` @@ -25,7 +24,7 @@ func (s *Defaults) GetDefaults() *Defaults { // SetDefaults sets the default values func (s *Defaults) SetDefaults() { - s.WatchRepo = utl.NewFalse() + s.WatchRepo = WatchRepoNo s.NotifyOn = NotifyOnDefaults s.SortTags = registry.SortTagReverse } diff --git a/internal/model/image.go b/internal/model/image.go index fe712df57..b1ea71d97 100644 --- a/internal/model/image.go +++ b/internal/model/image.go @@ -1,6 +1,8 @@ package model import ( + "slices" + "github.com/crazy-max/diun/v4/pkg/registry" ) @@ -9,7 +11,7 @@ type Image struct { Name string `yaml:"name,omitempty" json:",omitempty"` Platform ImagePlatform `yaml:"platform,omitempty" json:",omitempty"` RegOpt string `yaml:"regopt,omitempty" json:",omitempty"` - WatchRepo *bool `yaml:"watch_repo,omitempty" json:",omitempty"` + WatchRepo WatchRepo `yaml:"watch_repo,omitempty" json:",omitempty"` NotifyOn []NotifyOn `yaml:"notify_on,omitempty" json:",omitempty"` MaxTags int `yaml:"max_tags,omitempty" json:",omitempty"` SortTags registry.SortTag `yaml:"sort_tags,omitempty" json:",omitempty"` @@ -27,6 +29,25 @@ type ImagePlatform struct { Variant string `yaml:"variant,omitempty" json:",omitempty"` } +// WatchRepo constants +const ( + WatchRepoNo = WatchRepo("false") + WatchRepoAll = WatchRepo("true") + WatchRepoSemver = WatchRepo("semver") +) + +// Valid checks watch repo is valid +func (w WatchRepo) Valid() bool { + return slices.Contains([]WatchRepo{ + WatchRepoNo, + WatchRepoAll, + WatchRepoSemver, + }, w) +} + +// WatchRepo holds repo watching intent +type WatchRepo string + // ImageStatus constants const ( ImageStatusNew = ImageStatus("new") diff --git a/internal/provider/common.go b/internal/provider/common.go index 36ed17027..36dbcad31 100644 --- a/internal/provider/common.go +++ b/internal/provider/common.go @@ -51,9 +51,14 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef img.RegOpt = value case key == "diun.watch_repo": if watchRepo, err := strconv.ParseBool(value); err == nil { - img.WatchRepo = &watchRepo + // If boolean value, coerce it into true/false + img.WatchRepo = model.WatchRepo(strconv.FormatBool(watchRepo)) } else { - return img, errors.Wrapf(err, "cannot parse %q value of label %s", value, key) + // Otherwise use it directly + img.WatchRepo = model.WatchRepo(value) + } + if !img.WatchRepo.Valid() { + return img, errors.Errorf(`invalid value of watch_repo %q`, value) } case key == "diun.notify_on": if len(value) == 0 { diff --git a/internal/provider/common_test.go b/internal/provider/common_test.go index 0982d69c0..b469c624a 100644 --- a/internal/provider/common_test.go +++ b/internal/provider/common_test.go @@ -5,7 +5,6 @@ import ( "github.com/crazy-max/diun/v4/internal/model" "github.com/crazy-max/diun/v4/pkg/registry" - "github.com/crazy-max/diun/v4/pkg/utl" "github.com/pkg/errors" "github.com/stretchr/testify/require" ) @@ -111,11 +110,11 @@ func TestValidateImage(t *testing.T) { image: "myimg", watchByDef: true, defaults: &model.Defaults{ - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, }, expectedImage: model.Image{ Name: "myimg", - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, }, expectedErr: nil, }, @@ -130,7 +129,7 @@ func TestValidateImage(t *testing.T) { expectedImage: model.Image{ Name: "myimg", }, - expectedErr: errors.New(`cannot parse "chickens" value of label diun.watch_repo`), + expectedErr: errors.New(`invalid value of watch_repo "chickens"`), }, { name: "Override default image values with labels (true > false)", @@ -140,27 +139,43 @@ func TestValidateImage(t *testing.T) { "diun.watch_repo": "false", }, defaults: &model.Defaults{ - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, }, expectedImage: model.Image{ Name: "myimg", - WatchRepo: utl.NewFalse(), + WatchRepo: model.WatchRepoNo, }, expectedErr: nil, }, { - name: "Override default image values with labels (false > true): invalid label error", + name: "Override default image values with labels (false > true)", image: "myimg", watchByDef: true, labels: map[string]string{ "diun.watch_repo": "true", }, defaults: &model.Defaults{ - WatchRepo: utl.NewFalse(), + WatchRepo: model.WatchRepoNo, }, expectedImage: model.Image{ Name: "myimg", - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, + }, + expectedErr: nil, + }, + { + name: "Override default image values with labels (false > semver)", + image: "myimg", + watchByDef: true, + labels: map[string]string{ + "diun.watch_repo": "semver", + }, + defaults: &model.Defaults{ + WatchRepo: model.WatchRepoNo, + }, + expectedImage: model.Image{ + Name: "myimg", + WatchRepo: model.WatchRepoSemver, }, expectedErr: nil, }, diff --git a/internal/provider/file/file_test.go b/internal/provider/file/file_test.go index 26d3cf490..25f749b5c 100644 --- a/internal/provider/file/file_test.go +++ b/internal/provider/file/file_test.go @@ -5,7 +5,6 @@ import ( "github.com/crazy-max/diun/v4/internal/model" "github.com/crazy-max/diun/v4/pkg/registry" - "github.com/crazy-max/diun/v4/pkg/utl" "github.com/stretchr/testify/assert" ) @@ -42,7 +41,7 @@ var ( Provider: "file", Image: model.Image{ Name: "docker.bintray.io/jfrog/xray-server:2.8.6", - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, NotifyOn: []model.NotifyOn{ model.NotifyOnNew, }, @@ -78,7 +77,7 @@ var ( Provider: "file", Image: model.Image{ Name: "crazymax/swarm-cronjob", - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, NotifyOn: model.NotifyOnDefaults, SortTags: registry.SortTagSemver, MaxTags: 25, @@ -94,7 +93,7 @@ var ( Provider: "file", Image: model.Image{ Name: "docker.io/portainer/portainer", - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, NotifyOn: model.NotifyOnDefaults, MaxTags: 10, SortTags: registry.SortTagReverse, @@ -110,7 +109,7 @@ var ( Provider: "file", Image: model.Image{ Name: "traefik", - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, NotifyOn: model.NotifyOnDefaults, SortTags: registry.SortTagDefault, MaxTags: 25, @@ -176,7 +175,7 @@ var ( Provider: "file", Image: model.Image{ Name: "crazymax/ddns-route53", - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, NotifyOn: model.NotifyOnDefaults, SortTags: registry.SortTagReverse, MaxTags: 25, @@ -261,10 +260,10 @@ func TestDefaultImageOptions(t *testing.T) { fc := New(&model.PrdFile{ Filename: "./fixtures/dockerhub.yml", }, &model.Defaults{ - WatchRepo: utl.NewTrue(), + WatchRepo: model.WatchRepoAll, }) for _, job := range fc.ListJob() { - assert.True(t, *job.Image.WatchRepo) + assert.Equal(t, model.WatchRepoAll, job.Image.WatchRepo) } } diff --git a/internal/provider/file/image.go b/internal/provider/file/image.go index 836e63fcb..4775890be 100644 --- a/internal/provider/file/image.go +++ b/internal/provider/file/image.go @@ -33,7 +33,7 @@ func (c *Client) listFileImage() []model.Image { for _, item := range items { // Set default WatchRepo - if item.WatchRepo == nil { + if item.WatchRepo == "" { item.WatchRepo = c.defaults.WatchRepo } // Check NotifyOn diff --git a/pkg/registry/tags.go b/pkg/registry/tags.go index f8955ee62..18b323df3 100644 --- a/pkg/registry/tags.go +++ b/pkg/registry/tags.go @@ -22,11 +22,12 @@ type Tags struct { // TagsOptions holds docker tags image options type TagsOptions struct { - Image Image - Max int - Sort SortTag - Include []string - Exclude []string + Image Image + Max int + Sort SortTag + Include []string + Exclude []string + ExcludeOldVersions bool } // Tags returns tags of a Docker repository @@ -53,6 +54,8 @@ func (c *Client) Tags(opts TagsOptions) (*Tags, error) { // Sort tags tags = SortTags(tags, opts.Sort) + foundCurrent := false + // Filter for _, tag := range tags { if !utl.IsIncluded(tag, opts.Include) { @@ -61,6 +64,15 @@ func (c *Client) Tags(opts TagsOptions) (*Tags, error) { } else if utl.IsExcluded(tag, opts.Exclude) { res.Excluded++ continue + } else if opts.ExcludeOldVersions && opts.Sort == SortTagSemver { + if foundCurrent { + res.Excluded++ + continue + } + + if tag == opts.Image.Tag { + foundCurrent = true + } } res.List = append(res.List, tag) } diff --git a/pkg/registry/tags_test.go b/pkg/registry/tags_test.go index ecd452bed..56cbccee3 100644 --- a/pkg/registry/tags_test.go +++ b/pkg/registry/tags_test.go @@ -8,46 +8,72 @@ import ( func TestTags(t *testing.T) { assert.NotNil(t, rc) + t.Parallel() - image, err := ParseImage(ParseImageOptions{ - Name: "crazymax/diun:3.0.0", - }) - if err != nil { - t.Error(err) - } - - tags, err := rc.Tags(TagsOptions{ - Image: image, - }) - if err != nil { - t.Error(err) + cases := []struct { + name string + imageName string + excludeOldVersions bool + expectedNotIncluded bool + expectedExcluded bool + expectedContains string + }{ + { + "parse image and tag", + "crazymax/diun:3.0.0", + false, + false, + false, + "4.0.0", + }, + { + "parse image digest", + "crazymax/diun:latest@sha256:3fca3dd86c2710586208b0f92d1ec4ce25382f4cad4ae76a2275db8e8bb24031", + false, + false, + false, + "4.0.0", + }, + { + "exclude older semver", + "crazymax/diun:4.0.0", + true, + false, + true, + "4.20", + }, } - assert.Greater(t, tags.Total, 0) - assert.Greater(t, len(tags.List), 0) -} + for _, c := range cases { + c := c -func TestTagsWithDigest(t *testing.T) { - t.Parallel() + t.Run(c.name, func(t *testing.T) { + image, err := ParseImage(ParseImageOptions{ + Name: c.imageName, + }) + if err != nil { + t.Fatal(err) + } - assert.NotNil(t, rc) + tags, err := rc.Tags(TagsOptions{ + Image: image, + Sort: SortTagSemver, + ExcludeOldVersions: c.excludeOldVersions, + }) + if err != nil { + t.Fatal(err) + } - image, err := ParseImage(ParseImageOptions{ - Name: "crazymax/diun:latest@sha256:3fca3dd86c2710586208b0f92d1ec4ce25382f4cad4ae76a2275db8e8bb24031", - }) - if err != nil { - t.Error(err) - } + assert.Greater(t, tags.Total, 0) + assert.Greater(t, len(tags.List), 0) + // Make sure final list includes original tag and additional expected + assert.Contains(t, tags.List, image.Tag) + assert.Contains(t, tags.List, c.expectedContains) - tags, err := rc.Tags(TagsOptions{ - Image: image, - }) - if err != nil { - t.Error(err) + assert.Equal(t, c.expectedExcluded, tags.Excluded > 0, "Unexpected excluded tags") + assert.Equal(t, c.expectedNotIncluded, tags.NotIncluded > 0, "Unexpected not included tags") + }) } - - assert.Greater(t, tags.Total, 0) - assert.Greater(t, len(tags.List), 0) } func TestTagsSort(t *testing.T) {