diff --git a/docs/faq.md b/docs/faq.md index 8fe4b8e1..51616125 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -54,34 +54,34 @@ The title and body of a notification message can be customized for each notifier Templating is supported with the following fields: -| Key | Description | -|----------------------------------|-------------| -| `.Meta.ID` | App ID: `diun` | -| `.Meta.Name` | App Name: `Diun` | -| `.Meta.Desc` | App description: `Docker image update notifier` | -| `.Meta.URL` | App repo URL: `https://github.com/crazy-max/diun` | -| `.Meta.Logo` | App logo URL: `https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png` | -| `.Meta.Author` | App author: `CrazyMax` | -| `.Meta.Version` | App version: `v4.19.0` | -| `.Meta.UserAgent` | App user-agent used to talk with registries: `diun/4.19.0 go/1.16 Linux` | -| `.Meta.Hostname` | Hostname | -| `.Entry.Status` | Entry status. Can be `new`, `update`, `unchange`, `skip` or `error` | -| `.Entry.Provider` | [Provider](config/providers.md) used | -| `.Entry.Image` | Docker image name. e.g. `docker.io/crazymax/diun:latest` | -| `.Entry.Image.Domain` | Docker image domain. e.g. `docker.io` | -| `.Entry.Image.Path` | Docker image path. e.g. `crazymax/diun` | -| `.Entry.Image.Tag` | Docker image tag. e.g. `latest` | -| `.Entry.Image.Digest` | Docker image digest | -| `.Entry.Image.HubLink` | Docker image hub link (if available). e.g. `https://hub.docker.com/r/crazymax/diun` | -| `.Entry.Manifest.Name` | Manifest name. e.g. `docker.io/crazymax/diun` | -| `.Entry.Manifest.Tag` | Manifest tag. e.g. `latest` | -| `.Entry.Manifest.MIMEType` | Manifest MIME type. e.g. `application/vnd.docker.distribution.manifest.list.v2+json` | -| `.Entry.Manifest.Digest` | Manifest digest | -| `.Entry.Manifest.Created` | Manifest created date. e.g. `2021-06-20T12:23:56Z` | -| `.Entry.Manifest.DockerVersion` | Version of Docker that was used to build the image. e.g. `20.10.7` | -| `.Entry.Manifest.Labels` | Image labels | -| `.Entry.Manifest.Layers` | Image layers | -| `.Entry.Manifest.Platform` | Platform that the image is runs on. e.g. `linux/amd64` | +| Key | Description | +|---------------------------------|---------------------------------------------------------------------------------------| +| `.Meta.ID` | App ID: `diun` | +| `.Meta.Name` | App Name: `Diun` | +| `.Meta.Desc` | App description: `Docker image update notifier` | +| `.Meta.URL` | App repo URL: `https://github.com/crazy-max/diun` | +| `.Meta.Logo` | App logo URL: `https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png` | +| `.Meta.Author` | App author: `CrazyMax` | +| `.Meta.Version` | App version: `v4.19.0` | +| `.Meta.UserAgent` | App user-agent used to talk with registries: `diun/4.19.0 go/1.16 Linux` | +| `.Meta.Hostname` | Hostname | +| `.Entry.Status` | Entry status. Can be `new`, `update`, `unchange`, `skip` or `error` | +| `.Entry.Provider` | [Provider](config/providers.md) used | +| `.Entry.Image` | Docker image name. e.g. `docker.io/crazymax/diun:latest` | +| `.Entry.Image.Domain` | Docker image domain. e.g. `docker.io` | +| `.Entry.Image.Path` | Docker image path. e.g. `crazymax/diun` | +| `.Entry.Image.Tag` | Docker image tag. e.g. `latest` | +| `.Entry.Image.Digest` | Docker image digest | +| `.Entry.Image.HubLink` | Docker image hub link (if available). e.g. `https://hub.docker.com/r/crazymax/diun` | +| `.Entry.Manifest.Name` | Manifest name. e.g. `docker.io/crazymax/diun` | +| `.Entry.Manifest.Tag` | Manifest tag. e.g. `latest` | +| `.Entry.Manifest.MIMEType` | Manifest MIME type. e.g. `application/vnd.docker.distribution.manifest.list.v2+json` | +| `.Entry.Manifest.Digest` | Manifest digest | +| `.Entry.Manifest.Created` | Manifest created date. e.g. `2021-06-20T12:23:56Z` | +| `.Entry.Manifest.DockerVersion` | Version of Docker that was used to build the image. e.g. `20.10.7` | +| `.Entry.Manifest.Labels` | Image labels | +| `.Entry.Manifest.Layers` | Image layers | +| `.Entry.Manifest.Platform` | Platform that the image is runs on. e.g. `linux/amd64` | ## Authentication against the registry @@ -205,6 +205,97 @@ Or you can tweak the [`schedule` setting](config/watch.md#schedule) with somethi !!! warning Also be careful with the `watch_repo` setting as it will fetch manifest for **ALL** tags available for the image. +## Tags sorting when using `watch_repo` + +When you use the `watch_repo` setting, Diun will fetch all tags available for +the image. Depending on the registry, order of the tags list can change. + +You can use the `sort_tags` setting available for each provider to use a +specific sorting method for the tags list. + +* `default`: do not sort and use the expected tags list from the registry +* `reverse`: reverse order for the tags list from the registry +* `lexicographical`: sort the tags list lexicographically +* `semver`: sort the tags list using semantic versioning + +Given the following list of tags received from the registry: + +```json +[ + "0.1.0", + "0.4.0", + "3.0.0-beta.1", + "3.0.0-beta.4", + "4", + "4.0.0", + "4.0.0-beta.1", + "4.1.0", + "4.1.1", + "4.10.0", + "4.11.0", + "4.20", + "4.20.0", + "4.20.1", + "4.3.0", + "4.3.1", + "4.9.0", + "edge", + "latest" +] +``` + +Here is the result for `reverse`: + +```json +[ + "latest", + "edge", + "4.9.0", + "4.3.1", + "4.3.0", + "4.20.1", + "4.20.0", + "4.20", + "4.11.0", + "4.10.0", + "4.1.1", + "4.1.0", + "4.0.0-beta.1", + "4.0.0", + "4", + "3.0.0-beta.4", + "3.0.0-beta.1", + "0.4.0", + "0.1.0" +] +``` + +And for `semver`: + +```json +[ + "4.20.1", + "4.20.0", + "4.20", + "4.11.0", + "4.10.0", + "4.9.0", + "4.3.1", + "4.3.0", + "4.1.1", + "4.1.0", + "4.0.0", + "4", + "4.0.0-beta.1", + "3.0.0-beta.4", + "3.0.0-beta.1", + "0.4.0", + "0.1.0", + "edge", + "latest" +] +``` + ## Profiling Diun provides a simple way to manage runtime/pprof profiling through the diff --git a/docs/providers/docker.md b/docs/providers/docker.md index d5e0323c..8457831e 100644 --- a/docs/providers/docker.md +++ b/docs/providers/docker.md @@ -172,13 +172,14 @@ Include created and exited containers too (default `false`). You can configure more finely the way to analyze the image of your container through Docker labels: -| Name | Default | Description | -|-------------------------------|---------------|---------------| -| `diun.enable` | | Set to true to enable image analysis of this container | -| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | -| `diun.watch_repo` | `false` | Watch all tags of this container image ([be careful](../faq.md#docker-hub-rate-limits) with this setting) | -| `diun.notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update`. | -| `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them | -| `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | -| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | -| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | +| Name | Default | Description | +|---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `diun.enable` | | Set to true to enable image analysis of this container | +| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | +| `diun.watch_repo` | `false` | Watch all tags of this container image ([be careful](../faq.md#docker-hub-rate-limits) with this setting) | +| `diun.notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update` | +| `diun.sort_tags` | `reverse` | [Sort tags method](../faq.md#tags-sorting-when-using-watch_repo) if `diun.watch_repo` enabled. One of `default`, `reverse`, `numerical`, `lexicographical` | +| `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them | +| `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | +| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | +| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | diff --git a/docs/providers/dockerfile.md b/docs/providers/dockerfile.md index a6eb0aa4..7b52b5ad 100644 --- a/docs/providers/dockerfile.md +++ b/docs/providers/dockerfile.md @@ -106,12 +106,13 @@ List of path patterns with [matching and globbing supporting patterns](https://g The following annotations can be added as comments before the target instruction to customize the image analysis: -| Name | Default | Description | -|-------------------------------|---------------|---------------| -| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | -| `diun.watch_repo` | `false` | Watch all tags of this image | -| `diun.notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update`. | -| `diun.max_tags` | `0` | Maximum number of tags to watch if `watch_repo` enabled. `0` means all of them | -| `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | -| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | -| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | +| Name | Default | Description | +|---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | +| `diun.watch_repo` | `false` | Watch all tags of this image | +| `diun.notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update` | +| `diun.sort_tags` | `reverse` | [Sort tags method](../faq.md#tags-sorting-when-using-watch_repo) if `diun.watch_repo` enabled. One of `default`, `reverse`, `numerical`, `lexicographical` | +| `diun.max_tags` | `0` | Maximum number of tags to watch if `watch_repo` enabled. `0` means all of them | +| `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | +| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | +| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | diff --git a/docs/providers/file.md b/docs/providers/file.md index 5e122b43..d3dc7d25 100644 --- a/docs/providers/file.md +++ b/docs/providers/file.md @@ -172,15 +172,16 @@ Defines the path to the directory that contains the [configuration files](#yaml- The configuration file(s) defines a slice of images to analyze with the following fields: -| Name | Default | Description | -|-------------------------------|----------------------------------|---------------| -| `name` | `latest` | Docker image name to watch using `registry/path:tag` format. If registry omitted, `docker.io` will be used and if tag omitted, `latest` will be used | -| `regopt` | | [Registry options](../config/regopts.md) name to use | -| `watch_repo` | `false` | Watch all tags of this image ([be careful](../faq.md#docker-hub-rate-limits) with this setting) | -| `notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update`. | -| `max_tags` | `0` | Maximum number of tags to watch if `watch_repo` enabled. `0` means all of them | -| `include_tags` | | List of regular expressions to include tags. Can be useful if you enable `watch_repo` | -| `exclude_tags` | | List of regular expressions to exclude tags. Can be useful if you enable `watch_repo` | -| `platform.os` | _automatic_ | Operating system to use as custom platform | -| `platform.arch` | _automatic_ | CPU architecture to use as custom platform | -| `platform.variant` | _automatic_ | Variant of the CPU to use as custom platform | +| Name | Default | Description | +|--------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `name` | `latest` | Docker image name to watch using `registry/path:tag` format. If registry omitted, `docker.io` will be used and if tag omitted, `latest` will be used | +| `regopt` | | [Registry options](../config/regopts.md) name to use | +| `watch_repo` | `false` | Watch all tags of this image ([be careful](../faq.md#docker-hub-rate-limits) with this setting) | +| `notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update` | +| `sort_tags` | `reverse` | [Sort tags method](../faq.md#tags-sorting-when-using-watch_repo) if `diun.watch_repo` enabled. One of `default`, `reverse`, `numerical`, `lexicographical` | +| `max_tags` | `0` | Maximum number of tags to watch if `watch_repo` enabled. `0` means all of them | +| `include_tags` | | List of regular expressions to include tags. Can be useful if you enable `watch_repo` | +| `exclude_tags` | | List of regular expressions to exclude tags. Can be useful if you enable `watch_repo` | +| `platform.os` | _automatic_ | Operating system to use as custom platform | +| `platform.arch` | _automatic_ | CPU architecture to use as custom platform | +| `platform.variant` | _automatic_ | Variant of the CPU to use as custom platform | diff --git a/docs/providers/kubernetes.md b/docs/providers/kubernetes.md index dba8ff1d..929c0454 100644 --- a/docs/providers/kubernetes.md +++ b/docs/providers/kubernetes.md @@ -280,13 +280,14 @@ Enable watch by default. If false, pods that don't have `diun.enable: "true"` an You can configure more finely the way to analyze the image of your pods through Kubernetes annotations: -| Name | Default | Description | -|-------------------------------|---------------|---------------| -| `diun.enable` | | Set to true to enable image analysis of this pod | -| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | -| `diun.watch_repo` | `false` | Watch all tags of this pod image ([be careful](../faq.md#docker-hub-rate-limits) with this setting) | -| `diun.notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update`. | -| `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them | -| `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | -| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | -| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | +| Name | Default | Description | +|---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `diun.enable` | | Set to true to enable image analysis of this pod | +| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | +| `diun.watch_repo` | `false` | Watch all tags of this pod image ([be careful](../faq.md#docker-hub-rate-limits) with this setting) | +| `diun.notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update`. | +| `diun.sort_tags` | `reverse` | [Sort tags method](../faq.md#tags-sorting-when-using-watch_repo) if `diun.watch_repo` enabled. One of `default`, `reverse`, `numerical`, `lexicographical` | +| `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them | +| `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | +| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | +| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | diff --git a/docs/providers/swarm.md b/docs/providers/swarm.md index 5bc7d7dd..760433a4 100644 --- a/docs/providers/swarm.md +++ b/docs/providers/swarm.md @@ -174,13 +174,14 @@ Enable watch by default. If false, services that don't have `diun.enable=true` l You can configure more finely the way to analyze the image of your service through Docker labels: -| Name | Default | Description | -|-------------------------------|---------------|---------------| -| `diun.enable` | | Set to true to enable image analysis of this service | -| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | -| `diun.watch_repo` | `false` | Watch all tags of this service image ([be careful](../faq.md#docker-hub-rate-limits) with this setting) | -| `diun.notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update`. | -| `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them | -| `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | -| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | -| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | +| Name | Default | Description | +|---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `diun.enable` | | Set to true to enable image analysis of this service | +| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | +| `diun.watch_repo` | `false` | Watch all tags of this service image ([be careful](../faq.md#docker-hub-rate-limits) with this setting) | +| `diun.notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update`. | +| `diun.sort_tags` | `reverse` | [Sort tags method](../faq.md#tags-sorting-when-using-watch_repo) if `diun.watch_repo` enabled. One of `default`, `reverse`, `numerical`, `lexicographical` | +| `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them | +| `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | +| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | +| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | diff --git a/go.mod b/go.mod index 24d39951..c985afac 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/stretchr/testify v1.8.0 github.com/tidwall/pretty v1.2.0 go.etcd.io/bbolt v1.3.6 + golang.org/x/mod v0.5.1 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 google.golang.org/grpc v1.48.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 diff --git a/go.sum b/go.sum index d7c6d2cd..5d954f1d 100644 --- a/go.sum +++ b/go.sum @@ -1144,6 +1144,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/internal/app/job.go b/internal/app/job.go index fc0b81f3..1cca5f8e 100644 --- a/internal/app/job.go +++ b/internal/app/job.go @@ -124,6 +124,7 @@ func (di *Diun) createJob(job model.Job) { tags, err := job.Registry.Tags(registry.TagsOptions{ Image: job.RegImage, Max: job.Image.MaxTags, + Sort: job.Image.SortTags, Include: job.Image.IncludeTags, Exclude: job.Image.ExcludeTags, }) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index aac1ec02..1d69e2b2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -360,6 +360,7 @@ func TestLoadEnv(t *testing.T) { } for _, tt := range testCases { + tt := tt t.Run(tt.desc, func(t *testing.T) { UnsetEnv("DIUN_") @@ -484,6 +485,7 @@ for {{ .Entry.Manifest.Platform }} platform. } for _, tt := range testCases { + tt := tt t.Run(tt.desc, func(t *testing.T) { UnsetEnv("DIUN_") diff --git a/internal/model/image.go b/internal/model/image.go index 95a66b8f..1baec90f 100644 --- a/internal/model/image.go +++ b/internal/model/image.go @@ -1,16 +1,19 @@ package model +import "github.com/crazy-max/diun/v4/pkg/registry" + // Image holds image configuration 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"` - NotifyOn []NotifyOn `yaml:"notify_on,omitempty" json:",omitempty"` - MaxTags int `yaml:"max_tags,omitempty" json:",omitempty"` - IncludeTags []string `yaml:"include_tags,omitempty" json:",omitempty"` - ExcludeTags []string `yaml:"exclude_tags,omitempty" json:",omitempty"` - HubTpl string `yaml:"hub_tpl,omitempty" json:",omitempty"` + 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"` + 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"` + IncludeTags []string `yaml:"include_tags,omitempty" json:",omitempty"` + ExcludeTags []string `yaml:"exclude_tags,omitempty" json:",omitempty"` + HubTpl string `yaml:"hub_tpl,omitempty" json:",omitempty"` } // ImagePlatform holds image platform configuration diff --git a/internal/provider/common.go b/internal/provider/common.go index ffeeef92..dbd33b14 100644 --- a/internal/provider/common.go +++ b/internal/provider/common.go @@ -7,6 +7,7 @@ import ( "github.com/containerd/containerd/platforms" "github.com/crazy-max/diun/v4/internal/model" + "github.com/crazy-max/diun/v4/pkg/registry" ) // ValidateImage returns a standard image through Docker labels @@ -17,6 +18,7 @@ func ValidateImage(image string, labels map[string]string, watchByDef bool) (img img = model.Image{ Name: image, NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagReverse, } if enableStr, ok := labels["diun.enable"]; ok { @@ -51,6 +53,15 @@ func ValidateImage(image string, labels map[string]string, watchByDef bool) (img } img.NotifyOn = append(img.NotifyOn, notifyOn) } + case "diun.sort_tags": + if value == "" { + break + } + sortTags := registry.SortTag(value) + if !sortTags.Valid() { + return img, fmt.Errorf("unknown sort tags type %q", value) + } + img.SortTags = sortTags case "diun.max_tags": if img.MaxTags, err = strconv.Atoi(value); err != nil { return img, fmt.Errorf("cannot parse %s value of label %s", value, key) diff --git a/internal/provider/file/file_test.go b/internal/provider/file/file_test.go index 9109aee0..5e31ef43 100644 --- a/internal/provider/file/file_test.go +++ b/internal/provider/file/file_test.go @@ -5,6 +5,7 @@ import ( "github.com/crazy-max/diun/v4/internal/model" "github.com/crazy-max/diun/v4/internal/provider/file" + "github.com/crazy-max/diun/v4/pkg/registry" "github.com/stretchr/testify/assert" ) @@ -16,6 +17,7 @@ var ( Name: "jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0", RegOpt: "bintrayoptions", NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagReverse, }, }, { @@ -26,7 +28,8 @@ var ( NotifyOn: []model.NotifyOn{ model.NotifyOnNew, }, - MaxTags: 50, + SortTags: registry.SortTagLexicographical, + MaxTags: 50, }, }, } @@ -37,6 +40,7 @@ var ( Name: "docker.io/crazymax/nextcloud:latest", RegOpt: "myregistry", NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagReverse, }, }, { @@ -45,6 +49,7 @@ var ( Name: "crazymax/swarm-cronjob", WatchRepo: true, NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagSemver, IncludeTags: []string{ `^1\.2\..*`, }, @@ -57,6 +62,7 @@ var ( WatchRepo: true, NotifyOn: model.NotifyOnDefaults, MaxTags: 10, + SortTags: registry.SortTagReverse, IncludeTags: []string{ `^(0|[1-9]\d*)\..*`, }, @@ -68,6 +74,7 @@ var ( Name: "traefik", WatchRepo: true, NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagDefault, }, }, { @@ -75,6 +82,7 @@ var ( Image: model.Image{ Name: "alpine", NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagReverse, Platform: model.ImagePlatform{ OS: "linux", Arch: "arm64", @@ -87,6 +95,7 @@ var ( Image: model.Image{ Name: "docker.io/graylog/graylog:3.2.0", NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagReverse, }, }, { @@ -94,6 +103,7 @@ var ( Image: model.Image{ Name: "jacobalberty/unifi:5.9", NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagReverse, }, }, { @@ -102,6 +112,7 @@ var ( Name: "crazymax/ddns-route53", WatchRepo: true, NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagReverse, IncludeTags: []string{ `^1\..*`, }, @@ -114,6 +125,7 @@ var ( Image: model.Image{ Name: "quay.io/coreos/hyperkube", NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagReverse, }, }, { @@ -121,6 +133,7 @@ var ( Image: model.Image{ Name: "quay.io/coreos/hyperkube:v1.1.7-coreos.1", NotifyOn: model.NotifyOnDefaults, + SortTags: registry.SortTagReverse, }, }, } diff --git a/internal/provider/file/fixtures/bintray.yml b/internal/provider/file/fixtures/bintray.yml index 6b9cdd76..43a1859c 100644 --- a/internal/provider/file/fixtures/bintray.yml +++ b/internal/provider/file/fixtures/bintray.yml @@ -4,4 +4,5 @@ watch_repo: true notify_on: - new + sort_tags: lexicographical max_tags: 50 diff --git a/internal/provider/file/fixtures/dockerhub.yml b/internal/provider/file/fixtures/dockerhub.yml index e415be02..8176a65d 100644 --- a/internal/provider/file/fixtures/dockerhub.yml +++ b/internal/provider/file/fixtures/dockerhub.yml @@ -2,15 +2,18 @@ regopt: myregistry - name: crazymax/swarm-cronjob watch_repo: true + sort_tags: semver include_tags: - ^1\.2\..* - name: docker.io/portainer/portainer watch_repo: true max_tags: 10 + sort_tags: reverse include_tags: - ^(0|[1-9]\d*)\..* - name: traefik watch_repo: true + sort_tags: default - name: alpine platform: os: linux diff --git a/internal/provider/file/image.go b/internal/provider/file/image.go index 71672a4f..1339f801 100644 --- a/internal/provider/file/image.go +++ b/internal/provider/file/image.go @@ -7,6 +7,7 @@ import ( "github.com/containerd/containerd/platforms" "github.com/crazy-max/diun/v4/internal/model" + "github.com/crazy-max/diun/v4/pkg/registry" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "gopkg.in/yaml.v2" ) @@ -45,6 +46,17 @@ func (c *Client) listFileImage() []model.Image { } } + // Check SortType + if item.SortTags == "" { + item.SortTags = registry.SortTagReverse + } + if !item.SortTags.Valid() { + c.logger.Error(). + Str("file", file). + Str("img_name", item.Name). + Msgf("unknown sort tags type %q", item.SortTags) + } + // Check Platform if item.Platform != (model.ImagePlatform{}) { _, err = platforms.Parse(platforms.Format(ocispecs.Platform{ diff --git a/pkg/registry/image_test.go b/pkg/registry/image_test.go index d9fcdfd7..07d8e696 100644 --- a/pkg/registry/image_test.go +++ b/pkg/registry/image_test.go @@ -126,6 +126,7 @@ func TestParseImage(t *testing.T) { } for _, tt := range testCases { + tt := tt t.Run(tt.desc, func(t *testing.T) { img, err := registry.ParseImage(tt.parseOpts) if err != nil { @@ -239,6 +240,7 @@ func TestHubLink(t *testing.T) { } for _, tt := range testCases { + tt := tt t.Run(tt.desc, func(t *testing.T) { img, err := registry.ParseImage(tt.parseOpts) if err != nil { diff --git a/pkg/registry/ref_test.go b/pkg/registry/ref_test.go index 7d947efd..f3fbd7d7 100644 --- a/pkg/registry/ref_test.go +++ b/pkg/registry/ref_test.go @@ -52,6 +52,7 @@ func TestParseReference(t *testing.T) { } for _, tt := range testCases { + tt := tt t.Run(tt.input, func(t *testing.T) { ref, err := registry.ParseReference(tt.input) if tt.wantErr { diff --git a/pkg/registry/tags.go b/pkg/registry/tags.go index b18c97ad..4dd44c61 100644 --- a/pkg/registry/tags.go +++ b/pkg/registry/tags.go @@ -18,6 +18,7 @@ type Tags struct { type TagsOptions struct { Image Image Max int + Sort SortTag Include []string Exclude []string } @@ -43,6 +44,9 @@ func (c *Client) Tags(opts TagsOptions) (*Tags, error) { Total: len(tags), } + // Sort tags + tags = SortTags(tags, opts.Sort) + // Filter for _, tag := range tags { if !utl.IsIncluded(tag, opts.Include) { @@ -55,12 +59,6 @@ func (c *Client) Tags(opts TagsOptions) (*Tags, error) { res.List = append(res.List, tag) } - // Reverse order (latest tags first) - for i := len(res.List)/2 - 1; i >= 0; i-- { - opp := len(res.List) - 1 - i - res.List[i], res.List[opp] = res.List[opp], res.List[i] - } - if opts.Max > 0 && len(res.List) >= opts.Max { res.List = res.List[:opts.Max] } diff --git a/pkg/registry/tags_sort.go b/pkg/registry/tags_sort.go new file mode 100644 index 00000000..184e743e --- /dev/null +++ b/pkg/registry/tags_sort.go @@ -0,0 +1,79 @@ +package registry + +import ( + "fmt" + "sort" + "strings" + + "golang.org/x/mod/semver" +) + +// SortTags sorts tags list +func SortTags(tags []string, sortTag SortTag) []string { + switch sortTag { + case SortTagReverse: + for i := len(tags)/2 - 1; i >= 0; i-- { + opp := len(tags) - 1 - i + tags[i], tags[opp] = tags[opp], tags[i] + } + return tags + case SortTagLexicographical: + sort.Strings(tags) + return tags + case SortTagSemver: + semverIsh := func(s string) string { + if semver.IsValid(s) { + return s + } + if vt := fmt.Sprintf("v%s", s); semver.IsValid(vt) { + return vt + } + return "" + } + sort.Slice(tags, func(i, j int) bool { + if c := semver.Compare(semverIsh(tags[i]), semverIsh(tags[j])); c > 0 { + return true + } else if c < 0 { + return false + } + return strings.Count(tags[i], ".") > strings.Count(tags[j], ".") + }) + return tags + default: + return tags + } +} + +// SortTag holds sort tag type +type SortTag string + +// SortTag constants +const ( + SortTagDefault = SortTag("default") + SortTagReverse = SortTag("reverse") + SortTagLexicographical = SortTag("lexicographical") + SortTagSemver = SortTag("semver") +) + +// SortTagTypes is the list of available sort tag types +var SortTagTypes = []SortTag{ + SortTagDefault, + SortTagReverse, + SortTagLexicographical, + SortTagSemver, +} + +// Valid checks sort tag type is valid +func (st *SortTag) Valid() bool { + return st.OneOf(SortTagTypes) +} + +// OneOf checks if sort type is one of the values in the list +func (st *SortTag) OneOf(stl []SortTag) bool { + for _, n := range stl { + if n == *st { + return true + } + } + return false +} diff --git a/pkg/registry/tags_test.go b/pkg/registry/tags_test.go index 073a1be3..943ae21a 100644 --- a/pkg/registry/tags_test.go +++ b/pkg/registry/tags_test.go @@ -27,3 +27,207 @@ func TestTags(t *testing.T) { assert.True(t, tags.Total > 0) assert.True(t, len(tags.List) > 0) } + +func TestTagsSort(t *testing.T) { + repotags := []string{ + "0.1.0", + "0.4.0", + "3.0.0-beta.1", + "3.0.0-beta.3", + "3.0.0-beta.4", + "4", + "4.0.0", + "4.0.0-beta.1", + "4.1.0", + "4.1.1", + "4.10.0", + "4.11.0", + "4.12.0", + "4.13.0", + "4.14.0", + "4.19.0", + "4.2.0", + "4.20", + "4.20.0", + "4.20.1", + "4.21", + "4.21.0", + "4.3.0", + "4.3.1", + "4.4.0", + "4.6.1", + "4.7.0", + "4.8.0", + "4.8.1", + "4.9.0", + "edge", + "latest", + } + + testCases := []struct { + name string + sortTag registry.SortTag + expected []string + }{ + { + name: "sort default", + sortTag: registry.SortTagDefault, + expected: []string{ + "0.1.0", + "0.4.0", + "3.0.0-beta.1", + "3.0.0-beta.3", + "3.0.0-beta.4", + "4", + "4.0.0", + "4.0.0-beta.1", + "4.1.0", + "4.1.1", + "4.10.0", + "4.11.0", + "4.12.0", + "4.13.0", + "4.14.0", + "4.19.0", + "4.2.0", + "4.20", + "4.20.0", + "4.20.1", + "4.21", + "4.21.0", + "4.3.0", + "4.3.1", + "4.4.0", + "4.6.1", + "4.7.0", + "4.8.0", + "4.8.1", + "4.9.0", + "edge", + "latest", + }, + }, + { + name: "sort lexicographical", + sortTag: registry.SortTagLexicographical, + expected: []string{ + "0.1.0", + "0.4.0", + "3.0.0-beta.1", + "3.0.0-beta.3", + "3.0.0-beta.4", + "4", + "4.0.0", + "4.0.0-beta.1", + "4.1.0", + "4.1.1", + "4.10.0", + "4.11.0", + "4.12.0", + "4.13.0", + "4.14.0", + "4.19.0", + "4.2.0", + "4.20", + "4.20.0", + "4.20.1", + "4.21", + "4.21.0", + "4.3.0", + "4.3.1", + "4.4.0", + "4.6.1", + "4.7.0", + "4.8.0", + "4.8.1", + "4.9.0", + "edge", + "latest", + }, + }, + { + name: "sort reverse", + sortTag: registry.SortTagReverse, + expected: []string{ + "latest", + "edge", + "4.9.0", + "4.8.1", + "4.8.0", + "4.7.0", + "4.6.1", + "4.4.0", + "4.3.1", + "4.3.0", + "4.21.0", + "4.21", + "4.20.1", + "4.20.0", + "4.20", + "4.2.0", + "4.19.0", + "4.14.0", + "4.13.0", + "4.12.0", + "4.11.0", + "4.10.0", + "4.1.1", + "4.1.0", + "4.0.0-beta.1", + "4.0.0", + "4", + "3.0.0-beta.4", + "3.0.0-beta.3", + "3.0.0-beta.1", + "0.4.0", + "0.1.0", + }, + }, + { + name: "sort semver", + sortTag: registry.SortTagSemver, + expected: []string{ + "4.21.0", + "4.21", + "4.20.1", + "4.20.0", + "4.20", + "4.19.0", + "4.14.0", + "4.13.0", + "4.12.0", + "4.11.0", + "4.10.0", + "4.9.0", + "4.8.1", + "4.8.0", + "4.7.0", + "4.6.1", + "4.4.0", + "4.3.1", + "4.3.0", + "4.2.0", + "4.1.1", + "4.1.0", + "4.0.0", + "4", + "4.0.0-beta.1", + "3.0.0-beta.4", + "3.0.0-beta.3", + "3.0.0-beta.1", + "0.4.0", + "0.1.0", + "edge", + "latest", + }, + }, + } + + for _, tt := range testCases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + tags := registry.SortTags(repotags, tt.sortTag) + assert.Equal(t, tt.expected, tags) + }) + } +}