Repository: crazy-max/diun
Pull request #981 (2023-09-23 10:58:12)

fix defaults not handled by yaml configuration
related to https://github.com/crazy-max/diun/pull/887

Current implementation of defaults is not working because the `model.Image` struct does not support proper yaml and json tags for the configuration.

Need a dedicated struct for this and also remove some fields that I don't think are necessary. Also move `imageDefaults` from watch to `defaults` in config root.


Patch:
diff --git a/internal/app/diun.go b/internal/app/diun.go
index 9c5d05d4..e6883003 100644
--- a/internal/app/diun.go
+++ b/internal/app/diun.go
@@ -153,32 +153,32 @@ func (di *Diun) Run() {
 	defer di.pool.Release()
 
 	// Docker provider
-	for _, job := range dockerPrd.New(di.cfg.Providers.Docker, di.cfg.Watch.ImageDefaults).ListJob() {
+	for _, job := range dockerPrd.New(di.cfg.Providers.Docker, di.cfg.Defaults).ListJob() {
 		di.createJob(job)
 	}
 
 	// Swarm provider
-	for _, job := range swarmPrd.New(di.cfg.Providers.Swarm, di.cfg.Watch.ImageDefaults).ListJob() {
+	for _, job := range swarmPrd.New(di.cfg.Providers.Swarm, di.cfg.Defaults).ListJob() {
 		di.createJob(job)
 	}
 
 	// Kubernetes provider
-	for _, job := range kubernetesPrd.New(di.cfg.Providers.Kubernetes, di.cfg.Watch.ImageDefaults).ListJob() {
+	for _, job := range kubernetesPrd.New(di.cfg.Providers.Kubernetes, di.cfg.Defaults).ListJob() {
 		di.createJob(job)
 	}
 
 	// File provider
-	for _, job := range filePrd.New(di.cfg.Providers.File, di.cfg.Watch.ImageDefaults).ListJob() {
+	for _, job := range filePrd.New(di.cfg.Providers.File, di.cfg.Defaults).ListJob() {
 		di.createJob(job)
 	}
 
 	// Dockerfile provider
-	for _, job := range dockerfilePrd.New(di.cfg.Providers.Dockerfile, di.cfg.Watch.ImageDefaults).ListJob() {
+	for _, job := range dockerfilePrd.New(di.cfg.Providers.Dockerfile, di.cfg.Defaults).ListJob() {
 		di.createJob(job)
 	}
 
 	// Nomad provider
-	for _, job := range nomadPrd.New(di.cfg.Providers.Nomad, di.cfg.Watch.ImageDefaults).ListJob() {
+	for _, job := range nomadPrd.New(di.cfg.Providers.Nomad, di.cfg.Defaults).ListJob() {
 		di.createJob(job)
 	}
 
diff --git a/internal/app/job.go b/internal/app/job.go
index f186b17d..0b13efd0 100644
--- a/internal/app/job.go
+++ b/internal/app/job.go
@@ -118,7 +118,7 @@ func (di *Diun) createJob(job model.Job) {
 		sublog.Error().Err(err).Msgf("Invoking job")
 	}
 
-	if job.Image.WatchRepo == nil || !*job.Image.WatchRepo || len(job.RegImage.Domain) == 0 {
+	if !*job.Image.WatchRepo || len(job.RegImage.Domain) == 0 {
 		return
 	}
 
diff --git a/internal/config/config.go b/internal/config/config.go
index a39b0982..e8f5671d 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -16,6 +16,7 @@ import (
 type Config struct {
 	Db        *model.Db        `yaml:"db,omitempty" json:"db,omitempty"`
 	Watch     *model.Watch     `yaml:"watch,omitempty" json:"watch,omitempty"`
+	Defaults  *model.Defaults  `yaml:"defaults,omitempty" json:"defaults,omitempty"`
 	Notif     *model.Notif     `yaml:"notif,omitempty" json:"notif,omitempty"`
 	RegOpts   model.RegOpts    `yaml:"regopts,omitempty" json:"regopts,omitempty" validate:"unique=Name,dive"`
 	Providers *model.Providers `yaml:"providers,omitempty" json:"providers,omitempty"`
@@ -24,8 +25,9 @@ type Config struct {
 // Load returns Config struct
 func Load(config string) (*Config, error) {
 	cfg := Config{
-		Db:    (&model.Db{}).GetDefaults(),
-		Watch: (&model.Watch{}).GetDefaults(),
+		Db:       (&model.Db{}).GetDefaults(),
+		Watch:    (&model.Watch{}).GetDefaults(),
+		Defaults: (&model.Defaults{}).GetDefaults(),
 	}
 
 	fileLoader := gonfig.NewFileLoader(gonfig.FileLoaderConfig{
diff --git a/internal/model/defaults.go b/internal/model/defaults.go
new file mode 100644
index 00000000..88a78a90
--- /dev/null
+++ b/internal/model/defaults.go
@@ -0,0 +1,31 @@
+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"`
+	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"`
+	IncludeTags []string          `yaml:"includeTags,omitempty" json:"includeTags,omitempty"`
+	ExcludeTags []string          `yaml:"excludeTags,omitempty" json:"excludeTags,omitempty"`
+	Metadata    map[string]string `yaml:"metadata,omitempty" json:"metadata,omitempty"`
+}
+
+// GetDefaults gets the default values
+func (s *Defaults) GetDefaults() *Defaults {
+	n := &Defaults{}
+	n.SetDefaults()
+	return n
+}
+
+// SetDefaults sets the default values
+func (s *Defaults) SetDefaults() {
+	s.WatchRepo = utl.NewFalse()
+	s.NotifyOn = NotifyOnDefaults
+	s.SortTags = registry.SortTagReverse
+}
diff --git a/internal/model/watch.go b/internal/model/watch.go
index 13f192be..f9239519 100644
--- a/internal/model/watch.go
+++ b/internal/model/watch.go
@@ -3,7 +3,6 @@ package model
 import (
 	"time"
 
-	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/crazy-max/diun/v4/pkg/utl"
 )
 
@@ -16,7 +15,6 @@ type Watch struct {
 	RunOnStartup    *bool          `yaml:"runOnStartup,omitempty" json:"runOnStartup,omitempty" validate:"required"`
 	CompareDigest   *bool          `yaml:"compareDigest,omitempty" json:"compareDigest,omitempty" validate:"required"`
 	Healthchecks    *Healthchecks  `yaml:"healthchecks,omitempty" json:"healthchecks,omitempty"`
-	ImageDefaults   *Image         `yaml:"defaults,omitempty" json:"defaults,omitempty"`
 }
 
 // GetDefaults gets the default values
@@ -33,8 +31,4 @@ func (s *Watch) SetDefaults() {
 	s.FirstCheckNotif = utl.NewFalse()
 	s.RunOnStartup = utl.NewTrue()
 	s.CompareDigest = utl.NewTrue()
-	s.ImageDefaults = &Image{
-		NotifyOn: NotifyOnDefaults,
-		SortTags: registry.SortTagReverse,
-	}
 }
diff --git a/internal/provider/common.go b/internal/provider/common.go
index 27830b4d..f2f98802 100644
--- a/internal/provider/common.go
+++ b/internal/provider/common.go
@@ -18,19 +18,25 @@ var (
 )
 
 // ValidateImage returns a standard image through Docker labels
-func ValidateImage(image string, metadata, labels map[string]string, watchByDef bool, imageDefaults model.Image) (img model.Image, err error) {
+func ValidateImage(image string, metadata, labels map[string]string, watchByDef bool, defaults *model.Defaults) (img model.Image, err error) {
 	img = model.Image{
 		Name: image,
 	}
 
-	if err := mergo.Merge(&img, imageDefaults); err != nil {
-		return img, &invalidLabelError{errors.Wrapf(err, "failed to merge image defaults for image %s", image)}
+	if defaults != nil {
+		img.WatchRepo = defaults.WatchRepo
+		img.NotifyOn = defaults.NotifyOn
+		img.MaxTags = defaults.MaxTags
+		img.SortTags = defaults.SortTags
+		img.IncludeTags = defaults.IncludeTags
+		img.ExcludeTags = defaults.ExcludeTags
+		img.Metadata = defaults.Metadata
 	}
 
 	if enableStr, ok := labels["diun.enable"]; ok {
 		enable, err := strconv.ParseBool(enableStr)
 		if err != nil {
-			return img, &invalidLabelError{errors.Wrapf(err, "cannot parse %q value of label diun.enable", enableStr)}
+			return img, errors.Wrapf(err, "cannot parse %q value of label diun.enable", enableStr)
 		}
 		if !enable {
 			return model.Image{}, nil
@@ -47,7 +53,7 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 			if watchRepo, err := strconv.ParseBool(value); err == nil {
 				img.WatchRepo = &watchRepo
 			} else {
-				return img, &invalidLabelError{errors.Wrapf(err, "cannot parse %q value of label %s", value, key)}
+				return img, errors.Wrapf(err, "cannot parse %q value of label %s", value, key)
 			}
 		case key == "diun.notify_on":
 			if len(value) == 0 {
@@ -57,7 +63,7 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 			for _, no := range strings.Split(value, ";") {
 				notifyOn := model.NotifyOn(no)
 				if !notifyOn.Valid() {
-					return img, &invalidLabelError{errors.Errorf("unknown notify status %q", value)}
+					return img, errors.Errorf("unknown notify status %q", value)
 				}
 				img.NotifyOn = append(img.NotifyOn, notifyOn)
 			}
@@ -67,12 +73,12 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 			}
 			sortTags := registry.SortTag(value)
 			if !sortTags.Valid() {
-				return img, &invalidLabelError{errors.Errorf("unknown sort tags type %q", value)}
+				return img, errors.Errorf("unknown sort tags type %q", value)
 			}
 			img.SortTags = sortTags
 		case key == "diun.max_tags":
 			if img.MaxTags, err = strconv.Atoi(value); err != nil {
-				return img, &invalidLabelError{errors.Wrapf(err, "cannot parse %q value of label %s", value, key)}
+				return img, errors.Wrapf(err, "cannot parse %q value of label %s", value, key)
 			}
 		case key == "diun.include_tags":
 			img.IncludeTags = strings.Split(value, ";")
@@ -85,7 +91,7 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 		case key == "diun.platform":
 			platform, err := platforms.Parse(value)
 			if err != nil {
-				return img, &invalidLabelError{errors.Wrapf(err, "cannot parse %q platform of label %s", value, key)}
+				return img, errors.Wrapf(err, "cannot parse %q platform of label %s", value, key)
 			}
 			img.Platform = model.ImagePlatform{
 				OS:      platform.OS,
@@ -98,7 +104,7 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 				break
 			}
 			if err := validateMetadataKey(mkey); err != nil {
-				return img, &invalidLabelError{errors.Wrapf(err, "invalid metadata key %q", mkey)}
+				return img, errors.Wrapf(err, "invalid metadata key %q", mkey)
 			}
 			if img.Metadata == nil {
 				img.Metadata = map[string]string{}
@@ -121,15 +127,3 @@ func validateMetadataKey(key string) error {
 	}
 	return nil
 }
-
-type invalidLabelError struct {
-	error
-}
-
-func (e *invalidLabelError) Error() string {
-	return e.Error()
-}
-
-func (e *invalidLabelError) Unwrap() error {
-	return e
-}
diff --git a/internal/provider/docker/container.go b/internal/provider/docker/container.go
index 8f264847..bf3b0375 100644
--- a/internal/provider/docker/container.go
+++ b/internal/provider/docker/container.go
@@ -91,7 +91,7 @@ func (c *Client) listContainerImage() []model.Image {
 			Str("ctn_image", imageName).
 			Interface("ctn_labels", ctn.Labels).
 			Msg("Validate image")
-		image, err := provider.ValidateImage(imageName, metadata(ctn), ctn.Labels, *c.config.WatchByDefault, *c.imageDefaults)
+		image, err := provider.ValidateImage(imageName, metadata(ctn), ctn.Labels, *c.config.WatchByDefault, c.defaults)
 
 		if err != nil {
 			c.logger.Error().Err(err).
diff --git a/internal/provider/docker/docker.go b/internal/provider/docker/docker.go
index 26a7521c..bc42ce33 100644
--- a/internal/provider/docker/docker.go
+++ b/internal/provider/docker/docker.go
@@ -10,18 +10,18 @@ import (
 // Client represents an active docker provider object
 type Client struct {
 	*provider.Client
-	config        *model.PrdDocker
-	logger        zerolog.Logger
-	imageDefaults *model.Image
+	config   *model.PrdDocker
+	logger   zerolog.Logger
+	defaults *model.Defaults
 }
 
 // New creates new docker provider instance
-func New(config *model.PrdDocker, imageDefaults *model.Image) *provider.Client {
+func New(config *model.PrdDocker, defaults *model.Defaults) *provider.Client {
 	return &provider.Client{
 		Handler: &Client{
-			config:        config,
-			logger:        log.With().Str("provider", "docker").Logger(),
-			imageDefaults: imageDefaults,
+			config:   config,
+			logger:   log.With().Str("provider", "docker").Logger(),
+			defaults: defaults,
 		},
 	}
 }
diff --git a/internal/provider/dockerfile/dockerfile.go b/internal/provider/dockerfile/dockerfile.go
index cf009b83..1f9e8d26 100644
--- a/internal/provider/dockerfile/dockerfile.go
+++ b/internal/provider/dockerfile/dockerfile.go
@@ -10,18 +10,18 @@ import (
 // Client represents an active dockerfile provider object
 type Client struct {
 	*provider.Client
-	config        *model.PrdDockerfile
-	logger        zerolog.Logger
-	imageDefaults *model.Image
+	config   *model.PrdDockerfile
+	logger   zerolog.Logger
+	defaults *model.Defaults
 }
 
 // New creates new dockerfile provider instance
-func New(config *model.PrdDockerfile, imageDefaults *model.Image) *provider.Client {
+func New(config *model.PrdDockerfile, defaults *model.Defaults) *provider.Client {
 	return &provider.Client{
 		Handler: &Client{
-			config:        config,
-			logger:        log.With().Str("provider", "dockerfile").Logger(),
-			imageDefaults: imageDefaults,
+			config:   config,
+			logger:   log.With().Str("provider", "dockerfile").Logger(),
+			defaults: defaults,
 		},
 	}
 }
diff --git a/internal/provider/dockerfile/image.go b/internal/provider/dockerfile/image.go
index c941fd10..b2d48fe8 100644
--- a/internal/provider/dockerfile/image.go
+++ b/internal/provider/dockerfile/image.go
@@ -32,7 +32,7 @@ func (c *Client) listExtImage() (list []model.Image) {
 				Interface("dfile_comments", fromImage.Comments).
 				Int("dfile_line", fromImage.Line).
 				Msg("Validate image")
-			image, err := provider.ValidateImage(fromImage.Name, nil, c.extractLabels(fromImage.Comments), true, *c.imageDefaults)
+			image, err := provider.ValidateImage(fromImage.Name, nil, c.extractLabels(fromImage.Comments), true, c.defaults)
 			if err != nil {
 				c.logger.Error().Err(err).
 					Str("dfile_image", fromImage.Name).
diff --git a/internal/provider/file/file.go b/internal/provider/file/file.go
index 0f8468b7..dd2f9336 100644
--- a/internal/provider/file/file.go
+++ b/internal/provider/file/file.go
@@ -10,18 +10,18 @@ import (
 // Client represents an active file provider object
 type Client struct {
 	*provider.Client
-	config        *model.PrdFile
-	logger        zerolog.Logger
-	imageDefaults *model.Image
+	config   *model.PrdFile
+	logger   zerolog.Logger
+	defaults *model.Defaults
 }
 
 // New creates new file provider instance
-func New(config *model.PrdFile, imageDefaults *model.Image) *provider.Client {
+func New(config *model.PrdFile, defaults *model.Defaults) *provider.Client {
 	return &provider.Client{
 		Handler: &Client{
-			config:        config,
-			logger:        log.With().Str("provider", "file").Logger(),
-			imageDefaults: imageDefaults,
+			config:   config,
+			logger:   log.With().Str("provider", "file").Logger(),
+			defaults: defaults,
 		},
 	}
 }
diff --git a/internal/provider/file/image.go b/internal/provider/file/image.go
index f26de19b..37e6feb8 100644
--- a/internal/provider/file/image.go
+++ b/internal/provider/file/image.go
@@ -34,11 +34,11 @@ func (c *Client) listFileImage() []model.Image {
 		for _, item := range items {
 			// Set default WatchRepo
 			if item.WatchRepo == nil {
-				item.WatchRepo = c.imageDefaults.WatchRepo
+				item.WatchRepo = c.defaults.WatchRepo
 			}
 			// Check NotifyOn
 			if len(item.NotifyOn) == 0 {
-				item.NotifyOn = c.imageDefaults.NotifyOn
+				item.NotifyOn = c.defaults.NotifyOn
 			} else {
 				for _, no := range item.NotifyOn {
 					if !no.Valid() {
@@ -52,7 +52,7 @@ func (c *Client) listFileImage() []model.Image {
 
 			// Check SortType
 			if item.SortTags == "" {
-				item.SortTags = c.imageDefaults.SortTags
+				item.SortTags = c.defaults.SortTags
 			}
 			if !item.SortTags.Valid() {
 				c.logger.Error().
diff --git a/internal/provider/kubernetes/kubernetes.go b/internal/provider/kubernetes/kubernetes.go
index b1c91e36..58a9eccc 100644
--- a/internal/provider/kubernetes/kubernetes.go
+++ b/internal/provider/kubernetes/kubernetes.go
@@ -10,18 +10,18 @@ import (
 // Client represents an active kubernetes provider object
 type Client struct {
 	*provider.Client
-	config        *model.PrdKubernetes
-	logger        zerolog.Logger
-	imageDefaults *model.Image
+	config   *model.PrdKubernetes
+	logger   zerolog.Logger
+	defaults *model.Defaults
 }
 
 // New creates new kubernetes provider instance
-func New(config *model.PrdKubernetes, imageDefaults *model.Image) *provider.Client {
+func New(config *model.PrdKubernetes, defaults *model.Defaults) *provider.Client {
 	return &provider.Client{
 		Handler: &Client{
-			config:        config,
-			logger:        log.With().Str("provider", "kubernetes").Logger(),
-			imageDefaults: imageDefaults,
+			config:   config,
+			logger:   log.With().Str("provider", "kubernetes").Logger(),
+			defaults: defaults,
 		},
 	}
 }
diff --git a/internal/provider/kubernetes/pod.go b/internal/provider/kubernetes/pod.go
index dd478d15..a870b746 100644
--- a/internal/provider/kubernetes/pod.go
+++ b/internal/provider/kubernetes/pod.go
@@ -41,7 +41,7 @@ func (c *Client) listPodImage() []model.Image {
 				Str("ctn_image", ctn.Image).
 				Msg("Validate image")
 
-			image, err := provider.ValidateImage(ctn.Image, metadata(pod, ctn), pod.Annotations, *c.config.WatchByDefault, *c.imageDefaults)
+			image, err := provider.ValidateImage(ctn.Image, metadata(pod, ctn), pod.Annotations, *c.config.WatchByDefault, c.defaults)
 			if err != nil {
 				c.logger.Error().Err(err).
 					Str("pod_name", pod.Name).
diff --git a/internal/provider/nomad/nomad.go b/internal/provider/nomad/nomad.go
index 26d4f21e..42c244d8 100644
--- a/internal/provider/nomad/nomad.go
+++ b/internal/provider/nomad/nomad.go
@@ -10,18 +10,18 @@ import (
 // Client represents an active nomad provider object
 type Client struct {
 	*provider.Client
-	config        *model.PrdNomad
-	logger        zerolog.Logger
-	imageDefaults *model.Image
+	config   *model.PrdNomad
+	logger   zerolog.Logger
+	defaults *model.Defaults
 }
 
 // New creates new nomad provider instance
-func New(config *model.PrdNomad, imageDefaults *model.Image) *provider.Client {
+func New(config *model.PrdNomad, defaults *model.Defaults) *provider.Client {
 	return &provider.Client{
 		Handler: &Client{
-			config:        config,
-			logger:        log.With().Str("provider", "nomad").Logger(),
-			imageDefaults: imageDefaults,
+			config:   config,
+			logger:   log.With().Str("provider", "nomad").Logger(),
+			defaults: defaults,
 		},
 	}
 }
diff --git a/internal/provider/nomad/task.go b/internal/provider/nomad/task.go
index 7a1bec0b..9dc52a1d 100644
--- a/internal/provider/nomad/task.go
+++ b/internal/provider/nomad/task.go
@@ -101,7 +101,7 @@ func (c *Client) listTaskImages() []model.Image {
 						c.logger.Error().Err(err).Msg("Cannot merge task metadata")
 					}
 
-					image, err := provider.ValidateImage(imageName, metadata(job, taskGroup, task), labels, *c.config.WatchByDefault, *c.imageDefaults)
+					image, err := provider.ValidateImage(imageName, metadata(job, taskGroup, task), labels, *c.config.WatchByDefault, c.defaults)
 					if err != nil {
 						c.logger.Error().
 							Err(err).
diff --git a/internal/provider/swarm/service.go b/internal/provider/swarm/service.go
index d32c9019..44f42a65 100644
--- a/internal/provider/swarm/service.go
+++ b/internal/provider/swarm/service.go
@@ -37,7 +37,7 @@ func (c *Client) listServiceImage() []model.Image {
 			Str("ctn_image", svc.Spec.TaskTemplate.ContainerSpec.Image).
 			Msg("Validate image")
 
-		image, err := provider.ValidateImage(svc.Spec.TaskTemplate.ContainerSpec.Image, metadata(svc), svc.Spec.Labels, *c.config.WatchByDefault, *c.imageDefaults)
+		image, err := provider.ValidateImage(svc.Spec.TaskTemplate.ContainerSpec.Image, metadata(svc), svc.Spec.Labels, *c.config.WatchByDefault, c.defaults)
 		if err != nil {
 			c.logger.Error().Err(err).
 				Str("svc_name", svc.Spec.Name).
diff --git a/internal/provider/swarm/swarm.go b/internal/provider/swarm/swarm.go
index 56a8ce40..522c9c81 100644
--- a/internal/provider/swarm/swarm.go
+++ b/internal/provider/swarm/swarm.go
@@ -10,18 +10,18 @@ import (
 // Client represents an active swarm provider object
 type Client struct {
 	*provider.Client
-	config        *model.PrdSwarm
-	logger        zerolog.Logger
-	imageDefaults *model.Image
+	config   *model.PrdSwarm
+	logger   zerolog.Logger
+	defaults *model.Defaults
 }
 
 // New creates new swarm provider instance
-func New(config *model.PrdSwarm, imageDefaults *model.Image) *provider.Client {
+func New(config *model.PrdSwarm, defaults *model.Defaults) *provider.Client {
 	return &provider.Client{
 		Handler: &Client{
-			config:        config,
-			logger:        log.With().Str("provider", "swarm").Logger(),
-			imageDefaults: imageDefaults,
+			config:   config,
+			logger:   log.With().Str("provider", "swarm").Logger(),
+			defaults: defaults,
 		},
 	}
 }


Test patch:
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index b5263574..871dd817 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -45,6 +45,8 @@ jobs:
             loglevel: info
           - folder: docker4
             loglevel: info
+          - folder: docker5
+            loglevel: info
           - folder: dockerfile1
             loglevel: debug
           - folder: dockerfile2
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 1986071e..5b70440f 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -58,10 +58,12 @@ func TestLoadFile(t *testing.T) {
 						BaseURL: "https://hc-ping.com/",
 						UUID:    "5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278",
 					},
-					ImageDefaults: &model.Image{
-						NotifyOn: model.NotifyOnDefaults,
-						SortTags: registry.SortTagReverse,
-					},
+				},
+				Defaults: &model.Defaults{
+					WatchRepo: utl.NewFalse(),
+					NotifyOn:  []model.NotifyOn{model.NotifyOnNew},
+					MaxTags:   5,
+					SortTags:  registry.SortTagReverse,
 				},
 				Notif: &model.Notif{
 					Amqp: &model.NotifAmqp{
@@ -269,10 +271,11 @@ func TestLoadEnv(t *testing.T) {
 				"DIUN_PROVIDERS_DOCKER=true",
 			},
 			expected: &Config{
-				Db:      (&model.Db{}).GetDefaults(),
-				Watch:   (&model.Watch{}).GetDefaults(),
-				Notif:   nil,
-				RegOpts: nil,
+				Db:       (&model.Db{}).GetDefaults(),
+				Watch:    (&model.Watch{}).GetDefaults(),
+				Defaults: (&model.Defaults{}).GetDefaults(),
+				Notif:    nil,
+				RegOpts:  nil,
 				Providers: &model.Providers{
 					Docker: &model.PrdDocker{
 						TLSVerify:      utl.NewTrue(),
@@ -294,8 +297,9 @@ func TestLoadEnv(t *testing.T) {
 				"DIUN_PROVIDERS_DOCKER=true",
 			},
 			expected: &Config{
-				Db:    (&model.Db{}).GetDefaults(),
-				Watch: (&model.Watch{}).GetDefaults(),
+				Db:       (&model.Db{}).GetDefaults(),
+				Watch:    (&model.Watch{}).GetDefaults(),
+				Defaults: (&model.Defaults{}).GetDefaults(),
 				RegOpts: model.RegOpts{
 					{
 						Name:         "docker.io",
@@ -324,8 +328,9 @@ func TestLoadEnv(t *testing.T) {
 				"DIUN_PROVIDERS_SWARM=true",
 			},
 			expected: &Config{
-				Db:    (&model.Db{}).GetDefaults(),
-				Watch: (&model.Watch{}).GetDefaults(),
+				Db:       (&model.Db{}).GetDefaults(),
+				Watch:    (&model.Watch{}).GetDefaults(),
+				Defaults: (&model.Defaults{}).GetDefaults(),
 				Notif: &model.Notif{
 					Telegram: &model.NotifTelegram{
 						Token:        "abcdef123456",
@@ -350,8 +355,9 @@ func TestLoadEnv(t *testing.T) {
 				"DIUN_PROVIDERS_FILE_DIRECTORY=./fixtures",
 			},
 			expected: &Config{
-				Db:    (&model.Db{}).GetDefaults(),
-				Watch: (&model.Watch{}).GetDefaults(),
+				Db:       (&model.Db{}).GetDefaults(),
+				Watch:    (&model.Watch{}).GetDefaults(),
+				Defaults: (&model.Defaults{}).GetDefaults(),
 				Notif: &model.Notif{
 					Script: &model.NotifScript{
 						Cmd: "uname",
@@ -422,8 +428,9 @@ func TestLoadMixed(t *testing.T) {
 				"DIUN_NOTIF_MAIL_LOCALNAME=foo.com",
 			},
 			expected: &Config{
-				Db:    (&model.Db{}).GetDefaults(),
-				Watch: (&model.Watch{}).GetDefaults(),
+				Db:       (&model.Db{}).GetDefaults(),
+				Watch:    (&model.Watch{}).GetDefaults(),
+				Defaults: (&model.Defaults{}).GetDefaults(),
 				Notif: &model.Notif{
 					Mail: &model.NotifMail{
 						Host:               "127.0.0.1",
@@ -467,8 +474,9 @@ for <code>{{ .Entry.Manifest.Platform }}</code> platform.
 				"DIUN_NOTIF_WEBHOOK_TIMEOUT=1m",
 			},
 			expected: &Config{
-				Db:    (&model.Db{}).GetDefaults(),
-				Watch: (&model.Watch{}).GetDefaults(),
+				Db:       (&model.Db{}).GetDefaults(),
+				Watch:    (&model.Watch{}).GetDefaults(),
+				Defaults: (&model.Defaults{}).GetDefaults(),
 				Notif: &model.Notif{
 					Webhook: &model.NotifWebhook{
 						Endpoint: "http://webhook.foo.com/sd54qad89azd5a",
diff --git a/internal/config/fixtures/config.test.yml b/internal/config/fixtures/config.test.yml
index 39055cdf..58d3c686 100644
--- a/internal/config/fixtures/config.test.yml
+++ b/internal/config/fixtures/config.test.yml
@@ -12,6 +12,11 @@ watch:
     baseURL: https://hc-ping.com/
     uuid: 5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278
 
+defaults:
+  notifyOn:
+    - new
+  maxTags: 5
+
 notif:
   amqp:
     host: localhost
diff --git a/internal/provider/common_test.go b/internal/provider/common_test.go
index abaf59f7..72a9abe3 100644
--- a/internal/provider/common_test.go
+++ b/internal/provider/common_test.go
@@ -6,6 +6,7 @@ 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/assert"
 )
 
@@ -16,9 +17,9 @@ func TestValidateImage(t *testing.T) {
 		metadata      map[string]string
 		labels        map[string]string
 		watchByDef    bool
-		imageDefaults model.Image
+		defaults      *model.Defaults
 		expectedImage model.Image
-		expectedErr   interface{}
+		expectedErr   error
 	}{
 		{
 			name:       "Test with digest",
@@ -76,17 +77,16 @@ func TestValidateImage(t *testing.T) {
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: &invalidLabelError{},
+			expectedErr: errors.Errorf(`cannot parse "chickens" value of label diun.enable`),
 		},
-		// Test diun.regopt
 		{
 			name:  "Set regopt",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.regopt": "foo",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
+			defaults:   &model.Defaults{},
 			expectedImage: model.Image{
 				Name:   "myimg",
 				RegOpt: "foo",
@@ -99,50 +99,18 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.regopt": "",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
-			expectedImage: model.Image{
-				Name:   "myimg",
-				RegOpt: "",
-			},
-			expectedErr: nil,
-		},
-		{
-			name:       "Default regopt",
-			image:      "myimg",
-			watchByDef: true,
-			labels:     map[string]string{},
-			imageDefaults: model.Image{
-				RegOpt: "foo",
-			},
-			expectedImage: model.Image{
-				Name:   "myimg",
-				RegOpt: "foo",
-			},
-			expectedErr: nil,
-		},
-		{
-			name:       "Override default regopt",
-			image:      "myimg",
 			watchByDef: true,
-			labels: map[string]string{
-				"diun.regopt": "bar",
-			},
-			imageDefaults: model.Image{
-				RegOpt: "foo",
-			},
 			expectedImage: model.Image{
 				Name:   "myimg",
-				RegOpt: "bar",
+				RegOpt: "",
 			},
 			expectedErr: nil,
 		},
-		// Test watch_repo
 		{
 			name:       "Include using global settings",
 			image:      "myimg",
 			watchByDef: true,
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				WatchRepo: utl.NewTrue(),
 			},
 			expectedImage: model.Image{
@@ -158,11 +126,11 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.watch_repo": "chickens",
 			},
-			imageDefaults: model.Image{},
+			defaults: &model.Defaults{},
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: &invalidLabelError{},
+			expectedErr: errors.New(`cannot parse "chickens" value of label diun.watch_repo`),
 		},
 		{
 			name:       "Override default image values with labels (true > false)",
@@ -171,7 +139,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.watch_repo": "false",
 			},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				WatchRepo: utl.NewTrue(),
 			},
 			expectedImage: model.Image{
@@ -187,7 +155,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.watch_repo": "true",
 			},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				WatchRepo: utl.NewFalse(),
 			},
 			expectedImage: model.Image{
@@ -196,15 +164,13 @@ func TestValidateImage(t *testing.T) {
 			},
 			expectedErr: nil,
 		},
-		// Test diun.notify_on
 		{
 			name:  "Set valid notify_on",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.notify_on": "new",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:     "myimg",
 				NotifyOn: []model.NotifyOn{model.NotifyOnNew},
@@ -218,12 +184,11 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.notify_on": "chickens",
 			},
-			imageDefaults: model.Image{},
 			expectedImage: model.Image{
 				Name:     "myimg",
 				NotifyOn: []model.NotifyOn{},
 			},
-			expectedErr: &invalidLabelError{},
+			expectedErr: errors.New(`unknown notify status "chickens"`),
 		},
 		{
 			name:       "Set empty notify_on",
@@ -232,7 +197,6 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.notify_on": "",
 			},
-			imageDefaults: model.Image{},
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
@@ -243,7 +207,7 @@ func TestValidateImage(t *testing.T) {
 			image:      "myimg",
 			watchByDef: true,
 			labels:     map[string]string{},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				NotifyOn: []model.NotifyOn{model.NotifyOnNew},
 			},
 			expectedImage: model.Image{
@@ -259,7 +223,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.notify_on": "update",
 			},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				NotifyOn: []model.NotifyOn{model.NotifyOnNew},
 			},
 			expectedImage: model.Image{
@@ -268,15 +232,13 @@ func TestValidateImage(t *testing.T) {
 			},
 			expectedErr: nil,
 		},
-		// Test diun.sort_tags
 		{
 			name:  "Set valid sort_tags",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.sort_tags": "semver",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:     "myimg",
 				SortTags: registry.SortTagSemver,
@@ -289,12 +251,11 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.sort_tags": "chickens",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: &invalidLabelError{},
+			expectedErr: errors.New(`unknown sort tags type "chickens"`),
 		},
 		{
 			name:  "Set empty sort_tags",
@@ -302,8 +263,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.sort_tags": "",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
@@ -314,7 +274,7 @@ func TestValidateImage(t *testing.T) {
 			image:      "myimg",
 			watchByDef: true,
 			labels:     map[string]string{},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				SortTags: registry.SortTagSemver,
 			},
 			expectedImage: model.Image{
@@ -330,7 +290,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.sort_tags": "reverse",
 			},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				SortTags: registry.SortTagSemver,
 			},
 			expectedImage: model.Image{
@@ -339,15 +299,13 @@ func TestValidateImage(t *testing.T) {
 			},
 			expectedErr: nil,
 		},
-		// Test diun.max_tags
 		{
 			name:  "Set valid max_tags",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.max_tags": "10",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:    "myimg",
 				MaxTags: 10,
@@ -360,12 +318,11 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.max_tags": "chickens",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: &invalidLabelError{},
+			expectedErr: errors.New(`cannot parse "chickens" value of label diun.max_tags`),
 		},
 		{
 			name:  "Set empty max_tags",
@@ -373,19 +330,18 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.max_tags": "",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: &invalidLabelError{},
+			expectedErr: errors.New(`cannot parse "" value of label diun.max_tags`),
 		},
 		{
 			name:       "Default max_tags",
 			image:      "myimg",
 			watchByDef: true,
 			labels:     map[string]string{},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				MaxTags: 10,
 			},
 			expectedImage: model.Image{
@@ -401,7 +357,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.max_tags": "11",
 			},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				MaxTags: 10,
 			},
 			expectedImage: model.Image{
@@ -410,15 +366,13 @@ func TestValidateImage(t *testing.T) {
 			},
 			expectedErr: nil,
 		},
-		// Test diun.include_tags
 		{
 			name:  "Set include_tags",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.include_tags": "alpine;ubuntu",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:        "myimg",
 				IncludeTags: []string{"alpine", "ubuntu"},
@@ -431,8 +385,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.include_tags": "",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:        "myimg",
 				IncludeTags: []string{""},
@@ -444,7 +397,7 @@ func TestValidateImage(t *testing.T) {
 			image:      "myimg",
 			watchByDef: true,
 			labels:     map[string]string{},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				IncludeTags: []string{"alpine"},
 			},
 			expectedImage: model.Image{
@@ -460,7 +413,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.include_tags": "ubuntu",
 			},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				IncludeTags: []string{"alpine"},
 			},
 			expectedImage: model.Image{
@@ -469,15 +422,13 @@ func TestValidateImage(t *testing.T) {
 			},
 			expectedErr: nil,
 		},
-		// Test diun.exclude_tags
 		{
 			name:  "Set exclude_tags",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.exclude_tags": "alpine;ubuntu",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:        "myimg",
 				ExcludeTags: []string{"alpine", "ubuntu"},
@@ -490,8 +441,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.exclude_tags": "",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:        "myimg",
 				ExcludeTags: []string{""},
@@ -503,7 +453,7 @@ func TestValidateImage(t *testing.T) {
 			image:      "myimg",
 			watchByDef: true,
 			labels:     map[string]string{},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				ExcludeTags: []string{"alpine"},
 			},
 			expectedImage: model.Image{
@@ -519,7 +469,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.exclude_tags": "ubuntu",
 			},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				ExcludeTags: []string{"alpine"},
 			},
 			expectedImage: model.Image{
@@ -528,15 +478,13 @@ func TestValidateImage(t *testing.T) {
 			},
 			expectedErr: nil,
 		},
-		// Test diun.hub_tpl
 		{
 			name:  "Set hub_tpl",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.hub_tpl": "foo",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:   "myimg",
 				HubTpl: "foo",
@@ -549,53 +497,20 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.hub_tpl": "",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
-			expectedImage: model.Image{
-				Name:   "myimg",
-				HubTpl: "",
-			},
-			expectedErr: nil,
-		},
-		{
-			name:       "Default hub_tpl",
-			image:      "myimg",
-			watchByDef: true,
-			labels:     map[string]string{},
-			imageDefaults: model.Image{
-				HubTpl: "foo",
-			},
-			expectedImage: model.Image{
-				Name:   "myimg",
-				HubTpl: "foo",
-			},
-			expectedErr: nil,
-		},
-		{
-			name:       "Override default hub_tpl",
-			image:      "myimg",
 			watchByDef: true,
-			labels: map[string]string{
-				"diun.hub_tpl": "bar",
-			},
-			imageDefaults: model.Image{
-				HubTpl: "foo",
-			},
 			expectedImage: model.Image{
 				Name:   "myimg",
-				HubTpl: "bar",
+				HubTpl: "",
 			},
 			expectedErr: nil,
 		},
-		// Test diun.hub_link
 		{
 			name:  "Set hub_link",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.hub_link": "foo",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:    "myimg",
 				HubLink: "foo",
@@ -608,53 +523,20 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.hub_link": "",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
-			expectedImage: model.Image{
-				Name:    "myimg",
-				HubLink: "",
-			},
-			expectedErr: nil,
-		},
-		{
-			name:       "Default hub_link",
-			image:      "myimg",
 			watchByDef: true,
-			labels:     map[string]string{},
-			imageDefaults: model.Image{
-				HubLink: "foo",
-			},
-			expectedImage: model.Image{
-				Name:    "myimg",
-				HubLink: "foo",
-			},
-			expectedErr: nil,
-		},
-		{
-			name:       "Override default hub_link",
-			image:      "myimg",
-			watchByDef: true,
-			labels: map[string]string{
-				"diun.hub_link": "bar",
-			},
-			imageDefaults: model.Image{
-				HubLink: "foo",
-			},
 			expectedImage: model.Image{
 				Name:    "myimg",
-				HubLink: "bar",
+				HubLink: "",
 			},
 			expectedErr: nil,
 		},
-		// Test diun.platform
 		{
 			name:  "Set valid platform",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.platform": "linux/arm/v7",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 				Platform: model.ImagePlatform{
@@ -671,12 +553,11 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.platform": "chickens",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: &invalidLabelError{},
+			expectedErr: errors.New(`cannot parse "chickens" platform of label diun.platform`),
 		},
 		{
 			name:  "Set empty platform",
@@ -684,69 +565,20 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.platform": "",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name:     "myimg",
 				Platform: model.ImagePlatform{},
 			},
-			expectedErr: &invalidLabelError{},
-		},
-		{
-			name:       "Default platform",
-			image:      "myimg",
-			watchByDef: true,
-			labels:     map[string]string{},
-			imageDefaults: model.Image{
-				Platform: model.ImagePlatform{
-					OS:      "linux",
-					Arch:    "arm",
-					Variant: "v7",
-				},
-			},
-			expectedImage: model.Image{
-				Name: "myimg",
-				Platform: model.ImagePlatform{
-					OS:      "linux",
-					Arch:    "arm",
-					Variant: "v7",
-				},
-			},
-			expectedErr: nil,
-		},
-		{
-			name:       "Override default platform",
-			image:      "myimg",
-			watchByDef: true,
-			labels: map[string]string{
-				"diun.platform": "linux/arm/v6",
-			},
-			imageDefaults: model.Image{
-				Platform: model.ImagePlatform{
-					OS:      "linux",
-					Arch:    "arm",
-					Variant: "v7",
-				},
-			},
-			expectedImage: model.Image{
-				Name: "myimg",
-				Platform: model.ImagePlatform{
-					OS:      "linux",
-					Arch:    "arm",
-					Variant: "v6",
-				},
-			},
-			expectedErr: nil,
+			expectedErr: errors.New(`cannot parse "" platform of label diun.platform`),
 		},
-		// Test diun.metadata
 		{
 			name:  "Set valid metadata",
 			image: "myimg",
 			labels: map[string]string{
 				"diun.metadata.foo123": "bar",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 				Metadata: map[string]string{
@@ -761,12 +593,11 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.metadata.lots of chickens": "bar",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: &invalidLabelError{},
+			expectedErr: errors.New(`invalid metadata key "lots of chickens"`),
 		},
 		{
 			name:  "Set empty metadata key",
@@ -774,8 +605,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.metadata.": "bar",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
@@ -786,8 +616,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.metadata.foo123": "",
 			},
-			watchByDef:    true,
-			imageDefaults: model.Image{},
+			watchByDef: true,
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
@@ -797,7 +626,7 @@ func TestValidateImage(t *testing.T) {
 			image:      "myimg",
 			watchByDef: true,
 			labels:     map[string]string{},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				Metadata: map[string]string{
 					"foo123": "bar",
 				},
@@ -817,7 +646,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.metadata.biz123": "baz",
 			},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				Metadata: map[string]string{
 					"foo123": "bar",
 				},
@@ -838,7 +667,7 @@ func TestValidateImage(t *testing.T) {
 			labels: map[string]string{
 				"diun.metadata.foo123": "baz",
 			},
-			imageDefaults: model.Image{
+			defaults: &model.Defaults{
 				Metadata: map[string]string{
 					"foo123": "bar",
 				},
@@ -856,24 +685,18 @@ func TestValidateImage(t *testing.T) {
 	for _, tt := range cases {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
-			t.Parallel()
 			img, err := ValidateImage(
 				tt.image,
 				tt.metadata,
 				tt.labels,
 				tt.watchByDef,
-				tt.imageDefaults,
+				tt.defaults,
 			)
 			if tt.expectedErr == nil {
 				assert.NoError(t, err)
 				assert.Equal(t, tt.expectedImage, img)
 			} else {
-				switch err.(type) {
-				case *invalidLabelError:
-					assert.Error(t, err)
-				default:
-					assert.Error(t, err)
-				}
+				assert.ErrorContains(t, err, tt.expectedErr.Error())
 			}
 		})
 	}
diff --git a/internal/provider/file/file_test.go b/internal/provider/file/file_test.go
index c1b3a019..b3268ff4 100644
--- a/internal/provider/file/file_test.go
+++ b/internal/provider/file/file_test.go
@@ -10,7 +10,7 @@ import (
 )
 
 var (
-	defaultImageDefaults = model.Image{
+	defaults = model.Defaults{
 		NotifyOn: model.NotifyOnDefaults,
 		SortTags: registry.SortTagReverse,
 	}
@@ -157,7 +157,7 @@ var (
 func TestListJobFilename(t *testing.T) {
 	fc := New(&model.PrdFile{
 		Filename: "./fixtures/dockerhub.yml",
-	}, &defaultImageDefaults)
+	}, &defaults)
 
 	assert.Equal(t, dockerhubFile, fc.ListJob())
 }
@@ -165,7 +165,7 @@ func TestListJobFilename(t *testing.T) {
 func TestListJobDirectory(t *testing.T) {
 	fc := New(&model.PrdFile{
 		Directory: "./fixtures",
-	}, &defaultImageDefaults)
+	}, &defaults)
 
 	assert.Equal(t, append(append(bintrayFile, dockerhubFile...), append(lscrFile, quayFile...)...), fc.ListJob())
 }
@@ -173,7 +173,9 @@ func TestListJobDirectory(t *testing.T) {
 func TestDefaultImageOptions(t *testing.T) {
 	fc := New(&model.PrdFile{
 		Filename: "./fixtures/dockerhub.yml",
-	}, &model.Image{WatchRepo: utl.NewTrue()})
+	}, &model.Defaults{
+		WatchRepo: utl.NewTrue(),
+	})
 
 	for _, job := range fc.ListJob() {
 		assert.True(t, *job.Image.WatchRepo)
diff --git a/test/docker5/diun.yml b/test/docker5/diun.yml
new file mode 100644
index 00000000..3a3c8917
--- /dev/null
+++ b/test/docker5/diun.yml
@@ -0,0 +1,12 @@
+watch:
+  workers: 20
+  schedule: "0 */6 * * *"
+
+defaults:
+  watchRepo: true
+  notifyOn:
+    - new
+  maxTags: 5
+
+providers:
+  docker: {}