mirror of
https://github.com/crazy-max/diun.git
synced 2026-01-04 20:15:01 +01:00
@@ -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
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user