summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/actions/spelling/allow.txt6
-rw-r--r--.github/workflows/ci-tests.yaml6
-rw-r--r--pkg/image/options.go16
-rw-r--r--pkg/image/version.go9
-rw-r--r--pkg/registry/client.go93
-rw-r--r--pkg/registry/mocks/RegistryClient.go51
-rw-r--r--pkg/registry/registry.go55
-rw-r--r--pkg/registry/registry_test.go48
-rw-r--r--pkg/tag/tag.go5
9 files changed, 231 insertions, 58 deletions
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt
index ee57cac..94a692d 100644
--- a/.github/actions/spelling/allow.txt
+++ b/.github/actions/spelling/allow.txt
@@ -40,6 +40,7 @@ Ctx
deadcode
Debugf
defaultns
+deserialized
dest
dexidp
dirname
@@ -50,6 +51,7 @@ dst
egrep
endif
ENTRYPOINT
+eps
Errorf
extldflags
Fatalf
@@ -108,6 +110,7 @@ logctx
Logf
loglevel
logrus
+Matchfunc
memcache
metadata
misconfigured
@@ -133,6 +136,7 @@ otherimg
otherparam
othervalue
parametrizable
+params
parseable
patrickmn
pb
@@ -190,6 +194,7 @@ toolchain
Torvalds
Tracef
unmarshal
+unmarshals
unparam
updateable
url
@@ -197,6 +202,7 @@ Useragent
username
usr
varcheck
+versioned
versioning
Warnf
webkit
diff --git a/.github/workflows/ci-tests.yaml b/.github/workflows/ci-tests.yaml
index ae0d057..8dab78e 100644
--- a/.github/workflows/ci-tests.yaml
+++ b/.github/workflows/ci-tests.yaml
@@ -55,9 +55,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: Run golangci-lint
- uses: golangci/golangci-lint-action@v1
+ uses: golangci/golangci-lint-action@v2
with:
- version: v1.26
+ version: v1.32
args: --timeout 5m
test:
name: Ensure unit tests are passing
@@ -75,4 +75,4 @@ jobs:
- name: Upload code coverage information to codecov.io
uses: codecov/codecov-action@v1
with:
- file: coverage.out \ No newline at end of file
+ file: coverage.out
diff --git a/pkg/image/options.go b/pkg/image/options.go
index 75f9cbd..d0b585b 100644
--- a/pkg/image/options.go
+++ b/pkg/image/options.go
@@ -63,20 +63,23 @@ func (img *ContainerImage) GetParameterUpdateStrategy(annotations map[string]str
log.Tracef("No sort option %s found", key)
return VersionSortSemVer
}
+ log.Tracef("found update strategy %s in %s", val, key)
+ return ParseUpdateStrategy(val)
+}
+
+func ParseUpdateStrategy(val string) VersionSortMode {
switch strings.ToLower(val) {
case "semver":
- log.Tracef("Sort option semver in %s", key)
return VersionSortSemVer
case "latest":
- log.Tracef("Sort option date in %s", key)
return VersionSortLatest
case "name":
- log.Tracef("Sort option name in %s", key)
return VersionSortName
default:
- log.Warnf("Unknown sort option in %s: %s -- using semver", key, val)
+ log.Warnf("Unknown sort option %s -- using semver", val)
return VersionSortSemVer
}
+
}
// GetParameterMatch returns the match function and pattern to use for matching
@@ -99,6 +102,11 @@ func (img *ContainerImage) GetParameterMatch(annotations map[string]string) (Mat
}
}
+ return ParseMatchfunc(val)
+}
+
+// ParseMatchfunc returns a matcher function and its argument from given value
+func ParseMatchfunc(val string) (MatchFuncFn, interface{}) {
// The special value "any" doesn't take any parameter
if strings.ToLower(val) == "any" {
return MatchFuncAny, nil
diff --git a/pkg/image/version.go b/pkg/image/version.go
index 3abdc7d..26c3a19 100644
--- a/pkg/image/version.go
+++ b/pkg/image/version.go
@@ -75,10 +75,13 @@ func (img *ContainerImage) GetNewestVersionFromTags(vc *VersionConstraint, tagLi
// The given constraint MUST match a semver constraint
var semverConstraint *semver.Constraints
+ var err error
if vc.SortMode == VersionSortSemVer {
- _, err := semver.NewVersion(img.ImageTag.TagName)
- if err != nil {
- return nil, err
+ if img.ImageTag != nil {
+ _, err := semver.NewVersion(img.ImageTag.TagName)
+ if err != nil {
+ return nil, err
+ }
}
if vc.Constraint != "" {
diff --git a/pkg/registry/client.go b/pkg/registry/client.go
index a5682ba..0e478bc 100644
--- a/pkg/registry/client.go
+++ b/pkg/registry/client.go
@@ -1,16 +1,28 @@
package registry
import (
- "github.com/argoproj-labs/argocd-image-updater/pkg/registry/mocks"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "time"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/log"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/tag"
+
+ "github.com/docker/distribution"
"github.com/docker/distribution/manifest/schema1"
+ "github.com/docker/distribution/manifest/schema2"
"github.com/nokia/docker-registry-client/registry"
)
+// TODO: Check image's architecture and OS
+
// RegistryClient defines the methods we need for querying container registries
type RegistryClient interface {
Tags(nameInRepository string) ([]string, error)
ManifestV1(repository string, reference string) (*schema1.SignedManifest, error)
+ ManifestV2(repository string, reference string) (*schema2.DeserializedManifest, error)
+ TagMetadata(repository string, manifest distribution.Manifest) (*tag.TagInfo, error)
}
type NewRegistryClient func(*RegistryEndpoint, string, string) (RegistryClient, error)
@@ -20,11 +32,6 @@ type registryClient struct {
regClient *registry.Registry
}
-// NewMockClient returns a new mocked RegistryClient
-func NewMockClient(endpoint *RegistryEndpoint, username, password string) (RegistryClient, error) {
- return &mocks.RegistryClient{}, nil
-}
-
// NewClient returns a new RegistryClient for the given endpoint information
func NewClient(endpoint *RegistryEndpoint, username, password string) (RegistryClient, error) {
@@ -55,7 +62,79 @@ func (client *registryClient) Tags(nameInRepository string) ([]string, error) {
return client.regClient.Tags(nameInRepository)
}
-// ManifestV1 returns a signed manifest for a given tag in given repository
+// ManifestV1 returns a signed V1 manifest for a given tag in given repository
func (client *registryClient) ManifestV1(repository string, reference string) (*schema1.SignedManifest, error) {
return client.regClient.ManifestV1(repository, reference)
}
+
+// ManifestV2 returns a deserialized V2 manifest for a given tag in given repository
+func (client *registryClient) ManifestV2(repository string, reference string) (*schema2.DeserializedManifest, error) {
+ return client.regClient.ManifestV2(repository, reference)
+}
+
+// GetTagInfo retrieves metadata for a given manifest of given repository
+func (client *registryClient) TagMetadata(repository string, manifest distribution.Manifest) (*tag.TagInfo, error) {
+ ti := &tag.TagInfo{}
+
+ var info struct {
+ Arch string `json:"architecture"`
+ Created string `json:"created"`
+ OS string `json:"os"`
+ }
+
+ // We support both V1 and V2 manifest schemas. Everything else will trigger
+ // an error.
+ switch deserialized := manifest.(type) {
+
+ case *schema1.SignedManifest:
+ var man schema1.Manifest = deserialized.Manifest
+ if len(man.History) == 0 {
+ return nil, fmt.Errorf("no history information found in schema V1")
+ }
+ if err := json.Unmarshal([]byte(man.History[0].V1Compatibility), &info); err != nil {
+ return nil, err
+ }
+ if createdAt, err := time.Parse(time.RFC3339Nano, info.Created); err != nil {
+ return nil, err
+ } else {
+ ti.CreatedAt = createdAt
+ }
+ return ti, nil
+
+ case *schema2.DeserializedManifest:
+ var man schema2.Manifest = deserialized.Manifest
+
+ // The data we require from a V2 manifest is in a blob that we need to
+ // fetch from the registry.
+ _, err := client.regClient.BlobMetadata(repository, man.Config.Digest)
+ if err != nil {
+ return nil, fmt.Errorf("could not get metadata: %v", err)
+ }
+
+ blobReader, err := client.regClient.DownloadBlob(repository, man.Config.Digest)
+ if err != nil {
+ return nil, err
+ }
+ defer blobReader.Close()
+
+ blobBytes := bytes.Buffer{}
+ n, err := blobBytes.ReadFrom(blobReader)
+ if err != nil {
+ return nil, err
+ }
+
+ log.Tracef("read %d bytes of blob data for %s", n, repository)
+
+ if err := json.Unmarshal(blobBytes.Bytes(), &info); err != nil {
+ return nil, err
+ }
+
+ if ti.CreatedAt, err = time.Parse(time.RFC3339Nano, info.Created); err != nil {
+ return nil, err
+ }
+ return ti, nil
+
+ default:
+ return nil, fmt.Errorf("invalid manifest type")
+ }
+}
diff --git a/pkg/registry/mocks/RegistryClient.go b/pkg/registry/mocks/RegistryClient.go
index ae86cac..6352903 100644
--- a/pkg/registry/mocks/RegistryClient.go
+++ b/pkg/registry/mocks/RegistryClient.go
@@ -3,9 +3,14 @@
package mocks
import (
+ distribution "github.com/docker/distribution"
mock "github.com/stretchr/testify/mock"
schema1 "github.com/docker/distribution/manifest/schema1"
+
+ schema2 "github.com/docker/distribution/manifest/schema2"
+
+ tag "github.com/argoproj-labs/argocd-image-updater/pkg/tag"
)
// RegistryClient is an autogenerated mock type for the RegistryClient type
@@ -36,6 +41,52 @@ func (_m *RegistryClient) ManifestV1(repository string, reference string) (*sche
return r0, r1
}
+// ManifestV2 provides a mock function with given fields: repository, reference
+func (_m *RegistryClient) ManifestV2(repository string, reference string) (*schema2.DeserializedManifest, error) {
+ ret := _m.Called(repository, reference)
+
+ var r0 *schema2.DeserializedManifest
+ if rf, ok := ret.Get(0).(func(string, string) *schema2.DeserializedManifest); ok {
+ r0 = rf(repository, reference)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*schema2.DeserializedManifest)
+ }
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func(string, string) error); ok {
+ r1 = rf(repository, reference)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// TagMetadata provides a mock function with given fields: repository, manifest
+func (_m *RegistryClient) TagMetadata(repository string, manifest distribution.Manifest) (*tag.TagInfo, error) {
+ ret := _m.Called(repository, manifest)
+
+ var r0 *tag.TagInfo
+ if rf, ok := ret.Get(0).(func(string, distribution.Manifest) *tag.TagInfo); ok {
+ r0 = rf(repository, manifest)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*tag.TagInfo)
+ }
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func(string, distribution.Manifest) error); ok {
+ r1 = rf(repository, manifest)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
// Tags provides a mock function with given fields: nameInRepository
func (_m *RegistryClient) Tags(nameInRepository string) ([]string, error) {
ret := _m.Called(nameInRepository)
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index 496e301..d0aaa80 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -6,11 +6,12 @@ package registry
// TODO: Refactor this package and provide mocks for better testing.
import (
- "encoding/json"
"fmt"
"strings"
"time"
+ "github.com/docker/distribution"
+
"github.com/argoproj-labs/argocd-image-updater/pkg/client"
"github.com/argoproj-labs/argocd-image-updater/pkg/image"
"github.com/argoproj-labs/argocd-image-updater/pkg/log"
@@ -76,8 +77,9 @@ func (endpoint *RegistryEndpoint) GetTags(img *image.ContainerImage, regClient R
// Fetch the manifest for the tag -- we need v1, because it contains history
// information that we require.
+ i := 0
for _, tagStr := range tags {
-
+ i += 1
// Look into the cache first and re-use any found item. If GetTag() returns
// an error, we treat it as a cache miss and just go ahead to invalidate
// the entry.
@@ -90,44 +92,35 @@ func (endpoint *RegistryEndpoint) GetTags(img *image.ContainerImage, regClient R
continue
}
- ml, err := regClient.ManifestV1(nameInRegistry, tagStr)
- if err != nil {
- return nil, err
- }
+ log.Tracef("Getting manifest for image %s:%s (operation %d/%d)", nameInRegistry, tagStr, i, len(tags))
- if len(ml.History) < 1 {
- log.Warnf("Could not get creation date for %s: History information missing", img.GetFullNameWithTag())
- continue
+ var ml distribution.Manifest
+ var err error
+
+ // We first try to fetch a V2 manifest, and if that's not available we fall
+ // back to fetching V1 manifest. If that fails also, we just skip this tag.
+ if ml, err = regClient.ManifestV2(nameInRegistry, tagStr); err != nil {
+ log.Debugf("No V2 manifest for %s:%s, fetching V1 (%v)", nameInRegistry, tagStr, err)
+ if ml, err = regClient.ManifestV1(nameInRegistry, tagStr); err != nil {
+ log.Errorf("Error fetching metadata for %s:%s - neither V1 or V2 manifest returned by registry: %v", nameInRegistry, tagStr, err)
+ continue
+ }
}
- var histInfo map[string]interface{}
- err = json.Unmarshal([]byte(ml.History[0].V1Compatibility), &histInfo)
+ // Parse required meta data from the manifest. The metadata contains all
+ // information needed to decide whether to consider this tag or not.
+ ti, err := regClient.TagMetadata(nameInRegistry, ml)
if err != nil {
- log.Warnf("Could not unmarshal history info for %s: %v", img.GetFullNameWithTag(), err)
- continue
+ return nil, err
}
-
- crIf, ok := histInfo["created"]
- if !ok {
- log.Warnf("Incomplete history information for %s: no creation timestamp found", img.GetFullNameWithTag())
+ if ti == nil {
+ log.Debugf("No metadata found for %s:%s", nameInRegistry, tagStr)
continue
}
- crStr, ok := crIf.(string)
- if !ok {
- log.Warnf("Creation timestamp for %s has wrong type - need string, is %T", img.GetFullNameWithTag(), crIf)
- continue
- }
+ log.Tracef("Found date %s", ti.CreatedAt.String())
- // Creation date is stored as RFC3339 timestamp with nanoseconds, i.e. like
- // this: 2017-12-01T23:06:12.607835588Z
- log.Tracef("Found origin creation date for %s: %s", tagStr, crStr)
- crDate, err := time.Parse(time.RFC3339Nano, crStr)
- if err != nil {
- log.Warnf("Could not parse creation timestamp for %s (%s): %v", img.GetFullNameWithTag(), crStr, err)
- continue
- }
- imgTag = tag.NewImageTag(tagStr, crDate)
+ imgTag = tag.NewImageTag(tagStr, ti.CreatedAt)
tagList.Add(imgTag)
endpoint.Cache.SetTag(nameInRegistry, imgTag)
}
diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go
index f49fa96..2b890a5 100644
--- a/pkg/registry/registry_test.go
+++ b/pkg/registry/registry_test.go
@@ -1,12 +1,15 @@
package registry
import (
+ "fmt"
"testing"
"github.com/argoproj-labs/argocd-image-updater/pkg/image"
"github.com/argoproj-labs/argocd-image-updater/pkg/registry/mocks"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/tag"
"github.com/docker/distribution/manifest/schema1"
+ "github.com/docker/distribution/manifest/schema2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -71,7 +74,7 @@ func Test_GetTags(t *testing.T) {
t.Run("Check for correctly returned tags with latest sort", func(t *testing.T) {
ts := "2006-01-02T15:04:05.999999999Z"
- meta := &schema1.SignedManifest{
+ meta1 := &schema1.SignedManifest{
Manifest: schema1.Manifest{
History: []schema1.History{
{
@@ -80,10 +83,15 @@ func Test_GetTags(t *testing.T) {
},
},
}
+ meta2 := &schema2.DeserializedManifest{
+ Manifest: schema2.Manifest{},
+ }
regClient := mocks.RegistryClient{}
regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
- regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta, nil)
+ regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta1, nil)
+ regClient.On("ManifestV2", mock.Anything, mock.Anything).Return(meta2, nil)
+ regClient.On("TagMetadata", mock.Anything, mock.Anything).Return(&tag.TagInfo{}, nil)
ep, err := GetRegistryEndpoint("")
require.NoError(t, err)
@@ -101,15 +109,20 @@ func Test_GetTags(t *testing.T) {
})
t.Run("Check for correct error handling when manifest contains no history", func(t *testing.T) {
- meta := &schema1.SignedManifest{
+ meta1 := &schema1.SignedManifest{
Manifest: schema1.Manifest{
History: []schema1.History{},
},
}
+ meta2 := &schema2.DeserializedManifest{
+ Manifest: schema2.Manifest{},
+ }
regClient := mocks.RegistryClient{}
regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
- regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta, nil)
+ regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta1, nil)
+ regClient.On("ManifestV2", mock.Anything, mock.Anything).Return(meta2, fmt.Errorf("not implemented"))
+ regClient.On("TagMetadata", mock.Anything, mock.Anything).Return(nil, nil)
ep, err := GetRegistryEndpoint("")
require.NoError(t, err)
@@ -126,7 +139,7 @@ func Test_GetTags(t *testing.T) {
})
t.Run("Check for correct error handling when manifest contains invalid history", func(t *testing.T) {
- meta := &schema1.SignedManifest{
+ meta1 := &schema1.SignedManifest{
Manifest: schema1.Manifest{
History: []schema1.History{
{
@@ -135,10 +148,15 @@ func Test_GetTags(t *testing.T) {
},
},
}
+ meta2 := &schema2.DeserializedManifest{
+ Manifest: schema2.Manifest{},
+ }
regClient := mocks.RegistryClient{}
regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
- regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta, nil)
+ regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta1, nil)
+ regClient.On("ManifestV2", mock.Anything, mock.Anything).Return(meta2, fmt.Errorf("not implemented"))
+ regClient.On("TagMetadata", mock.Anything, mock.Anything).Return(nil, nil)
ep, err := GetRegistryEndpoint("")
require.NoError(t, err)
@@ -155,7 +173,7 @@ func Test_GetTags(t *testing.T) {
})
t.Run("Check for correct error handling when manifest contains invalid history", func(t *testing.T) {
- meta := &schema1.SignedManifest{
+ meta1 := &schema1.SignedManifest{
Manifest: schema1.Manifest{
History: []schema1.History{
{
@@ -164,10 +182,15 @@ func Test_GetTags(t *testing.T) {
},
},
}
+ meta2 := &schema2.DeserializedManifest{
+ Manifest: schema2.Manifest{},
+ }
regClient := mocks.RegistryClient{}
regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
- regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta, nil)
+ regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta1, nil)
+ regClient.On("ManifestV2", mock.Anything, mock.Anything).Return(meta2, fmt.Errorf("not implemented"))
+ regClient.On("TagMetadata", mock.Anything, mock.Anything).Return(nil, nil)
ep, err := GetRegistryEndpoint("")
require.NoError(t, err)
@@ -185,7 +208,7 @@ func Test_GetTags(t *testing.T) {
t.Run("Check for correct error handling when time stamp cannot be parsed", func(t *testing.T) {
ts := "invalid"
- meta := &schema1.SignedManifest{
+ meta1 := &schema1.SignedManifest{
Manifest: schema1.Manifest{
History: []schema1.History{
{
@@ -194,10 +217,15 @@ func Test_GetTags(t *testing.T) {
},
},
}
+ meta2 := &schema2.DeserializedManifest{
+ Manifest: schema2.Manifest{},
+ }
regClient := mocks.RegistryClient{}
regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
- regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta, nil)
+ regClient.On("ManifestV1", mock.Anything, mock.Anything).Return(meta1, nil)
+ regClient.On("ManifestV2", mock.Anything, mock.Anything).Return(meta2, fmt.Errorf("not implemented"))
+ regClient.On("TagMetadata", mock.Anything, mock.Anything).Return(nil, nil)
ep, err := GetRegistryEndpoint("")
require.NoError(t, err)
diff --git a/pkg/tag/tag.go b/pkg/tag/tag.go
index 69edc6f..e6798d8 100644
--- a/pkg/tag/tag.go
+++ b/pkg/tag/tag.go
@@ -24,6 +24,11 @@ type ImageTagList struct {
lock *sync.RWMutex
}
+// TagInfo contains information for a tag
+type TagInfo struct {
+ CreatedAt time.Time
+}
+
// SortableImageTagList is just that - a sortable list of ImageTag entries
type SortableImageTagList []*ImageTag