Tags sorting support (#645)

Co-authored-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2022-07-17 13:47:37 +02:00
committed by GitHub
parent 8fba9b158e
commit 857e462090
21 changed files with 524 additions and 95 deletions

View File

@@ -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: Templating is supported with the following fields:
| Key | Description | | Key | Description |
|----------------------------------|-------------| |---------------------------------|---------------------------------------------------------------------------------------|
| `.Meta.ID` | App ID: `diun` | | `.Meta.ID` | App ID: `diun` |
| `.Meta.Name` | App Name: `Diun` | | `.Meta.Name` | App Name: `Diun` |
| `.Meta.Desc` | App description: `Docker image update notifier` | | `.Meta.Desc` | App description: `Docker image update notifier` |
| `.Meta.URL` | App repo URL: `https://github.com/crazy-max/diun` | | `.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.Logo` | App logo URL: `https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png` |
| `.Meta.Author` | App author: `CrazyMax` | | `.Meta.Author` | App author: `CrazyMax` |
| `.Meta.Version` | App version: `v4.19.0` | | `.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.UserAgent` | App user-agent used to talk with registries: `diun/4.19.0 go/1.16 Linux` |
| `.Meta.Hostname` | Hostname | | `.Meta.Hostname` | Hostname |
| `.Entry.Status` | Entry status. Can be `new`, `update`, `unchange`, `skip` or `error` | | `.Entry.Status` | Entry status. Can be `new`, `update`, `unchange`, `skip` or `error` |
| `.Entry.Provider` | [Provider](config/providers.md) used | | `.Entry.Provider` | [Provider](config/providers.md) used |
| `.Entry.Image` | Docker image name. e.g. `docker.io/crazymax/diun:latest` | | `.Entry.Image` | Docker image name. e.g. `docker.io/crazymax/diun:latest` |
| `.Entry.Image.Domain` | Docker image domain. e.g. `docker.io` | | `.Entry.Image.Domain` | Docker image domain. e.g. `docker.io` |
| `.Entry.Image.Path` | Docker image path. e.g. `crazymax/diun` | | `.Entry.Image.Path` | Docker image path. e.g. `crazymax/diun` |
| `.Entry.Image.Tag` | Docker image tag. e.g. `latest` | | `.Entry.Image.Tag` | Docker image tag. e.g. `latest` |
| `.Entry.Image.Digest` | Docker image digest | | `.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.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.Name` | Manifest name. e.g. `docker.io/crazymax/diun` |
| `.Entry.Manifest.Tag` | Manifest tag. e.g. `latest` | | `.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.MIMEType` | Manifest MIME type. e.g. `application/vnd.docker.distribution.manifest.list.v2+json` |
| `.Entry.Manifest.Digest` | Manifest digest | | `.Entry.Manifest.Digest` | Manifest digest |
| `.Entry.Manifest.Created` | Manifest created date. e.g. `2021-06-20T12:23:56Z` | | `.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.DockerVersion` | Version of Docker that was used to build the image. e.g. `20.10.7` |
| `.Entry.Manifest.Labels` | Image labels | | `.Entry.Manifest.Labels` | Image labels |
| `.Entry.Manifest.Layers` | Image layers | | `.Entry.Manifest.Layers` | Image layers |
| `.Entry.Manifest.Platform` | Platform that the image is runs on. e.g. `linux/amd64` | | `.Entry.Manifest.Platform` | Platform that the image is runs on. e.g. `linux/amd64` |
## Authentication against the registry ## Authentication against the registry
@@ -205,6 +205,97 @@ Or you can tweak the [`schedule` setting](config/watch.md#schedule) with somethi
!!! warning !!! 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.
## 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 ## Profiling
Diun provides a simple way to manage runtime/pprof profiling through the Diun provides a simple way to manage runtime/pprof profiling through the

View File

@@ -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: You can configure more finely the way to analyze the image of your container through Docker labels:
| Name | Default | Description | | Name | Default | Description |
|-------------------------------|---------------|---------------| |---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `diun.enable` | | Set to true to enable image analysis of this container | | `diun.enable` | | Set to true to enable image analysis of this container |
| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | | `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.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.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.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.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | | `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them |
| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | | `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` |
| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | | `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`) |

View File

@@ -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: The following annotations can be added as comments before the target instruction to customize the image analysis:
| Name | Default | Description | | Name | Default | Description |
|-------------------------------|---------------|---------------| |---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | | `diun.regopt` | | [Registry options](../config/regopts.md) name to use |
| `diun.watch_repo` | `false` | Watch all tags of this image | | `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.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.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.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | | `diun.max_tags` | `0` | Maximum number of tags to watch if `watch_repo` enabled. `0` means all of them |
| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | | `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` |
| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | | `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`) |

View File

@@ -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: The configuration file(s) defines a slice of images to analyze with the following fields:
| Name | Default | Description | | 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 | | `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 | | `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) | | `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`. | | `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 | | `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` |
| `include_tags` | | List of regular expressions to include tags. Can be useful if you enable `watch_repo` | | `max_tags` | `0` | Maximum number of tags to watch if `watch_repo` enabled. `0` means all of them |
| `exclude_tags` | | List of regular expressions to exclude tags. Can be useful if you enable `watch_repo` | | `include_tags` | | List of regular expressions to include tags. Can be useful if you enable `watch_repo` |
| `platform.os` | _automatic_ | Operating system to use as custom platform | | `exclude_tags` | | List of regular expressions to exclude tags. Can be useful if you enable `watch_repo` |
| `platform.arch` | _automatic_ | CPU architecture to use as custom platform | | `platform.os` | _automatic_ | Operating system to use as custom platform |
| `platform.variant` | _automatic_ | Variant of the CPU 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 |

View File

@@ -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: You can configure more finely the way to analyze the image of your pods through Kubernetes annotations:
| Name | Default | Description | | Name | Default | Description |
|-------------------------------|---------------|---------------| |---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `diun.enable` | | Set to true to enable image analysis of this pod | | `diun.enable` | | Set to true to enable image analysis of this pod |
| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | | `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.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.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.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.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | | `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them |
| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | | `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` |
| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | | `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`) |

View File

@@ -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: You can configure more finely the way to analyze the image of your service through Docker labels:
| Name | Default | Description | | Name | Default | Description |
|-------------------------------|---------------|---------------| |---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `diun.enable` | | Set to true to enable image analysis of this service | | `diun.enable` | | Set to true to enable image analysis of this service |
| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | | `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.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.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.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.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | | `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them |
| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | | `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` |
| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | | `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`) |

1
go.mod
View File

@@ -39,6 +39,7 @@ require (
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0
github.com/tidwall/pretty v1.2.0 github.com/tidwall/pretty v1.2.0
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
golang.org/x/mod v0.5.1
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150
google.golang.org/grpc v1.48.0 google.golang.org/grpc v1.48.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0

2
go.sum
View File

@@ -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.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/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.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-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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@@ -124,6 +124,7 @@ func (di *Diun) createJob(job model.Job) {
tags, err := job.Registry.Tags(registry.TagsOptions{ tags, err := job.Registry.Tags(registry.TagsOptions{
Image: job.RegImage, Image: job.RegImage,
Max: job.Image.MaxTags, Max: job.Image.MaxTags,
Sort: job.Image.SortTags,
Include: job.Image.IncludeTags, Include: job.Image.IncludeTags,
Exclude: job.Image.ExcludeTags, Exclude: job.Image.ExcludeTags,
}) })

View File

@@ -360,6 +360,7 @@ func TestLoadEnv(t *testing.T) {
} }
for _, tt := range testCases { for _, tt := range testCases {
tt := tt
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
UnsetEnv("DIUN_") UnsetEnv("DIUN_")
@@ -484,6 +485,7 @@ for <code>{{ .Entry.Manifest.Platform }}</code> platform.
} }
for _, tt := range testCases { for _, tt := range testCases {
tt := tt
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
UnsetEnv("DIUN_") UnsetEnv("DIUN_")

View File

@@ -1,16 +1,19 @@
package model package model
import "github.com/crazy-max/diun/v4/pkg/registry"
// Image holds image configuration // Image holds image configuration
type Image struct { type Image struct {
Name string `yaml:"name,omitempty" json:",omitempty"` Name string `yaml:"name,omitempty" json:",omitempty"`
Platform ImagePlatform `yaml:"platform,omitempty" json:",omitempty"` Platform ImagePlatform `yaml:"platform,omitempty" json:",omitempty"`
RegOpt string `yaml:"regopt,omitempty" json:",omitempty"` RegOpt string `yaml:"regopt,omitempty" json:",omitempty"`
WatchRepo bool `yaml:"watch_repo,omitempty" json:",omitempty"` WatchRepo bool `yaml:"watch_repo,omitempty" json:",omitempty"`
NotifyOn []NotifyOn `yaml:"notify_on,omitempty" json:",omitempty"` NotifyOn []NotifyOn `yaml:"notify_on,omitempty" json:",omitempty"`
MaxTags int `yaml:"max_tags,omitempty" json:",omitempty"` MaxTags int `yaml:"max_tags,omitempty" json:",omitempty"`
IncludeTags []string `yaml:"include_tags,omitempty" json:",omitempty"` SortTags registry.SortTag `yaml:"sort_tags,omitempty" json:",omitempty"`
ExcludeTags []string `yaml:"exclude_tags,omitempty" json:",omitempty"` IncludeTags []string `yaml:"include_tags,omitempty" json:",omitempty"`
HubTpl string `yaml:"hub_tpl,omitempty" json:",omitempty"` ExcludeTags []string `yaml:"exclude_tags,omitempty" json:",omitempty"`
HubTpl string `yaml:"hub_tpl,omitempty" json:",omitempty"`
} }
// ImagePlatform holds image platform configuration // ImagePlatform holds image platform configuration

View File

@@ -7,6 +7,7 @@ import (
"github.com/containerd/containerd/platforms" "github.com/containerd/containerd/platforms"
"github.com/crazy-max/diun/v4/internal/model" "github.com/crazy-max/diun/v4/internal/model"
"github.com/crazy-max/diun/v4/pkg/registry"
) )
// ValidateImage returns a standard image through Docker labels // 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{ img = model.Image{
Name: image, Name: image,
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagReverse,
} }
if enableStr, ok := labels["diun.enable"]; ok { 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) 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": case "diun.max_tags":
if img.MaxTags, err = strconv.Atoi(value); err != nil { if img.MaxTags, err = strconv.Atoi(value); err != nil {
return img, fmt.Errorf("cannot parse %s value of label %s", value, key) return img, fmt.Errorf("cannot parse %s value of label %s", value, key)

View File

@@ -5,6 +5,7 @@ import (
"github.com/crazy-max/diun/v4/internal/model" "github.com/crazy-max/diun/v4/internal/model"
"github.com/crazy-max/diun/v4/internal/provider/file" "github.com/crazy-max/diun/v4/internal/provider/file"
"github.com/crazy-max/diun/v4/pkg/registry"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -16,6 +17,7 @@ var (
Name: "jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0", Name: "jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0",
RegOpt: "bintrayoptions", RegOpt: "bintrayoptions",
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagReverse,
}, },
}, },
{ {
@@ -26,7 +28,8 @@ var (
NotifyOn: []model.NotifyOn{ NotifyOn: []model.NotifyOn{
model.NotifyOnNew, model.NotifyOnNew,
}, },
MaxTags: 50, SortTags: registry.SortTagLexicographical,
MaxTags: 50,
}, },
}, },
} }
@@ -37,6 +40,7 @@ var (
Name: "docker.io/crazymax/nextcloud:latest", Name: "docker.io/crazymax/nextcloud:latest",
RegOpt: "myregistry", RegOpt: "myregistry",
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagReverse,
}, },
}, },
{ {
@@ -45,6 +49,7 @@ var (
Name: "crazymax/swarm-cronjob", Name: "crazymax/swarm-cronjob",
WatchRepo: true, WatchRepo: true,
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagSemver,
IncludeTags: []string{ IncludeTags: []string{
`^1\.2\..*`, `^1\.2\..*`,
}, },
@@ -57,6 +62,7 @@ var (
WatchRepo: true, WatchRepo: true,
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
MaxTags: 10, MaxTags: 10,
SortTags: registry.SortTagReverse,
IncludeTags: []string{ IncludeTags: []string{
`^(0|[1-9]\d*)\..*`, `^(0|[1-9]\d*)\..*`,
}, },
@@ -68,6 +74,7 @@ var (
Name: "traefik", Name: "traefik",
WatchRepo: true, WatchRepo: true,
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagDefault,
}, },
}, },
{ {
@@ -75,6 +82,7 @@ var (
Image: model.Image{ Image: model.Image{
Name: "alpine", Name: "alpine",
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagReverse,
Platform: model.ImagePlatform{ Platform: model.ImagePlatform{
OS: "linux", OS: "linux",
Arch: "arm64", Arch: "arm64",
@@ -87,6 +95,7 @@ var (
Image: model.Image{ Image: model.Image{
Name: "docker.io/graylog/graylog:3.2.0", Name: "docker.io/graylog/graylog:3.2.0",
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagReverse,
}, },
}, },
{ {
@@ -94,6 +103,7 @@ var (
Image: model.Image{ Image: model.Image{
Name: "jacobalberty/unifi:5.9", Name: "jacobalberty/unifi:5.9",
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagReverse,
}, },
}, },
{ {
@@ -102,6 +112,7 @@ var (
Name: "crazymax/ddns-route53", Name: "crazymax/ddns-route53",
WatchRepo: true, WatchRepo: true,
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagReverse,
IncludeTags: []string{ IncludeTags: []string{
`^1\..*`, `^1\..*`,
}, },
@@ -114,6 +125,7 @@ var (
Image: model.Image{ Image: model.Image{
Name: "quay.io/coreos/hyperkube", Name: "quay.io/coreos/hyperkube",
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagReverse,
}, },
}, },
{ {
@@ -121,6 +133,7 @@ var (
Image: model.Image{ Image: model.Image{
Name: "quay.io/coreos/hyperkube:v1.1.7-coreos.1", Name: "quay.io/coreos/hyperkube:v1.1.7-coreos.1",
NotifyOn: model.NotifyOnDefaults, NotifyOn: model.NotifyOnDefaults,
SortTags: registry.SortTagReverse,
}, },
}, },
} }

View File

@@ -4,4 +4,5 @@
watch_repo: true watch_repo: true
notify_on: notify_on:
- new - new
sort_tags: lexicographical
max_tags: 50 max_tags: 50

View File

@@ -2,15 +2,18 @@
regopt: myregistry regopt: myregistry
- name: crazymax/swarm-cronjob - name: crazymax/swarm-cronjob
watch_repo: true watch_repo: true
sort_tags: semver
include_tags: include_tags:
- ^1\.2\..* - ^1\.2\..*
- name: docker.io/portainer/portainer - name: docker.io/portainer/portainer
watch_repo: true watch_repo: true
max_tags: 10 max_tags: 10
sort_tags: reverse
include_tags: include_tags:
- ^(0|[1-9]\d*)\..* - ^(0|[1-9]\d*)\..*
- name: traefik - name: traefik
watch_repo: true watch_repo: true
sort_tags: default
- name: alpine - name: alpine
platform: platform:
os: linux os: linux

View File

@@ -7,6 +7,7 @@ import (
"github.com/containerd/containerd/platforms" "github.com/containerd/containerd/platforms"
"github.com/crazy-max/diun/v4/internal/model" "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" ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"gopkg.in/yaml.v2" "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 // Check Platform
if item.Platform != (model.ImagePlatform{}) { if item.Platform != (model.ImagePlatform{}) {
_, err = platforms.Parse(platforms.Format(ocispecs.Platform{ _, err = platforms.Parse(platforms.Format(ocispecs.Platform{

View File

@@ -126,6 +126,7 @@ func TestParseImage(t *testing.T) {
} }
for _, tt := range testCases { for _, tt := range testCases {
tt := tt
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
img, err := registry.ParseImage(tt.parseOpts) img, err := registry.ParseImage(tt.parseOpts)
if err != nil { if err != nil {
@@ -239,6 +240,7 @@ func TestHubLink(t *testing.T) {
} }
for _, tt := range testCases { for _, tt := range testCases {
tt := tt
t.Run(tt.desc, func(t *testing.T) { t.Run(tt.desc, func(t *testing.T) {
img, err := registry.ParseImage(tt.parseOpts) img, err := registry.ParseImage(tt.parseOpts)
if err != nil { if err != nil {

View File

@@ -52,6 +52,7 @@ func TestParseReference(t *testing.T) {
} }
for _, tt := range testCases { for _, tt := range testCases {
tt := tt
t.Run(tt.input, func(t *testing.T) { t.Run(tt.input, func(t *testing.T) {
ref, err := registry.ParseReference(tt.input) ref, err := registry.ParseReference(tt.input)
if tt.wantErr { if tt.wantErr {

View File

@@ -18,6 +18,7 @@ type Tags struct {
type TagsOptions struct { type TagsOptions struct {
Image Image Image Image
Max int Max int
Sort SortTag
Include []string Include []string
Exclude []string Exclude []string
} }
@@ -43,6 +44,9 @@ func (c *Client) Tags(opts TagsOptions) (*Tags, error) {
Total: len(tags), Total: len(tags),
} }
// Sort tags
tags = SortTags(tags, opts.Sort)
// Filter // Filter
for _, tag := range tags { for _, tag := range tags {
if !utl.IsIncluded(tag, opts.Include) { if !utl.IsIncluded(tag, opts.Include) {
@@ -55,12 +59,6 @@ func (c *Client) Tags(opts TagsOptions) (*Tags, error) {
res.List = append(res.List, tag) 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 { if opts.Max > 0 && len(res.List) >= opts.Max {
res.List = res.List[:opts.Max] res.List = res.List[:opts.Max]
} }

79
pkg/registry/tags_sort.go Normal file
View File

@@ -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
}

View File

@@ -27,3 +27,207 @@ func TestTags(t *testing.T) {
assert.True(t, tags.Total > 0) assert.True(t, tags.Total > 0)
assert.True(t, len(tags.List) > 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)
})
}
}