Repository: crazy-max/diun
Pull request #965 (2023-09-16 09:04:50)

go test grab-bag
follow-ups:
* https://github.com/crazy-max/diun/pull/887#discussion_r1327856942
* https://github.com/crazy-max/diun/pull/887#discussion_r1327856623

A small grab-bag of tweaks and enhancements.


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 29a54f46..1e238623 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -30,6 +30,9 @@ linters-settings:
       # The io/ioutil package has been deprecated.
       # https://go.dev/doc/go1.16#ioutil
       - io/ioutil
+  forbidigo:
+    forbid:
+      - '^fmt\.Errorf(# use errors\.Errorf instead)?$'
   importas:
     no-unaliased: true
 
diff --git a/internal/app/diun.go b/internal/app/diun.go
index 9a035d66..9c5d05d4 100644
--- a/internal/app/diun.go
+++ b/internal/app/diun.go
@@ -75,7 +75,7 @@ func New(meta model.Meta, cfg *config.Config, grpcAuthority string) (*Diun, erro
 		if len(cfg.Watch.Healthchecks.BaseURL) > 0 {
 			hcBaseURL, err = url.Parse(cfg.Watch.Healthchecks.BaseURL)
 			if err != nil {
-				return nil, errors.Wrap(err, "Cannot parse Healthchecks base URL")
+				return nil, errors.Wrap(err, "cannot parse Healthchecks base URL")
 			}
 		}
 		diun.hc = gohealthchecks.NewClient(&gohealthchecks.ClientOptions{
diff --git a/internal/config/config.go b/internal/config/config.go
index d7f61258..a39b0982 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -36,7 +36,7 @@ func Load(config string) (*Config, error) {
 		},
 	})
 	if found, err := fileLoader.Load(&cfg); err != nil {
-		return nil, errors.Wrap(err, "Failed to decode configuration from file")
+		return nil, errors.Wrap(err, "failed to decode configuration from file")
 	} else if !found {
 		log.Debug().Msg("No configuration file found")
 	} else {
@@ -47,7 +47,7 @@ func Load(config string) (*Config, error) {
 		Prefix: "DIUN_",
 	})
 	if found, err := envLoader.Load(&cfg); err != nil {
-		return nil, errors.Wrap(err, "Failed to decode configuration from environment variables")
+		return nil, errors.Wrap(err, "failed to decode configuration from environment variables")
 	} else if !found {
 		log.Debug().Msg("No DIUN_* environment variables defined")
 	} else {
@@ -64,16 +64,16 @@ func Load(config string) (*Config, error) {
 func (cfg *Config) validate() error {
 	if len(cfg.Db.Path) > 0 {
 		if err := os.MkdirAll(path.Dir(cfg.Db.Path), os.ModePerm); err != nil {
-			return errors.Wrap(err, "Cannot create database destination folder")
+			return errors.Wrap(err, "cannot create database destination folder")
 		}
 	}
 
 	if cfg.Watch.Healthchecks != nil && len(cfg.Watch.Healthchecks.UUID) == 0 {
-		return errors.New("Healthchecks UUID is required")
+		return errors.New("healthchecks UUID is required")
 	}
 
 	if cfg.Providers == nil {
-		return errors.New("At least one provider is required")
+		return errors.New("at least one provider is required")
 	}
 
 	return validator.New().Struct(cfg)
diff --git a/internal/db/client.go b/internal/db/client.go
index 070c6edf..32a2840e 100644
--- a/internal/db/client.go
+++ b/internal/db/client.go
@@ -52,7 +52,7 @@ func New(cfg model.Db) (*Client, error) {
 		log.Debug().Msgf("%d entries found in manifest bucket", stats.KeyN)
 		return nil
 	}); err != nil {
-		return nil, errors.Wrap(err, "Cannot count entries in manifest bucket")
+		return nil, errors.Wrap(err, "cannot count entries in manifest bucket")
 	}
 
 	c := &Client{
diff --git a/internal/db/migrate.go b/internal/db/migrate.go
index 7a386d43..85413336 100644
--- a/internal/db/migrate.go
+++ b/internal/db/migrate.go
@@ -2,7 +2,6 @@ package db
 
 import (
 	"encoding/json"
-	"fmt"
 	"time"
 
 	"github.com/opencontainers/go-digest"
@@ -24,12 +23,12 @@ func (c *Client) Migrate() error {
 	for version := c.metadata.Version + 1; version <= dbVersion; version++ {
 		migration, found := migrations[version]
 		if !found {
-			return fmt.Errorf("database migration v%d not found", version)
+			return errors.Errorf("database migration v%d not found", version)
 		}
 
 		log.Info().Msgf("Database migration v%d...", version)
 		if err := migration(c); err != nil {
-			return errors.Wrapf(err, "Database migration v%d failed", version)
+			return errors.Wrapf(err, "database migration v%d failed", version)
 		}
 	}
 
diff --git a/internal/grpc/client.go b/internal/grpc/client.go
index f58699e5..83fb0dbc 100644
--- a/internal/grpc/client.go
+++ b/internal/grpc/client.go
@@ -46,7 +46,7 @@ func (c *Client) Start() error {
 
 	lis, err := net.Listen("tcp", c.authority)
 	if err != nil {
-		return errors.Wrap(err, "Cannot create gRPC listener")
+		return errors.Wrap(err, "cannot create gRPC listener")
 	}
 
 	return c.server.Serve(lis)
diff --git a/internal/grpc/image.go b/internal/grpc/image.go
index 6d3a711f..57ce26db 100644
--- a/internal/grpc/image.go
+++ b/internal/grpc/image.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/containers/image/v5/docker/reference"
 	"github.com/crazy-max/diun/v4/pb"
+	"github.com/pkg/errors"
 	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
@@ -55,7 +56,7 @@ func (c *Client) ImageInspect(ctx context.Context, request *pb.ImageInspectReque
 	}
 
 	if _, ok := images[ref.Name()]; !ok {
-		return nil, fmt.Errorf("%s not found in database", ref.Name())
+		return nil, errors.Errorf("%s not found in database", ref.Name())
 	}
 
 	iir := &pb.ImageInspectResponse_Image{
diff --git a/internal/model/regopts.go b/internal/model/regopts.go
index ee8fd65a..084a5b1f 100644
--- a/internal/model/regopts.go
+++ b/internal/model/regopts.go
@@ -1,12 +1,12 @@
 package model
 
 import (
-	"fmt"
 	"strings"
 	"time"
 
 	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/crazy-max/diun/v4/pkg/utl"
+	"github.com/pkg/errors"
 )
 
 // RegOpts holds slice of registry options
@@ -60,5 +60,5 @@ func (s *RegOpts) Select(name string, image registry.Image) (*RegOpt, error) {
 	if len(name) == 0 {
 		return nil, nil
 	}
-	return nil, fmt.Errorf("%s not found", name)
+	return nil, errors.Errorf("%s not found", name)
 }
diff --git a/internal/msg/client.go b/internal/msg/client.go
index 9f82819a..c2a01e73 100644
--- a/internal/msg/client.go
+++ b/internal/msg/client.go
@@ -41,7 +41,7 @@ func (c *Client) RenderMarkdown() (title []byte, body []byte, _ error) {
 	var titleBuf bytes.Buffer
 	titleTpl, err := template.New("title").Funcs(c.opts.TemplateFuncs).Parse(strings.TrimSuffix(strings.TrimSpace(c.opts.TemplateTitle), "\n"))
 	if err != nil {
-		return title, body, errors.Wrap(err, "Cannot parse title template")
+		return title, body, errors.Wrap(err, "cannot parse title template")
 	}
 	if err = titleTpl.Execute(&titleBuf, struct {
 		Meta  model.Meta
@@ -50,14 +50,14 @@ func (c *Client) RenderMarkdown() (title []byte, body []byte, _ error) {
 		Meta:  c.opts.Meta,
 		Entry: c.opts.Entry,
 	}); err != nil {
-		return title, body, errors.Wrap(err, "Cannot render notif title")
+		return title, body, errors.Wrap(err, "cannot render notif title")
 	}
 	title = titleBuf.Bytes()
 
 	var bodyBuf bytes.Buffer
 	bodyTpl, err := template.New("body").Funcs(c.opts.TemplateFuncs).Parse(strings.TrimSuffix(strings.TrimSpace(c.opts.TemplateBody), "\n"))
 	if err != nil {
-		return title, body, errors.Wrap(err, "Cannot parse body template")
+		return title, body, errors.Wrap(err, "cannot parse body template")
 	}
 	if err = bodyTpl.Execute(&bodyBuf, struct {
 		Meta  model.Meta
@@ -66,7 +66,7 @@ func (c *Client) RenderMarkdown() (title []byte, body []byte, _ error) {
 		Meta:  c.opts.Meta,
 		Entry: c.opts.Entry,
 	}); err != nil {
-		return title, body, errors.Wrap(err, "Cannot render notif body")
+		return title, body, errors.Wrap(err, "cannot render notif body")
 	}
 	body = bodyBuf.Bytes()
 
diff --git a/internal/notif/discord/client.go b/internal/notif/discord/client.go
index 3c895493..20966d82 100644
--- a/internal/notif/discord/client.go
+++ b/internal/notif/discord/client.go
@@ -11,6 +11,7 @@ import (
 	"github.com/crazy-max/diun/v4/internal/model"
 	"github.com/crazy-max/diun/v4/internal/msg"
 	"github.com/crazy-max/diun/v4/internal/notif/notifier"
+	"github.com/pkg/errors"
 )
 
 // Client represents an active discord notification object
@@ -138,7 +139,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
 	}
 
 	if resp.StatusCode != http.StatusNoContent {
-		return fmt.Errorf("unexpected HTTP status %d: %s", resp.StatusCode, resp.Body)
+		return errors.Errorf("unexpected HTTP status %d: %s", resp.StatusCode, resp.Body)
 	}
 
 	return nil
diff --git a/internal/notif/gotify/client.go b/internal/notif/gotify/client.go
index 46114123..94222c5c 100644
--- a/internal/notif/gotify/client.go
+++ b/internal/notif/gotify/client.go
@@ -3,7 +3,6 @@ package gotify
 import (
 	"bytes"
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"net/url"
 	"path"
@@ -42,7 +41,7 @@ func (c *Client) Name() string {
 func (c *Client) Send(entry model.NotifEntry) error {
 	token, err := utl.GetSecret(c.cfg.Token, c.cfg.TokenFile)
 	if err != nil {
-		return errors.New("Cannot retrieve token secret for Gotify notifier")
+		return errors.Wrap(err, "cannot retrieve token secret for Gotify notifier")
 	}
 
 	hc := http.Client{
@@ -117,7 +116,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
 		if err != nil {
 			return err
 		}
-		return fmt.Errorf("%d %s: %s", errBody.ErrorCode, errBody.Error, errBody.ErrorDescription)
+		return errors.Errorf("%d %s: %s", errBody.ErrorCode, errBody.Error, errBody.ErrorDescription)
 	}
 
 	return nil
diff --git a/internal/notif/mail/client.go b/internal/notif/mail/client.go
index 243cac61..67603213 100644
--- a/internal/notif/mail/client.go
+++ b/internal/notif/mail/client.go
@@ -13,6 +13,7 @@ import (
 	"github.com/crazy-max/diun/v4/pkg/utl"
 	"github.com/go-gomail/gomail"
 	"github.com/matcornic/hermes/v2"
+	"github.com/pkg/errors"
 	"github.com/rs/zerolog/log"
 )
 
@@ -89,13 +90,13 @@ func (c *Client) Send(entry model.NotifEntry) error {
 	// Generate an HTML email with the provided contents (for modern clients)
 	htmlpart, err := h.GenerateHTML(email)
 	if err != nil {
-		return fmt.Errorf("hermes: %v", err)
+		return errors.Wrap(err, "cannot generate HTML email")
 	}
 
 	// Generate the plaintext version of the e-mail (for clients that do not support xHTML)
 	textpart, err := h.GeneratePlainText(email)
 	if err != nil {
-		return fmt.Errorf("hermes: %v", err)
+		return errors.Wrap(err, "cannot generate plaintext email")
 	}
 
 	mailMessage := gomail.NewMessage()
diff --git a/internal/notif/matrix/client.go b/internal/notif/matrix/client.go
index dfce958d..37be85a5 100644
--- a/internal/notif/matrix/client.go
+++ b/internal/notif/matrix/client.go
@@ -48,11 +48,11 @@ func (c *Client) Send(entry model.NotifEntry) error {
 
 	user, err := utl.GetSecret(c.cfg.User, c.cfg.UserFile)
 	if err != nil {
-		return errors.New("Cannot retrieve username secret for Matrix notifier")
+		return errors.Wrap(err, "cannot retrieve username secret for Matrix notifier")
 	}
 	password, err := utl.GetSecret(c.cfg.Password, c.cfg.PasswordFile)
 	if err != nil {
-		return errors.New("Cannot retrieve password secret for Matrix notifier")
+		return errors.Wrap(err, "cannot retrieve password secret for Matrix notifier")
 	}
 
 	r, err := m.Login(&gomatrix.ReqLogin{
diff --git a/internal/notif/ntfy/client.go b/internal/notif/ntfy/client.go
index c444bc9c..7288438e 100644
--- a/internal/notif/ntfy/client.go
+++ b/internal/notif/ntfy/client.go
@@ -11,6 +11,7 @@ import (
 	"github.com/crazy-max/diun/v4/internal/msg"
 	"github.com/crazy-max/diun/v4/internal/notif/notifier"
 	"github.com/crazy-max/diun/v4/pkg/utl"
+	"github.com/pkg/errors"
 )
 
 // Client represents an active ntfy notification object
@@ -107,7 +108,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
 		if err != nil {
 			return err
 		}
-		return fmt.Errorf("%d %s: %s", errBody.ErrorCode, errBody.Error, errBody.ErrorDescription)
+		return errors.Errorf("%d %s: %s", errBody.ErrorCode, errBody.Error, errBody.ErrorDescription)
 	}
 
 	return nil
diff --git a/internal/notif/pushover/client.go b/internal/notif/pushover/client.go
index dd871bcf..2fb7b9ce 100644
--- a/internal/notif/pushover/client.go
+++ b/internal/notif/pushover/client.go
@@ -37,12 +37,12 @@ func (c *Client) Name() string {
 func (c *Client) Send(entry model.NotifEntry) error {
 	token, err := utl.GetSecret(c.cfg.Token, c.cfg.TokenFile)
 	if err != nil {
-		return errors.New("Cannot retrieve token secret for Pushover notifier")
+		return errors.Wrap(err, "cannot retrieve token secret for Pushover notifier")
 	}
 
 	recipient, err := utl.GetSecret(c.cfg.Recipient, c.cfg.RecipientFile)
 	if err != nil {
-		return errors.New("Cannot retrieve recipient secret for Pushover notifier")
+		return errors.Wrap(err, "cannot retrieve recipient secret for Pushover notifier")
 	}
 
 	message, err := msg.New(msg.Options{
diff --git a/internal/notif/rocketchat/client.go b/internal/notif/rocketchat/client.go
index bf2704e0..4db4aaf3 100644
--- a/internal/notif/rocketchat/client.go
+++ b/internal/notif/rocketchat/client.go
@@ -3,7 +3,6 @@ package rocketchat
 import (
 	"bytes"
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"net/url"
 	"path"
@@ -44,7 +43,7 @@ func (c *Client) Name() string {
 func (c *Client) Send(entry model.NotifEntry) error {
 	token, err := utl.GetSecret(c.cfg.Token, c.cfg.TokenFile)
 	if err != nil {
-		return errors.New("Cannot retrieve token secret for RocketChat notifier")
+		return errors.Wrap(err, "cannot retrieve token secret for RocketChat notifier")
 	}
 
 	hc := http.Client{
@@ -152,7 +151,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
 	}
 
 	if resp.StatusCode != http.StatusOK {
-		return fmt.Errorf("HTTP error %d: %s", resp.StatusCode, respBody.ErrorType)
+		return errors.Errorf("unexpected HTTP error %d: %s", resp.StatusCode, respBody.ErrorType)
 	}
 
 	return nil
diff --git a/internal/notif/telegram/client.go b/internal/notif/telegram/client.go
index 6f43a120..49422a12 100644
--- a/internal/notif/telegram/client.go
+++ b/internal/notif/telegram/client.go
@@ -39,17 +39,17 @@ func (c *Client) Name() string {
 func (c *Client) Send(entry model.NotifEntry) error {
 	token, err := utl.GetSecret(c.cfg.Token, c.cfg.TokenFile)
 	if err != nil {
-		return errors.New("Cannot retrieve token secret for Telegram notifier")
+		return errors.Wrap(err, "cannot retrieve token secret for Telegram notifier")
 	}
 
 	chatIDs := c.cfg.ChatIDs
 	chatIDsRaw, err := utl.GetSecret("", c.cfg.ChatIDsFile)
 	if err != nil {
-		return errors.New("Cannot retrieve chat IDs secret for Telegram notifier")
+		return errors.Wrap(err, "cannot retrieve chat IDs secret for Telegram notifier")
 	}
 	if len(chatIDsRaw) > 0 {
 		if err = json.Unmarshal([]byte(chatIDsRaw), &chatIDs); err != nil {
-			return errors.New("Cannot unmarshal chat IDs secret for Telegram notifier")
+			return errors.Wrap(err, "cannot unmarshal chat IDs secret for Telegram notifier")
 		}
 	}
 
diff --git a/internal/provider/common.go b/internal/provider/common.go
index 97cd5939..31687061 100644
--- a/internal/provider/common.go
+++ b/internal/provider/common.go
@@ -1,8 +1,6 @@
 package provider
 
 import (
-	"errors"
-	"fmt"
 	"regexp"
 	"strconv"
 	"strings"
@@ -11,12 +9,12 @@ import (
 	"github.com/crazy-max/diun/v4/internal/model"
 	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/imdario/mergo"
+	"github.com/pkg/errors"
 )
 
 var (
 	metadataKeyChars  = `a-zA-Z0-9_`
 	metadataKeyRegexp = regexp.MustCompile(`^[` + metadataKeyChars + `]+$`)
-	ErrInvalidLabel   = errors.New("invalid label error")
 )
 
 // ValidateImage returns a standard image through Docker labels
@@ -30,13 +28,13 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 	}
 
 	if err := mergo.Merge(&img, imageDefaults); err != nil {
-		return img, fmt.Errorf("failed to merge image defaults for image %s", image)
+		return img, &invalidLabelError{errors.Wrapf(err, "failed to merge image defaults for image %s", image)}
 	}
 
 	if enableStr, ok := labels["diun.enable"]; ok {
 		enable, err := strconv.ParseBool(enableStr)
 		if err != nil {
-			return img, fmt.Errorf("cannot parse %q value of label diun.enable: %w", enableStr, ErrInvalidLabel)
+			return img, &invalidLabelError{errors.Wrapf(err, "cannot parse %q value of label diun.enable", enableStr)}
 		}
 		if !enable {
 			return model.Image{}, nil
@@ -53,7 +51,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, fmt.Errorf("cannot parse %q value of label %s: %w", value, key, ErrInvalidLabel)
+				return img, &invalidLabelError{errors.Wrapf(err, "cannot parse %q value of label %s", value, key)}
 			}
 		case key == "diun.notify_on":
 			if len(value) == 0 {
@@ -63,7 +61,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, fmt.Errorf("unknown notify status %q: %w", value, ErrInvalidLabel)
+					return img, &invalidLabelError{errors.Errorf("unknown notify status %q", value)}
 				}
 				img.NotifyOn = append(img.NotifyOn, notifyOn)
 			}
@@ -73,12 +71,12 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 			}
 			sortTags := registry.SortTag(value)
 			if !sortTags.Valid() {
-				return img, fmt.Errorf("unknown sort tags type %q: %w", value, ErrInvalidLabel)
+				return img, &invalidLabelError{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, fmt.Errorf("cannot parse %q value of label %s: %w", value, key, ErrInvalidLabel)
+				return img, &invalidLabelError{errors.Wrapf(err, "cannot parse %q value of label %s", value, key)}
 			}
 		case key == "diun.include_tags":
 			img.IncludeTags = strings.Split(value, ";")
@@ -91,7 +89,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, fmt.Errorf("cannot parse %q platform of label %s: %w", value, key, ErrInvalidLabel)
+				return img, &invalidLabelError{errors.Wrapf(err, "cannot parse %q platform of label %s", value, key)}
 			}
 			img.Platform = model.ImagePlatform{
 				OS:      platform.OS,
@@ -104,7 +102,7 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 				break
 			}
 			if err := validateMetadataKey(mkey); err != nil {
-				return img, fmt.Errorf("invalid metadata key %q: %w: %w", mkey, err, ErrInvalidLabel)
+				return img, &invalidLabelError{errors.Wrapf(err, "invalid metadata key %q", mkey)}
 			}
 			if img.Metadata == nil {
 				img.Metadata = map[string]string{}
@@ -115,7 +113,7 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 
 	// Update provider metadata with metadata from img labels
 	if err := mergo.Merge(&img.Metadata, metadata); err != nil {
-		return img, fmt.Errorf("failed merging metadata: %w", err)
+		return img, errors.Wrapf(err, "failed merging metadata")
 	}
 
 	return img, nil
@@ -123,7 +121,19 @@ func ValidateImage(image string, metadata, labels map[string]string, watchByDef
 
 func validateMetadataKey(key string) error {
 	if !metadataKeyRegexp.MatchString(key) {
-		return fmt.Errorf("only %q are allowed", metadataKeyChars)
+		return errors.Errorf("only %q are allowed", metadataKeyChars)
 	}
 	return nil
 }
+
+type invalidLabelError struct {
+	error
+}
+
+func (e *invalidLabelError) Error() string {
+	return e.Error()
+}
+
+func (e *invalidLabelError) Unwrap() error {
+	return e
+}
diff --git a/pkg/dockerfile/client.go b/pkg/dockerfile/client.go
index 19d5d9b3..f7fa2e5b 100644
--- a/pkg/dockerfile/client.go
+++ b/pkg/dockerfile/client.go
@@ -27,17 +27,17 @@ type Options struct {
 func New(opts Options) (*Client, error) {
 	b, err := os.ReadFile(opts.Filename)
 	if err != nil {
-		return nil, errors.Wrapf(err, "Cannot read Dockerfile %s", opts.Filename)
+		return nil, errors.Wrapf(err, "cannot read Dockerfile %s", opts.Filename)
 	}
 
 	parsed, err := parser.Parse(bytes.NewReader(b))
 	if err != nil {
-		return nil, errors.Wrapf(err, "Cannot parse Dockerfile %s", opts.Filename)
+		return nil, errors.Wrapf(err, "cannot parse Dockerfile %s", opts.Filename)
 	}
 
 	stages, metaArgs, err := instructions.Parse(parsed.AST)
 	if err != nil {
-		return nil, errors.Wrapf(err, "Cannot parse stages for Dockerfile %s", opts.Filename)
+		return nil, errors.Wrapf(err, "cannot parse stages for Dockerfile %s", opts.Filename)
 	}
 
 	var kvpoArgs []instructions.KeyValuePairOptional
diff --git a/pkg/dockerfile/image.go b/pkg/dockerfile/image.go
index fb177d6e..d8fb9ca8 100644
--- a/pkg/dockerfile/image.go
+++ b/pkg/dockerfile/image.go
@@ -35,7 +35,7 @@ func (c *Client) FromImages() (Images, error) {
 		case command.From:
 			ins, err := instructions.ParseInstruction(node)
 			if err != nil {
-				return nil, errors.Wrapf(err, "Cannot parse instruction")
+				return nil, errors.Wrapf(err, "cannot parse instruction")
 			}
 			if baseName := ins.(*instructions.Stage).BaseName; baseName != "scratch" {
 				name, err := c.shlex.ProcessWordWithMap(baseName, metaArgsToMap(c.metaArgs))
@@ -56,7 +56,7 @@ func (c *Client) FromImages() (Images, error) {
 		case command.Copy:
 			cmd, err := instructions.ParseCommand(node)
 			if err != nil {
-				return nil, errors.Wrapf(err, "Cannot parse command")
+				return nil, errors.Wrapf(err, "cannot parse command")
 			}
 			if copyFrom := cmd.(*instructions.CopyCommand).From; copyFrom != "null" {
 				name, err := c.shlex.ProcessWordWithMap(copyFrom, metaArgsToMap(c.metaArgs))
@@ -77,7 +77,7 @@ func (c *Client) FromImages() (Images, error) {
 		case command.Run:
 			cmd, err := instructions.ParseCommand(node)
 			if err != nil {
-				return nil, errors.Wrapf(err, "Cannot parse command")
+				return nil, errors.Wrapf(err, "cannot parse command")
 			}
 			if cmdRun, ok := cmd.(*instructions.RunCommand); ok {
 				mounts := instructions.GetMounts(cmdRun)
diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go
index 6bc987c4..538f1b20 100644
--- a/pkg/k8s/client.go
+++ b/pkg/k8s/client.go
@@ -90,12 +90,12 @@ func newExternalClusterClient(opts Options) (*kubernetes.Clientset, error) {
 	var err error
 
 	if opts.Endpoint == "" {
-		return nil, errors.New("Endpoint missing for external cluster client")
+		return nil, errors.New("endpoint missing for external cluster client")
 	}
 
 	opts.Token, err = utl.GetSecret(opts.Token, opts.TokenFile)
 	if err != nil {
-		return nil, errors.Wrap(err, "Cannot retrieve bearer token")
+		return nil, errors.Wrap(err, "cannot retrieve bearer token")
 	}
 
 	config := &rest.Config{
@@ -106,7 +106,7 @@ func newExternalClusterClient(opts Options) (*kubernetes.Clientset, error) {
 	if opts.CertAuthFilePath != "" {
 		caData, err := os.ReadFile(opts.CertAuthFilePath)
 		if err != nil {
-			return nil, errors.Wrap(err, "Failed to read CA file")
+			return nil, errors.Wrap(err, "failed to read CA file")
 		}
 		config.TLSClientConfig = rest.TLSClientConfig{
 			CAData: caData,
diff --git a/pkg/registry/image.go b/pkg/registry/image.go
index 4a823c47..cd09b8ce 100644
--- a/pkg/registry/image.go
+++ b/pkg/registry/image.go
@@ -29,32 +29,6 @@ type ParseImageOptions struct {
 	HubTpl string
 }
 
-// Name returns the full name representation of an image.
-func (i Image) Name() string {
-	return i.named.Name()
-}
-
-// String returns the string representation of an image.
-func (i Image) String() string {
-	return i.named.String()
-}
-
-// Reference returns either the digest if it is non-empty or the tag for the image.
-func (i Image) Reference() string {
-	if len(i.Digest.String()) > 1 {
-		return i.Digest.String()
-	}
-
-	return i.Tag
-}
-
-// WithDigest sets the digest for an image.
-func (i *Image) WithDigest(digest digest.Digest) (err error) {
-	i.Digest = digest
-	i.named, err = reference.WithDigest(i.named, digest)
-	return err
-}
-
 // ParseImage returns an Image struct with all the values filled in for a given image.
 func ParseImage(parseOpts ParseImageOptions) (Image, error) {
 	// Parse the image name and tag.
@@ -91,7 +65,33 @@ func ParseImage(parseOpts ParseImageOptions) (Image, error) {
 	return i, nil
 }
 
-func (i Image) hubLink() (string, error) {
+// Name returns the full name representation of an image.
+func (i *Image) Name() string {
+	return i.named.Name()
+}
+
+// String returns the string representation of an image.
+func (i *Image) String() string {
+	return i.named.String()
+}
+
+// Reference returns either the digest if it is non-empty or the tag for the image.
+func (i *Image) Reference() string {
+	if len(i.Digest.String()) > 1 {
+		return i.Digest.String()
+	}
+
+	return i.Tag
+}
+
+// WithDigest sets the digest for an image.
+func (i *Image) WithDigest(digest digest.Digest) (err error) {
+	i.Digest = digest
+	i.named, err = reference.WithDigest(i.named, digest)
+	return err
+}
+
+func (i *Image) hubLink() (string, error) {
 	if i.opts.HubTpl != "" {
 		var out bytes.Buffer
 		tmpl, err := template.New("tmpl").
diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go
index 6d32ae8a..7d811d7a 100644
--- a/pkg/registry/manifest.go
+++ b/pkg/registry/manifest.go
@@ -32,13 +32,13 @@ func (c *Client) Manifest(image Image, dbManifest Manifest) (Manifest, bool, err
 
 	rmRef, err := ParseReference(image.String())
 	if err != nil {
-		return Manifest{}, false, errors.Wrap(err, "Cannot parse reference")
+		return Manifest{}, false, errors.Wrap(err, "cannot parse reference")
 	}
 
 	// Retrieve remote digest through HEAD request
 	rmDigest, err := docker.GetDigest(ctx, c.sysCtx, rmRef)
 	if err != nil {
-		return Manifest{}, false, errors.Wrap(err, "Cannot get image digest from HEAD request")
+		return Manifest{}, false, errors.Wrap(err, "cannot get image digest from HEAD request")
 	}
 
 	// Digest match, returns db manifest
@@ -48,13 +48,13 @@ func (c *Client) Manifest(image Image, dbManifest Manifest) (Manifest, bool, err
 
 	rmCloser, err := rmRef.NewImage(ctx, c.sysCtx)
 	if err != nil {
-		return Manifest{}, false, errors.Wrap(err, "Cannot create image closer")
+		return Manifest{}, false, errors.Wrap(err, "cannot create image closer")
 	}
 	defer rmCloser.Close()
 
 	rmRawManifest, rmManifestMimeType, err := rmCloser.Manifest(ctx)
 	if err != nil {
-		return Manifest{}, false, errors.Wrap(err, "Cannot get raw manifest")
+		return Manifest{}, false, errors.Wrap(err, "cannot get raw manifest")
 	}
 
 	// For manifests list compare also digest matching the platform
@@ -62,19 +62,19 @@ func (c *Client) Manifest(image Image, dbManifest Manifest) (Manifest, bool, err
 	if c.opts.CompareDigest && len(dbManifest.Raw) > 0 && dbManifest.isManifestList() && isManifestList(rmManifestMimeType) {
 		dbManifestList, err := manifest.ListFromBlob(dbManifest.Raw, dbManifest.MIMEType)
 		if err != nil {
-			return Manifest{}, false, errors.Wrap(err, "Cannot parse manifest list")
+			return Manifest{}, false, errors.Wrap(err, "cannot parse manifest list")
 		}
 		dbManifestPlatformDigest, err := dbManifestList.ChooseInstance(c.sysCtx)
 		if err != nil {
-			return Manifest{}, false, errors.Wrapf(err, "Error choosing image instance")
+			return Manifest{}, false, errors.Wrapf(err, "error choosing image instance")
 		}
 		rmManifestList, err := manifest.ListFromBlob(rmRawManifest, rmManifestMimeType)
 		if err != nil {
-			return Manifest{}, false, errors.Wrap(err, "Cannot parse manifest list")
+			return Manifest{}, false, errors.Wrap(err, "cannot parse manifest list")
 		}
 		rmManifestPlatformDigest, err := rmManifestList.ChooseInstance(c.sysCtx)
 		if err != nil {
-			return Manifest{}, false, errors.Wrapf(err, "Error choosing image instance")
+			return Manifest{}, false, errors.Wrapf(err, "error choosing image instance")
 		}
 		updated = dbManifestPlatformDigest != rmManifestPlatformDigest
 	}
@@ -82,7 +82,7 @@ func (c *Client) Manifest(image Image, dbManifest Manifest) (Manifest, bool, err
 	// Metadata describing the Docker image
 	rmInspect, err := rmCloser.Inspect(ctx)
 	if err != nil {
-		return Manifest{}, false, errors.Wrap(err, "Cannot inspect")
+		return Manifest{}, false, errors.Wrap(err, "cannot inspect")
 	}
 	rmTag := rmInspect.Tag
 	if len(rmTag) == 0 {
diff --git a/pkg/registry/tags.go b/pkg/registry/tags.go
index 4dd44c61..e8c97668 100644
--- a/pkg/registry/tags.go
+++ b/pkg/registry/tags.go
@@ -1,9 +1,15 @@
 package registry
 
 import (
+	"fmt"
+	"sort"
+	"strings"
+	"unicode"
+
 	"github.com/containers/image/v5/docker"
 	"github.com/crazy-max/diun/v4/pkg/utl"
 	"github.com/pkg/errors"
+	"golang.org/x/mod/semver"
 )
 
 // Tags holds information about image tags.
@@ -30,7 +36,7 @@ func (c *Client) Tags(opts TagsOptions) (*Tags, error) {
 
 	imgRef, err := ParseReference(opts.Image.String())
 	if err != nil {
-		return nil, errors.Wrap(err, "Cannot parse reference")
+		return nil, errors.Wrap(err, "cannot parse reference")
 	}
 
 	tags, err := docker.GetRepositoryTags(ctx, c.sysCtx, imgRef)
@@ -65,3 +71,79 @@ func (c *Client) Tags(opts TagsOptions) (*Tags, error) {
 
 	return res, nil
 }
+
+// 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 {
+			s = strings.TrimLeftFunc(s, func(r rune) bool {
+				return !unicode.IsNumber(r)
+			})
+			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
+			}
+			if c := strings.Count(tags[i], ".") - strings.Count(tags[j], "."); c > 0 {
+				return true
+			} else if c < 0 {
+				return false
+			}
+
+			return strings.Compare(tags[i], tags[j]) < 0
+		})
+		return tags
+	default:
+		return tags
+	}
+}
+
+// SortTag holds sort tag type
+type SortTag string
+
+// SortTag constants
+const (
+	SortTagDefault         = SortTag("default")
+	SortTagReverse         = SortTag("reverse")
+	SortTagLexicographical = SortTag("lexicographical")
+	SortTagSemver          = SortTag("semver")
+)
+
+// SortTagTypes is the list of available sort tag types
+var SortTagTypes = []SortTag{
+	SortTagDefault,
+	SortTagReverse,
+	SortTagLexicographical,
+	SortTagSemver,
+}
+
+// Valid checks sort tag type is valid
+func (st *SortTag) Valid() bool {
+	return st.OneOf(SortTagTypes)
+}
+
+// OneOf checks if sort type is one of the values in the list
+func (st *SortTag) OneOf(stl []SortTag) bool {
+	for _, n := range stl {
+		if n == *st {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/registry/tags_sort.go b/pkg/registry/tags_sort.go
deleted file mode 100644
index 2cd0c62b..00000000
--- a/pkg/registry/tags_sort.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package registry
-
-import (
-	"fmt"
-	"sort"
-	"strings"
-	"unicode"
-
-	"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 {
-			s = strings.TrimLeftFunc(s, func(r rune) bool {
-				return !unicode.IsNumber(r)
-			})
-			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
-			}
-			if c := strings.Count(tags[i], ".") - strings.Count(tags[j], "."); c > 0 {
-				return true
-			} else if c < 0 {
-				return false
-			}
-
-			return strings.Compare(tags[i], tags[j]) < 0
-		})
-		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
-}


Test patch:
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 46ac17c8..1986071e 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -1,11 +1,10 @@
-package config_test
+package config
 
 import (
 	"strings"
 	"testing"
 	"time"
 
-	"github.com/crazy-max/diun/v4/internal/config"
 	"github.com/crazy-max/diun/v4/internal/model"
 	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/crazy-max/diun/v4/pkg/utl"
@@ -18,7 +17,7 @@ func TestLoadFile(t *testing.T) {
 	cases := []struct {
 		name     string
 		cfg      string
-		wantData *config.Config
+		wantData *Config
 		wantErr  bool
 	}{
 		{
@@ -44,7 +43,7 @@ func TestLoadFile(t *testing.T) {
 		{
 			name: "Success",
 			cfg:  "./fixtures/config.test.yml",
-			wantData: &config.Config{
+			wantData: &Config{
 				Db: &model.Db{
 					Path: "diun.db",
 				},
@@ -236,7 +235,7 @@ for <code>{{ .Entry.Manifest.Platform }}</code> platform.
 	}
 	for _, tt := range cases {
 		t.Run(tt.name, func(t *testing.T) {
-			cfg, err := config.Load(tt.cfg)
+			cfg, err := Load(tt.cfg)
 			if tt.wantErr {
 				require.Error(t, err)
 				return
@@ -269,7 +268,7 @@ func TestLoadEnv(t *testing.T) {
 			environ: []string{
 				"DIUN_PROVIDERS_DOCKER=true",
 			},
-			expected: &config.Config{
+			expected: &Config{
 				Db:      (&model.Db{}).GetDefaults(),
 				Watch:   (&model.Watch{}).GetDefaults(),
 				Notif:   nil,
@@ -294,7 +293,7 @@ func TestLoadEnv(t *testing.T) {
 				"DIUN_REGOPTS_0_TIMEOUT=30s",
 				"DIUN_PROVIDERS_DOCKER=true",
 			},
-			expected: &config.Config{
+			expected: &Config{
 				Db:    (&model.Db{}).GetDefaults(),
 				Watch: (&model.Watch{}).GetDefaults(),
 				RegOpts: model.RegOpts{
@@ -324,7 +323,7 @@ func TestLoadEnv(t *testing.T) {
 				"DIUN_NOTIF_TELEGRAM_CHATIDS=8547439,1234567",
 				"DIUN_PROVIDERS_SWARM=true",
 			},
-			expected: &config.Config{
+			expected: &Config{
 				Db:    (&model.Db{}).GetDefaults(),
 				Watch: (&model.Watch{}).GetDefaults(),
 				Notif: &model.Notif{
@@ -350,7 +349,7 @@ func TestLoadEnv(t *testing.T) {
 				"DIUN_NOTIF_SCRIPT_ARGS=-a",
 				"DIUN_PROVIDERS_FILE_DIRECTORY=./fixtures",
 			},
-			expected: &config.Config{
+			expected: &Config{
 				Db:    (&model.Db{}).GetDefaults(),
 				Watch: (&model.Watch{}).GetDefaults(),
 				Notif: &model.Notif{
@@ -381,7 +380,7 @@ func TestLoadEnv(t *testing.T) {
 				}
 			}
 
-			cfg, err := config.Load(tt.cfg)
+			cfg, err := Load(tt.cfg)
 			if tt.wantErr {
 				require.Error(t, err)
 				return
@@ -422,7 +421,7 @@ func TestLoadMixed(t *testing.T) {
 				"DIUN_NOTIF_MAIL_TO=webmaster@foo.com",
 				"DIUN_NOTIF_MAIL_LOCALNAME=foo.com",
 			},
-			expected: &config.Config{
+			expected: &Config{
 				Db:    (&model.Db{}).GetDefaults(),
 				Watch: (&model.Watch{}).GetDefaults(),
 				Notif: &model.Notif{
@@ -467,7 +466,7 @@ for <code>{{ .Entry.Manifest.Platform }}</code> platform.
 				"DIUN_NOTIF_WEBHOOK_METHOD=GET",
 				"DIUN_NOTIF_WEBHOOK_TIMEOUT=1m",
 			},
-			expected: &config.Config{
+			expected: &Config{
 				Db:    (&model.Db{}).GetDefaults(),
 				Watch: (&model.Watch{}).GetDefaults(),
 				Notif: &model.Notif{
@@ -502,7 +501,7 @@ for <code>{{ .Entry.Manifest.Platform }}</code> platform.
 				}
 			}
 
-			cfg, err := config.Load(tt.cfg)
+			cfg, err := Load(tt.cfg)
 			if tt.wantErr {
 				require.Error(t, err)
 				return
@@ -526,7 +525,7 @@ func TestValidation(t *testing.T) {
 	}
 	for _, tt := range cases {
 		t.Run(tt.name, func(t *testing.T) {
-			cfg, err := config.Load(tt.cfg)
+			cfg, err := Load(tt.cfg)
 			require.NoError(t, err)
 			_, err = env.Encode("DIUN_", cfg)
 			require.NoError(t, err)
diff --git a/internal/provider/common_test.go b/internal/provider/common_test.go
index 88822a7a..5166c1e5 100644
--- a/internal/provider/common_test.go
+++ b/internal/provider/common_test.go
@@ -1,10 +1,9 @@
-package provider_test
+package provider
 
 import (
 	"testing"
 
 	"github.com/crazy-max/diun/v4/internal/model"
-	"github.com/crazy-max/diun/v4/internal/provider"
 	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/crazy-max/diun/v4/pkg/utl"
 	"github.com/stretchr/testify/assert"
@@ -19,7 +18,7 @@ func TestValidateImage(t *testing.T) {
 		watchByDef    bool
 		imageDefaults model.Image
 		expectedImage model.Image
-		expectedErr   error
+		expectedErr   interface{}
 	}{
 		// Test strip sha
 		{
@@ -70,7 +69,7 @@ func TestValidateImage(t *testing.T) {
 			expectedErr:   nil,
 		},
 		{
-			name:       "Invlaid diun.enable",
+			name:       "Invalid diun.enable",
 			image:      "myimg",
 			watchByDef: false,
 			labels: map[string]string{
@@ -79,7 +78,7 @@ func TestValidateImage(t *testing.T) {
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: provider.ErrInvalidLabel,
+			expectedErr: &invalidLabelError{},
 		},
 		// Test diun.regopt
 		{
@@ -165,7 +164,7 @@ func TestValidateImage(t *testing.T) {
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: provider.ErrInvalidLabel,
+			expectedErr: &invalidLabelError{},
 		},
 		{
 			name:       "Override default image values with labels (true > false)",
@@ -226,7 +225,7 @@ func TestValidateImage(t *testing.T) {
 				Name:     "myimg",
 				NotifyOn: []model.NotifyOn{},
 			},
-			expectedErr: provider.ErrInvalidLabel,
+			expectedErr: &invalidLabelError{},
 		},
 		{
 			name:       "Set empty notify_on",
@@ -297,7 +296,7 @@ func TestValidateImage(t *testing.T) {
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: provider.ErrInvalidLabel,
+			expectedErr: &invalidLabelError{},
 		},
 		{
 			name:  "Set empty sort_tags",
@@ -368,7 +367,7 @@ func TestValidateImage(t *testing.T) {
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: provider.ErrInvalidLabel,
+			expectedErr: &invalidLabelError{},
 		},
 		{
 			name:  "Set empty max_tags",
@@ -381,7 +380,7 @@ func TestValidateImage(t *testing.T) {
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: provider.ErrInvalidLabel,
+			expectedErr: &invalidLabelError{},
 		},
 		{
 			name:       "Default max_tags",
@@ -679,7 +678,7 @@ func TestValidateImage(t *testing.T) {
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: provider.ErrInvalidLabel,
+			expectedErr: &invalidLabelError{},
 		},
 		{
 			name:  "Set empty platform",
@@ -693,7 +692,7 @@ func TestValidateImage(t *testing.T) {
 				Name:     "myimg",
 				Platform: model.ImagePlatform{},
 			},
-			expectedErr: provider.ErrInvalidLabel,
+			expectedErr: &invalidLabelError{},
 		},
 		{
 			name:       "Default platform",
@@ -769,7 +768,7 @@ func TestValidateImage(t *testing.T) {
 			expectedImage: model.Image{
 				Name: "myimg",
 			},
-			expectedErr: provider.ErrInvalidLabel,
+			expectedErr: &invalidLabelError{},
 		},
 		{
 			name:  "Set empty metadata key",
@@ -856,25 +855,26 @@ func TestValidateImage(t *testing.T) {
 		},
 	}
 
-	for _, c := range cases {
-		c := c
-		t.Run(c.name, func(t *testing.T) {
+	for _, tt := range cases {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
-			actualImg, actualErr := provider.ValidateImage(
-				c.image,
-				c.metadata,
-				c.labels,
-				c.watchByDef,
-				c.imageDefaults,
+			img, err := ValidateImage(
+				tt.image,
+				tt.metadata,
+				tt.labels,
+				tt.watchByDef,
+				tt.imageDefaults,
 			)
-
-			assert.Equal(t, c.expectedImage, actualImg)
-
-			if c.expectedErr == nil {
-				assert.NoError(t, actualErr)
+			if tt.expectedErr == nil {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.expectedImage, img)
 			} else {
-				if assert.Error(t, c.expectedErr) {
-					assert.ErrorIs(t, actualErr, c.expectedErr)
+				switch err.(type) {
+				case *invalidLabelError:
+					assert.Error(t, err)
+				default:
+					assert.Error(t, err)
 				}
 			}
 		})
diff --git a/internal/provider/file/file_test.go b/internal/provider/file/file_test.go
index 400133fa..c1b3a019 100644
--- a/internal/provider/file/file_test.go
+++ b/internal/provider/file/file_test.go
@@ -1,10 +1,9 @@
-package file_test
+package file
 
 import (
 	"testing"
 
 	"github.com/crazy-max/diun/v4/internal/model"
-	"github.com/crazy-max/diun/v4/internal/provider/file"
 	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/crazy-max/diun/v4/pkg/utl"
 	"github.com/stretchr/testify/assert"
@@ -156,7 +155,7 @@ var (
 )
 
 func TestListJobFilename(t *testing.T) {
-	fc := file.New(&model.PrdFile{
+	fc := New(&model.PrdFile{
 		Filename: "./fixtures/dockerhub.yml",
 	}, &defaultImageDefaults)
 
@@ -164,7 +163,7 @@ func TestListJobFilename(t *testing.T) {
 }
 
 func TestListJobDirectory(t *testing.T) {
-	fc := file.New(&model.PrdFile{
+	fc := New(&model.PrdFile{
 		Directory: "./fixtures",
 	}, &defaultImageDefaults)
 
@@ -172,7 +171,7 @@ func TestListJobDirectory(t *testing.T) {
 }
 
 func TestDefaultImageOptions(t *testing.T) {
-	fc := file.New(&model.PrdFile{
+	fc := New(&model.PrdFile{
 		Filename: "./fixtures/dockerhub.yml",
 	}, &model.Image{WatchRepo: utl.NewTrue()})
 
diff --git a/pkg/dockerfile/client_test.go b/pkg/dockerfile/client_test.go
index d7e6ae15..5f20dce7 100644
--- a/pkg/dockerfile/client_test.go
+++ b/pkg/dockerfile/client_test.go
@@ -1,23 +1,22 @@
-package dockerfile_test
+package dockerfile
 
 import (
 	"fmt"
 	"os"
 	"testing"
 
-	"github.com/crazy-max/diun/v4/pkg/dockerfile"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
 var (
-	dc *dockerfile.Client
+	dc *Client
 )
 
 func TestMain(m *testing.M) {
 	var err error
 
-	dc, err = dockerfile.New(dockerfile.Options{
+	dc, err = New(Options{
 		Filename: "./fixtures/valid.Dockerfile",
 	})
 	if err != nil {
@@ -60,7 +59,7 @@ func TestLoadFile(t *testing.T) {
 	}
 	for _, tt := range cases {
 		t.Run(tt.name, func(t *testing.T) {
-			c, err := dockerfile.New(dockerfile.Options{
+			c, err := New(Options{
 				Filename: tt.dfile,
 			})
 			if tt.wantErr {
diff --git a/pkg/dockerfile/image_test.go b/pkg/dockerfile/image_test.go
index a4d909fa..8fb333aa 100644
--- a/pkg/dockerfile/image_test.go
+++ b/pkg/dockerfile/image_test.go
@@ -1,15 +1,14 @@
-package dockerfile_test
+package dockerfile
 
 import (
 	"testing"
 
-	"github.com/crazy-max/diun/v4/pkg/dockerfile"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
 func TestFromImages(t *testing.T) {
-	c, err := dockerfile.New(dockerfile.Options{
+	c, err := New(Options{
 		Filename: "./fixtures/valid.Dockerfile",
 	})
 	require.NoError(t, err)
diff --git a/pkg/registry/image_test.go b/pkg/registry/image_test.go
index 07d8e696..20f20cbf 100644
--- a/pkg/registry/image_test.go
+++ b/pkg/registry/image_test.go
@@ -1,24 +1,23 @@
-package registry_test
+package registry
 
 import (
 	"testing"
 
-	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestParseImage(t *testing.T) {
 	testCases := []struct {
 		desc      string
-		parseOpts registry.ParseImageOptions
-		expected  registry.Image
+		parseOpts ParseImageOptions
+		expected  Image
 	}{
 		{
 			desc: "bintray artifactory-oss",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "jfrog-docker-reg2.bintray.io",
 				Path:   "jfrog/artifactory-oss",
 				Tag:    "4.0.0",
@@ -26,10 +25,10 @@ func TestParseImage(t *testing.T) {
 		},
 		{
 			desc: "bintray xray-server",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "docker.bintray.io/jfrog/xray-server:2.8.6",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "docker.bintray.io",
 				Path:   "jfrog/xray-server",
 				Tag:    "2.8.6",
@@ -37,10 +36,10 @@ func TestParseImage(t *testing.T) {
 		},
 		{
 			desc: "dockerhub alpine",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "alpine",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "docker.io",
 				Path:   "library/alpine",
 				Tag:    "latest",
@@ -48,10 +47,10 @@ func TestParseImage(t *testing.T) {
 		},
 		{
 			desc: "dockerhub crazymax/nextcloud",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "docker.io/crazymax/nextcloud:latest",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "docker.io",
 				Path:   "crazymax/nextcloud",
 				Tag:    "latest",
@@ -59,10 +58,10 @@ func TestParseImage(t *testing.T) {
 		},
 		{
 			desc: "gcr busybox",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "gcr.io/google-containers/busybox:latest",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "gcr.io",
 				Path:   "google-containers/busybox",
 				Tag:    "latest",
@@ -70,10 +69,10 @@ func TestParseImage(t *testing.T) {
 		},
 		{
 			desc: "github ddns-route53",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "docker.pkg.github.com/crazy-max/ddns-route53/ddns-route53:latest",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "docker.pkg.github.com",
 				Path:   "crazy-max/ddns-route53/ddns-route53",
 				Tag:    "latest",
@@ -81,10 +80,10 @@ func TestParseImage(t *testing.T) {
 		},
 		{
 			desc: "gitlab meltano",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "registry.gitlab.com/meltano/meltano",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "registry.gitlab.com",
 				Path:   "meltano/meltano",
 				Tag:    "latest",
@@ -92,10 +91,10 @@ func TestParseImage(t *testing.T) {
 		},
 		{
 			desc: "quay hypercube",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "quay.io/coreos/hyperkube",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "quay.io",
 				Path:   "coreos/hyperkube",
 				Tag:    "latest",
@@ -103,10 +102,10 @@ func TestParseImage(t *testing.T) {
 		},
 		{
 			desc: "ghcr ddns-route53",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "ghcr.io/crazy-max/ddns-route53",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "ghcr.io",
 				Path:   "crazy-max/ddns-route53",
 				Tag:    "latest",
@@ -114,10 +113,10 @@ func TestParseImage(t *testing.T) {
 		},
 		{
 			desc: "ghcr radarr",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "ghcr.io/linuxserver/radarr",
 			},
-			expected: registry.Image{
+			expected: Image{
 				Domain: "ghcr.io",
 				Path:   "linuxserver/radarr",
 				Tag:    "latest",
@@ -128,7 +127,7 @@ func TestParseImage(t *testing.T) {
 	for _, tt := range testCases {
 		tt := tt
 		t.Run(tt.desc, func(t *testing.T) {
-			img, err := registry.ParseImage(tt.parseOpts)
+			img, err := ParseImage(tt.parseOpts)
 			if err != nil {
 				t.Error(err)
 			}
@@ -142,96 +141,96 @@ func TestParseImage(t *testing.T) {
 func TestHubLink(t *testing.T) {
 	testCases := []struct {
 		desc      string
-		parseOpts registry.ParseImageOptions
+		parseOpts ParseImageOptions
 		expected  string
 	}{
 		{
 			desc: "bintray artifactory-oss",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0",
 			},
 			expected: "https://bintray.com/jfrog/reg2/jfrog%3Aartifactory-oss",
 		},
 		{
 			desc: "bintray kubexray",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "jfrog-docker-reg2.bintray.io/kubexray:latest",
 			},
 			expected: "https://bintray.com/jfrog/reg2/kubexray",
 		},
 		{
 			desc: "bintray xray-server",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "docker.bintray.io/jfrog/xray-server:2.8.6",
 			},
 			expected: "https://bintray.com/jfrog/reg2/jfrog%3Axray-server",
 		},
 		{
 			desc: "dockerhub alpine",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "alpine",
 			},
 			expected: "https://hub.docker.com/_/alpine",
 		},
 		{
 			desc: "dockerhub crazymax/nextcloud",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "docker.io/crazymax/nextcloud:latest",
 			},
 			expected: "https://hub.docker.com/r/crazymax/nextcloud",
 		},
 		{
 			desc: "gcr busybox",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "gcr.io/google-containers/busybox:latest",
 			},
 			expected: "https://gcr.io/google-containers/busybox",
 		},
 		{
 			desc: "github ddns-route53",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "docker.pkg.github.com/crazy-max/ddns-route53/ddns-route53:latest",
 			},
 			expected: "https://github.com/crazy-max/ddns-route53/packages",
 		},
 		{
 			desc: "gitlab meltano",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "registry.gitlab.com/meltano/meltano",
 			},
 			expected: "https://gitlab.com/meltano/meltano/container_registry",
 		},
 		{
 			desc: "quay hypercube",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "quay.io/coreos/hyperkube",
 			},
 			expected: "https://quay.io/repository/coreos/hyperkube",
 		},
 		{
 			desc: "ghcr ddns-route53",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "ghcr.io/crazy-max/ddns-route53",
 			},
 			expected: "https://github.com/users/crazy-max/packages/container/package/ddns-route53",
 		},
 		{
 			desc: "ghcr radarr",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "ghcr.io/linuxserver/radarr",
 			},
 			expected: "https://github.com/users/linuxserver/packages/container/package/radarr",
 		},
 		{
 			desc: "redhat etcd",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name: "registry.access.redhat.com/rhel7/etcd",
 			},
 			expected: "https://access.redhat.com/containers/#/registry.access.redhat.com/rhel7/etcd",
 		},
 		{
 			desc: "private",
-			parseOpts: registry.ParseImageOptions{
+			parseOpts: ParseImageOptions{
 				Name:   "myregistry.example.com/an/image:latest",
 				HubTpl: "https://{{ .Domain }}/ui/repos/{{ .Path }}",
 			},
@@ -242,7 +241,7 @@ func TestHubLink(t *testing.T) {
 	for _, tt := range testCases {
 		tt := tt
 		t.Run(tt.desc, func(t *testing.T) {
-			img, err := registry.ParseImage(tt.parseOpts)
+			img, err := ParseImage(tt.parseOpts)
 			if err != nil {
 				t.Error(err)
 			}
diff --git a/pkg/registry/manifest_test.go b/pkg/registry/manifest_test.go
index a0acfe9b..7eb8f407 100644
--- a/pkg/registry/manifest_test.go
+++ b/pkg/registry/manifest_test.go
@@ -1,29 +1,28 @@
-package registry_test
+package registry
 
 import (
 	"testing"
 
-	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestCompareDigest(t *testing.T) {
 	t.Parallel()
-	rc, err := registry.New(registry.Options{
+	rc, err := New(Options{
 		CompareDigest: true,
 	})
 	if err != nil {
 		t.Error(err)
 	}
 
-	img, err := registry.ParseImage(registry.ParseImageOptions{
+	img, err := ParseImage(ParseImageOptions{
 		Name: "crazymax/diun:2.5.0",
 	})
 	if err != nil {
 		t.Error(err)
 	}
 
-	manifest, _, err := rc.Manifest(img, registry.Manifest{
+	manifest, _, err := rc.Manifest(img, Manifest{
 		Name:     "docker.io/crazymax/diun",
 		Tag:      "2.5.0",
 		MIMEType: "application/vnd.docker.distribution.manifest.list.v2+json",
@@ -40,7 +39,7 @@ func TestCompareDigest(t *testing.T) {
 
 func TestManifest(t *testing.T) {
 	t.Parallel()
-	rc, err := registry.New(registry.Options{
+	rc, err := New(Options{
 		CompareDigest: true,
 		ImageOs:       "linux",
 		ImageArch:     "amd64",
@@ -49,14 +48,14 @@ func TestManifest(t *testing.T) {
 		t.Error(err)
 	}
 
-	img, err := registry.ParseImage(registry.ParseImageOptions{
+	img, err := ParseImage(ParseImageOptions{
 		Name: "portainer/portainer-ce:linux-amd64-2.5.1",
 	})
 	if err != nil {
 		t.Error(err)
 	}
 
-	manifest, updated, err := rc.Manifest(img, registry.Manifest{
+	manifest, updated, err := rc.Manifest(img, Manifest{
 		Name:     "docker.io/portainer/portainer-ce",
 		Tag:      "linux-amd64-2.5.1",
 		MIMEType: "application/vnd.docker.distribution.manifest.v2+json",
@@ -101,7 +100,7 @@ func TestManifest(t *testing.T) {
 
 func TestManifestMultiUpdatedPlatform(t *testing.T) {
 	t.Parallel()
-	rc, err := registry.New(registry.Options{
+	rc, err := New(Options{
 		CompareDigest: true,
 		ImageOs:       "linux",
 		ImageArch:     "amd64",
@@ -110,14 +109,14 @@ func TestManifestMultiUpdatedPlatform(t *testing.T) {
 		t.Error(err)
 	}
 
-	img, err := registry.ParseImage(registry.ParseImageOptions{
+	img, err := ParseImage(ParseImageOptions{
 		Name: "mongo:3.6.21",
 	})
 	if err != nil {
 		t.Error(err)
 	}
 
-	manifest, updated, err := rc.Manifest(img, registry.Manifest{
+	manifest, updated, err := rc.Manifest(img, Manifest{
 		Name:     "docker.io/library/mongo",
 		Tag:      "3.6.21",
 		MIMEType: "application/vnd.docker.distribution.manifest.list.v2+json",
@@ -181,7 +180,7 @@ func TestManifestMultiUpdatedPlatform(t *testing.T) {
 
 func TestManifestMultiNotUpdatedPlatform(t *testing.T) {
 	t.Parallel()
-	rc, err := registry.New(registry.Options{
+	rc, err := New(Options{
 		CompareDigest: true,
 		ImageOs:       "linux",
 		ImageArch:     "amd64",
@@ -190,14 +189,14 @@ func TestManifestMultiNotUpdatedPlatform(t *testing.T) {
 		t.Error(err)
 	}
 
-	img, err := registry.ParseImage(registry.ParseImageOptions{
+	img, err := ParseImage(ParseImageOptions{
 		Name: "mongo:3.6.21",
 	})
 	if err != nil {
 		t.Error(err)
 	}
 
-	manifest, updated, err := rc.Manifest(img, registry.Manifest{
+	manifest, updated, err := rc.Manifest(img, Manifest{
 		Name:     "docker.io/library/mongo",
 		Tag:      "3.6.21",
 		MIMEType: "application/vnd.docker.distribution.manifest.list.v2+json",
@@ -261,7 +260,7 @@ func TestManifestMultiNotUpdatedPlatform(t *testing.T) {
 
 func TestManifestVariant(t *testing.T) {
 	t.Parallel()
-	rc, err := registry.New(registry.Options{
+	rc, err := New(Options{
 		ImageOs:      "linux",
 		ImageArch:    "arm",
 		ImageVariant: "v7",
@@ -270,14 +269,14 @@ func TestManifestVariant(t *testing.T) {
 		t.Error(err)
 	}
 
-	img, err := registry.ParseImage(registry.ParseImageOptions{
+	img, err := ParseImage(ParseImageOptions{
 		Name: "crazymax/diun:2.5.0",
 	})
 	if err != nil {
 		t.Error(err)
 	}
 
-	manifest, _, err := rc.Manifest(img, registry.Manifest{})
+	manifest, _, err := rc.Manifest(img, Manifest{})
 	assert.NoError(t, err)
 	assert.Equal(t, "docker.io/crazymax/diun", manifest.Name)
 	assert.Equal(t, "2.5.0", manifest.Tag)
diff --git a/pkg/registry/ref_test.go b/pkg/registry/ref_test.go
index f3fbd7d7..3b4760e8 100644
--- a/pkg/registry/ref_test.go
+++ b/pkg/registry/ref_test.go
@@ -1,9 +1,8 @@
-package registry_test
+package registry
 
 import (
 	"testing"
 
-	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -54,7 +53,7 @@ func TestParseReference(t *testing.T) {
 	for _, tt := range testCases {
 		tt := tt
 		t.Run(tt.input, func(t *testing.T) {
-			ref, err := registry.ParseReference(tt.input)
+			ref, err := ParseReference(tt.input)
 			if tt.wantErr {
 				require.Error(t, err)
 				return
diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go
index 47b1dad8..e5ebb6e1 100644
--- a/pkg/registry/registry_test.go
+++ b/pkg/registry/registry_test.go
@@ -1,21 +1,20 @@
-package registry_test
+package registry
 
 import (
 	"os"
 	"testing"
 
-	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/stretchr/testify/assert"
 )
 
 var (
-	rc *registry.Client
+	rc *Client
 )
 
 func TestMain(m *testing.M) {
 	var err error
 
-	rc, err = registry.New(registry.Options{
+	rc, err = New(Options{
 		ImageOs:   "linux",
 		ImageArch: "amd64",
 	})
diff --git a/pkg/registry/tags_test.go b/pkg/registry/tags_test.go
index 6c2b8d7f..0956d8eb 100644
--- a/pkg/registry/tags_test.go
+++ b/pkg/registry/tags_test.go
@@ -1,23 +1,22 @@
-package registry_test
+package registry
 
 import (
 	"testing"
 
-	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestTags(t *testing.T) {
 	assert.NotNil(t, rc)
 
-	image, err := registry.ParseImage(registry.ParseImageOptions{
+	image, err := ParseImage(ParseImageOptions{
 		Name: "crazymax/diun:3.0.0",
 	})
 	if err != nil {
 		t.Error(err)
 	}
 
-	tags, err := rc.Tags(registry.TagsOptions{
+	tags, err := rc.Tags(TagsOptions{
 		Image: image,
 	})
 	if err != nil {
@@ -31,12 +30,12 @@ func TestTags(t *testing.T) {
 func TestTagsSort(t *testing.T) {
 	testCases := []struct {
 		name     string
-		sortTag  registry.SortTag
+		sortTag  SortTag
 		expected []string
 	}{
 		{
 			name:    "sort default",
-			sortTag: registry.SortTagDefault,
+			sortTag: SortTagDefault,
 			expected: []string{
 				"0.1.0",
 				"0.4.0",
@@ -76,7 +75,7 @@ func TestTagsSort(t *testing.T) {
 		},
 		{
 			name:    "sort lexicographical",
-			sortTag: registry.SortTagLexicographical,
+			sortTag: SortTagLexicographical,
 			expected: []string{
 				"0.1.0",
 				"0.4.0",
@@ -116,7 +115,7 @@ func TestTagsSort(t *testing.T) {
 		},
 		{
 			name:    "sort reverse",
-			sortTag: registry.SortTagReverse,
+			sortTag: SortTagReverse,
 			expected: []string{
 				"latest",
 				"edge",
@@ -156,7 +155,7 @@ func TestTagsSort(t *testing.T) {
 		},
 		{
 			name:    "sort semver",
-			sortTag: registry.SortTagSemver,
+			sortTag: SortTagSemver,
 			expected: []string{
 				"alpine-5.0",
 				"ubuntu-5.0",
@@ -237,7 +236,7 @@ func TestTagsSort(t *testing.T) {
 
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
-			tags := registry.SortTags(repotags, tt.sortTag)
+			tags := SortTags(repotags, tt.sortTag)
 			assert.Equal(t, tt.expected, tags)
 		})
 	}