From fc13b8c22c1b1dccfa4a2d764ea3cd1dd9ab60ab Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Mon, 30 Aug 2021 14:15:59 +0200 Subject: [PATCH] Allow to choose status to be notified (#475) Co-authored-by: CrazyMax --- .github/workflows/build.yml | 2 +- codecov.yml | 3 ++ docs/providers/docker.md | 5 +-- docs/providers/dockerfile.md | 5 +-- docs/providers/file.md | 5 ++- docs/providers/kubernetes.md | 5 +-- docs/providers/swarm.md | 5 +-- hack/test.Dockerfile | 3 +- internal/app/job.go | 8 ++++- internal/model/image.go | 35 ++++++++++++++++-- internal/provider/common.go | 17 +++++++-- internal/provider/file/file_test.go | 36 +++++++++++++------ internal/provider/file/fixtures/bintray.yml | 2 ++ internal/provider/file/image.go | 40 ++++++++++++++++++++- 14 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 codecov.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55897785..d41e43a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -132,7 +132,7 @@ jobs: path: ./dist/* if-no-files-found: error - - name: Build + name: Build image uses: docker/bake-action@v1 with: files: | diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..dc46984e --- /dev/null +++ b/codecov.yml @@ -0,0 +1,3 @@ +comment: false +github_checks: + annotations: false diff --git a/docs/providers/docker.md b/docs/providers/docker.md index a917db41..d5e0323c 100644 --- a/docs/providers/docker.md +++ b/docs/providers/docker.md @@ -177,7 +177,8 @@ You can configure more finely the way to analyze the image of your container thr | `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` | | Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | -| `diun.exclude_tags` | | Semi-colon 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.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 7804229f..a6eb0aa4 100644 --- a/docs/providers/dockerfile.md +++ b/docs/providers/dockerfile.md @@ -110,7 +110,8 @@ The following annotations can be added as comments before the target instruction |-------------------------------|---------------|---------------| | `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` | | Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | -| `diun.exclude_tags` | | Semi-colon 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.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 16689e63..5e122b43 100644 --- a/docs/providers/file.md +++ b/docs/providers/file.md @@ -52,9 +52,11 @@ providers: # Watch crazymax/swarm-cronjob image and assume docker.io registry and latest tag # with registry options named 'docker.io/crazymax' (image selector). -# Only include tags matching regexp ^1\.2\..* +# Only include tags matching regexp ^1\.2\..* and only be notified on new tag. - name: crazymax/swarm-cronjob watch_repo: true + notify_on: + - new include_tags: - ^1\.2\..* @@ -175,6 +177,7 @@ The configuration file(s) defines a slice of images to analyze with the followin | `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` | diff --git a/docs/providers/kubernetes.md b/docs/providers/kubernetes.md index dd3fcc76..7d89d23c 100644 --- a/docs/providers/kubernetes.md +++ b/docs/providers/kubernetes.md @@ -286,7 +286,8 @@ You can configure more finely the way to analyze the image of your pods through | `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` | | Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | -| `diun.exclude_tags` | | Semi-colon 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.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 9d707451..4e767f1c 100644 --- a/docs/providers/swarm.md +++ b/docs/providers/swarm.md @@ -180,7 +180,8 @@ You can configure more finely the way to analyze the image of your service throu | `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` | | Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | -| `diun.exclude_tags` | | Semi-colon 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.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/hack/test.Dockerfile b/hack/test.Dockerfile index 9db38b15..49b42983 100644 --- a/hack/test.Dockerfile +++ b/hack/test.Dockerfile @@ -14,7 +14,8 @@ FROM gomod AS test RUN --mount=type=bind,target=. \ --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ - go test -v -coverprofile=/tmp/coverage.txt -covermode=atomic -race ./... + go test -v -coverprofile=/tmp/coverage.txt -covermode=atomic -race ./... && \ + go tool cover -func=/tmp/coverage.txt FROM scratch AS test-coverage COPY --from=test /tmp/coverage.txt /coverage.txt diff --git a/internal/app/job.go b/internal/app/job.go index 59aa3dab..fc0b81f3 100644 --- a/internal/app/job.go +++ b/internal/app/job.go @@ -102,7 +102,7 @@ func (di *Diun) createJob(job model.Job) { InsecureTLS: *reg.InsecureTLS, UserAgent: di.meta.UserAgent, CompareDigest: *di.cfg.Watch.CompareDigest, - ImageOs: job.Image.Platform.Os, + ImageOs: job.Image.Platform.OS, ImageArch: job.Image.Platform.Arch, ImageVariant: job.Image.Platform.Variant, }) @@ -222,6 +222,12 @@ func (di *Diun) runJob(job model.Job) (entry model.NotifEntry) { return } + notifyOn := model.NotifyOn(entry.Status) + if !notifyOn.OneOf(job.Image.NotifyOn) { + sublog.Debug().Msgf("Skipping notification (%s not part of specified notify status)", entry.Status) + return + } + di.notif.Send(entry) return } diff --git a/internal/model/image.go b/internal/model/image.go index c410d506..95a66b8f 100644 --- a/internal/model/image.go +++ b/internal/model/image.go @@ -6,6 +6,7 @@ type Image struct { 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"` @@ -14,12 +15,12 @@ type Image struct { // ImagePlatform holds image platform configuration type ImagePlatform struct { - Os string `yaml:"os,omitempty" json:",omitempty"` + OS string `yaml:"os,omitempty" json:",omitempty"` Arch string `yaml:"arch,omitempty" json:",omitempty"` Variant string `yaml:"variant,omitempty" json:",omitempty"` } -// Image status constants +// ImageStatus constants const ( ImageStatusNew = ImageStatus("new") ImageStatusUpdate = ImageStatus("update") @@ -30,3 +31,33 @@ const ( // ImageStatus holds Docker image status analysis type ImageStatus string + +// NotifyOn constants +const ( + NotifyOnNew = NotifyOn(ImageStatusNew) + NotifyOnUpdate = NotifyOn(ImageStatusUpdate) +) + +// NotifyOn holds notify status type +type NotifyOn string + +// NotifyOnDefaults are the default notify status +var NotifyOnDefaults = []NotifyOn{ + NotifyOnNew, + NotifyOnUpdate, +} + +// Valid checks notify status is valid +func (ns *NotifyOn) Valid() bool { + return ns.OneOf(NotifyOnDefaults) +} + +// OneOf checks if notify status is one of the values in the list +func (ns *NotifyOn) OneOf(nsl []NotifyOn) bool { + for _, n := range nsl { + if n == *ns { + return true + } + } + return false +} diff --git a/internal/provider/common.go b/internal/provider/common.go index 4aa6ed71..ffeeef92 100644 --- a/internal/provider/common.go +++ b/internal/provider/common.go @@ -15,7 +15,8 @@ func ValidateImage(image string, labels map[string]string, watchByDef bool) (img image = image[:i] } img = model.Image{ - Name: image, + Name: image, + NotifyOn: model.NotifyOnDefaults, } if enableStr, ok := labels["diun.enable"]; ok { @@ -38,6 +39,18 @@ func ValidateImage(image string, labels map[string]string, watchByDef bool) (img if img.WatchRepo, err = strconv.ParseBool(value); err != nil { return img, fmt.Errorf("cannot parse %s value of label %s", value, key) } + case "diun.notify_on": + if len(value) == 0 { + break + } + img.NotifyOn = []model.NotifyOn{} + for _, no := range strings.Split(value, ";") { + notifyOn := model.NotifyOn(no) + if !notifyOn.Valid() { + return img, fmt.Errorf("unknown notify status %q", value) + } + img.NotifyOn = append(img.NotifyOn, notifyOn) + } 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) @@ -54,7 +67,7 @@ func ValidateImage(image string, labels map[string]string, watchByDef bool) (img return img, fmt.Errorf("cannot parse %s platform of label %s", value, key) } img.Platform = model.ImagePlatform{ - Os: platform.OS, + OS: platform.OS, Arch: platform.Architecture, Variant: platform.Variant, } diff --git a/internal/provider/file/file_test.go b/internal/provider/file/file_test.go index 1ef059eb..9109aee0 100644 --- a/internal/provider/file/file_test.go +++ b/internal/provider/file/file_test.go @@ -13,8 +13,9 @@ var ( { Provider: "file", Image: model.Image{ - Name: "jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0", - RegOpt: "bintrayoptions", + Name: "jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0", + RegOpt: "bintrayoptions", + NotifyOn: model.NotifyOnDefaults, }, }, { @@ -22,7 +23,10 @@ var ( Image: model.Image{ Name: "docker.bintray.io/jfrog/xray-server:2.8.6", WatchRepo: true, - MaxTags: 50, + NotifyOn: []model.NotifyOn{ + model.NotifyOnNew, + }, + MaxTags: 50, }, }, } @@ -30,8 +34,9 @@ var ( { Provider: "file", Image: model.Image{ - Name: "docker.io/crazymax/nextcloud:latest", - RegOpt: "myregistry", + Name: "docker.io/crazymax/nextcloud:latest", + RegOpt: "myregistry", + NotifyOn: model.NotifyOnDefaults, }, }, { @@ -39,6 +44,7 @@ var ( Image: model.Image{ Name: "crazymax/swarm-cronjob", WatchRepo: true, + NotifyOn: model.NotifyOnDefaults, IncludeTags: []string{ `^1\.2\..*`, }, @@ -49,6 +55,7 @@ var ( Image: model.Image{ Name: "docker.io/portainer/portainer", WatchRepo: true, + NotifyOn: model.NotifyOnDefaults, MaxTags: 10, IncludeTags: []string{ `^(0|[1-9]\d*)\..*`, @@ -60,14 +67,16 @@ var ( Image: model.Image{ Name: "traefik", WatchRepo: true, + NotifyOn: model.NotifyOnDefaults, }, }, { Provider: "file", Image: model.Image{ - Name: "alpine", + Name: "alpine", + NotifyOn: model.NotifyOnDefaults, Platform: model.ImagePlatform{ - Os: "linux", + OS: "linux", Arch: "arm64", Variant: "v8", }, @@ -76,13 +85,15 @@ var ( { Provider: "file", Image: model.Image{ - Name: "docker.io/graylog/graylog:3.2.0", + Name: "docker.io/graylog/graylog:3.2.0", + NotifyOn: model.NotifyOnDefaults, }, }, { Provider: "file", Image: model.Image{ - Name: "jacobalberty/unifi:5.9", + Name: "jacobalberty/unifi:5.9", + NotifyOn: model.NotifyOnDefaults, }, }, { @@ -90,6 +101,7 @@ var ( Image: model.Image{ Name: "crazymax/ddns-route53", WatchRepo: true, + NotifyOn: model.NotifyOnDefaults, IncludeTags: []string{ `^1\..*`, }, @@ -100,13 +112,15 @@ var ( { Provider: "file", Image: model.Image{ - Name: "quay.io/coreos/hyperkube", + Name: "quay.io/coreos/hyperkube", + NotifyOn: model.NotifyOnDefaults, }, }, { Provider: "file", 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, }, }, } diff --git a/internal/provider/file/fixtures/bintray.yml b/internal/provider/file/fixtures/bintray.yml index 3f9efd1a..6b9cdd76 100644 --- a/internal/provider/file/fixtures/bintray.yml +++ b/internal/provider/file/fixtures/bintray.yml @@ -2,4 +2,6 @@ regopt: bintrayoptions - name: docker.bintray.io/jfrog/xray-server:2.8.6 watch_repo: true + notify_on: + - new max_tags: 50 diff --git a/internal/provider/file/image.go b/internal/provider/file/image.go index 5368b449..ff83c6d4 100644 --- a/internal/provider/file/image.go +++ b/internal/provider/file/image.go @@ -5,7 +5,9 @@ import ( "path/filepath" "strings" + "github.com/containerd/containerd/platforms" "github.com/crazy-max/diun/v4/internal/model" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "gopkg.in/yaml.v2" ) @@ -28,7 +30,43 @@ func (c *Client) listFileImage() []model.Image { c.logger.Error().Err(err).Msgf("Unable to decode into struct %s", file) continue } - images = append(images, items...) + for _, item := range items { + + // Check NotifyOn + if len(item.NotifyOn) == 0 { + item.NotifyOn = model.NotifyOnDefaults + } else { + for _, no := range item.NotifyOn { + if !no.Valid() { + c.logger.Error(). + Str("file", file). + Str("img_name", item.Name). + Msgf("unknown notify status %q", no) + } + } + } + + // Check Platform + if item.Platform != (model.ImagePlatform{}) { + _, err = platforms.Parse(platforms.Format(ocispecs.Platform{ + OS: item.Platform.OS, + Architecture: item.Platform.Arch, + Variant: item.Platform.Variant, + })) + if err != nil { + c.logger.Error(). + Str("file", file). + Str("img_name", item.Name). + Msgf("cannot parse %s platform", platforms.Format(ocispecs.Platform{ + OS: item.Platform.OS, + Architecture: item.Platform.Arch, + Variant: item.Platform.Variant, + })) + } + } + + images = append(images, item) + } } return images