summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authorMercier Ludovic <ludovic.mercier.lm@gmail.com>2021-09-17 12:19:55 +0200
committerGitHub <noreply@github.com>2021-09-17 12:19:55 +0200
commitab7f7f1cd6fe6d02887e0fe9d1e89bbbbca6a575 (patch)
tree1a54b33a047ab945527580dc99c5a63f10e76682 /pkg
parent9aa295f9992d92a367964a254441addc147cfbda (diff)
feat: Support OCI repositories by using distribution/v3 (#249)
* upgrade registry dependencies to distribution v3 Signed-off-by: ludovicMercier <ludovic.mercier.lm@gmail.com> * upgrade registry dependencies to distribution v3 Signed-off-by: ludovicMercier <ludovic.mercier.lm@gmail.com> * upgrade registry dependencies to distribution v3 Signed-off-by: ludovicMercier <ludovic.mercier.lm@gmail.com> * upgrade registry dependencies to distribution v3 Signed-off-by: ludovicMercier <ludovic.mercier.lm@gmail.com> * upgrade to distribution v3 an implement ocischema Signed-off-by: ludovicMercier <ludovic.mercier.lm@gmail.com> * upgrade to distribution v3 an implement ocischema Signed-off-by: ludovicMercier <ludovic.mercier.lm@gmail.com> * go mod tidy Signed-off-by: ludovicMercier <ludovic.mercier.lm@gmail.com> * lint Signed-off-by: ludovicMercier <ludovic.mercier.lm@gmail.com> * fix broken tests Signed-off-by: ludovicMercier <ludovic.mercier.lm@gmail.com> * Update to changes from master branch Signed-off-by: jannfis <jann@mistrust.net> Co-authored-by: jannfis <jann@mistrust.net>
Diffstat (limited to 'pkg')
-rw-r--r--pkg/argocd/update_test.go29
-rw-r--r--pkg/image/credentials.go4
-rw-r--r--pkg/registry/client.go220
-rw-r--r--pkg/registry/client_test.go82
-rw-r--r--pkg/registry/mocks/RegistryClient.go78
-rw-r--r--pkg/registry/registry.go20
-rw-r--r--pkg/registry/registry_test.go147
7 files changed, 289 insertions, 291 deletions
diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go
index d3b9fdc..6cbe6df 100644
--- a/pkg/argocd/update_test.go
+++ b/pkg/argocd/update_test.go
@@ -33,9 +33,10 @@ func Test_UpdateApplication(t *testing.T) {
t.Run("Test successful update", func(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
- regMock.On("Tags", mock.MatchedBy(func(s string) bool {
+ regMock.On("NewRepository", mock.MatchedBy(func(s string) bool {
return s == "jannfis/foobar"
- })).Return([]string{"1.0.1"}, nil)
+ })).Return(nil)
+ regMock.On("Tags").Return([]string{"1.0.1"}, nil)
return &regMock, nil
}
@@ -91,7 +92,10 @@ func Test_UpdateApplication(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
assert.Equal(t, endpoint.RegistryPrefix, "quay.io")
- regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
+ regMock.On("NewRepository", mock.MatchedBy(func(s string) bool {
+ return s == "jannfis/foobar"
+ })).Return(nil)
+ regMock.On("Tags").Return([]string{"1.0.1"}, nil)
return &regMock, nil
}
@@ -152,9 +156,10 @@ func Test_UpdateApplication(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
assert.Equal(t, endpoint.RegistryPrefix, "quay.io")
- regMock.On("Tags", mock.MatchedBy(func(s string) bool {
+ regMock.On("NewRepository", mock.MatchedBy(func(s string) bool {
return s == "someorg/foobar"
- })).Return([]string{"1.0.1"}, nil)
+ })).Return(nil)
+ regMock.On("Tags").Return([]string{"1.0.1"}, nil)
return &regMock, nil
}
@@ -214,6 +219,7 @@ func Test_UpdateApplication(t *testing.T) {
t.Run("Test successful update when no tag is set in running workload", func(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
}
@@ -271,6 +277,7 @@ func Test_UpdateApplication(t *testing.T) {
regMock := regmock.RegistryClient{}
assert.Equal(t, "myuser", username)
assert.Equal(t, "mypass", password)
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
}
@@ -384,6 +391,7 @@ func Test_UpdateApplication(t *testing.T) {
t.Run("Test skip because of image up-to-date", func(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
}
@@ -439,6 +447,7 @@ func Test_UpdateApplication(t *testing.T) {
t.Run("Test update because of image registry changed", func(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
}
@@ -497,6 +506,7 @@ func Test_UpdateApplication(t *testing.T) {
t.Run("Test not updated because kustomize image is the same", func(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
}
@@ -569,8 +579,9 @@ func Test_UpdateApplication(t *testing.T) {
called := 0
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return([]string{"one", "two", "three", "four"}, nil)
- regMock.On("ManifestV1", mock.Anything).Return(meta[called], nil)
+ regMock.On("Manifest", mock.Anything).Return(meta[called], nil)
called += 1
return &regMock, nil
}
@@ -644,8 +655,9 @@ func Test_UpdateApplication(t *testing.T) {
called := 0
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return([]string{"one", "two", "three", "four"}, nil)
- regMock.On("ManifestV1", mock.Anything).Return(meta[called], nil)
+ regMock.On("Manifest", mock.Anything).Return(meta[called], nil)
called += 1
return &regMock, nil
}
@@ -705,6 +717,7 @@ func Test_UpdateApplication(t *testing.T) {
t.Run("Error - unknown registry", func(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
}
@@ -813,6 +826,7 @@ func Test_UpdateApplication(t *testing.T) {
t.Run("Test error on failure to list tags", func(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return(nil, errors.New("some error"))
return &regMock, nil
}
@@ -868,6 +882,7 @@ func Test_UpdateApplication(t *testing.T) {
t.Run("Test error on improper semver in tag", func(t *testing.T) {
mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
+ regMock.On("NewRepository", mock.Anything).Return(nil)
regMock.On("Tags", mock.Anything).Return([]string{"1.0.0", "1.0.1"}, nil)
return &regMock, nil
}
diff --git a/pkg/image/credentials.go b/pkg/image/credentials.go
index 2eb1062..50c3f37 100644
--- a/pkg/image/credentials.go
+++ b/pkg/image/credentials.go
@@ -229,9 +229,7 @@ func parseDockerConfigJson(registryURL string, jsonSource string) (string, strin
regPrefix = registryURL
}
- if strings.HasSuffix(regPrefix, "/") {
- regPrefix = strings.TrimSuffix(regPrefix, "/")
- }
+ regPrefix = strings.TrimSuffix(regPrefix, "/")
for registry, authConf := range auths {
if !strings.HasPrefix(registry, registryURL) && !strings.HasPrefix(registry, regPrefix) {
diff --git a/pkg/registry/client.go b/pkg/registry/client.go
index b4dc380..83197e2 100644
--- a/pkg/registry/client.go
+++ b/pkg/registry/client.go
@@ -1,41 +1,75 @@
package registry
import (
- "bytes"
+ "context"
"crypto/sha256"
- "crypto/tls"
- "encoding/json"
"fmt"
- "net/http"
- "strings"
- "time"
+
+ "github.com/argoproj/pkg/json"
"github.com/argoproj-labs/argocd-image-updater/pkg/log"
"github.com/argoproj-labs/argocd-image-updater/pkg/metrics"
"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"
+ "github.com/distribution/distribution/v3"
+ "github.com/distribution/distribution/v3/manifest/ocischema"
+ "github.com/distribution/distribution/v3/manifest/schema1"
+ "github.com/distribution/distribution/v3/manifest/schema2"
+ "github.com/distribution/distribution/v3/reference"
+ "github.com/distribution/distribution/v3/registry/client"
+ "github.com/distribution/distribution/v3/registry/client/auth"
+ "github.com/distribution/distribution/v3/registry/client/auth/challenge"
+ "github.com/distribution/distribution/v3/registry/client/transport"
+
+ "github.com/opencontainers/go-digest"
+
"go.uber.org/ratelimit"
+
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
)
// 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)
+ NewRepository(nameInRepository string) error
+ Tags() ([]string, error)
+ Manifest(tagStr string) (distribution.Manifest, error)
+ TagMetadata(manifest distribution.Manifest) (*tag.TagInfo, error)
}
type NewRegistryClient func(*RegistryEndpoint, string, string) (RegistryClient, error)
// Helper type for registry clients
type registryClient struct {
- regClient *registry.Registry
+ regClient distribution.Repository
+ endpoint *RegistryEndpoint
+ creds credentials
+}
+
+// credentials is an implementation of distribution/V3/session struct
+// to mangage registry credentials and token
+type credentials struct {
+ username string
+ password string
+ refreshTokens map[string]string
+}
+
+func (c credentials) Basic(url *url.URL) (string, string) {
+ return c.username, c.password
+}
+
+func (c credentials) RefreshToken(url *url.URL, service string) string {
+ return c.refreshTokens[service]
+}
+
+func (c credentials) SetRefreshToken(realm *url.URL, service, token string) {
+ if c.refreshTokens != nil {
+ c.refreshTokens[service] = token
+ }
}
// rateLimitTransport encapsulates our custom HTTP round tripper with rate
@@ -55,90 +89,84 @@ func (rlt *rateLimitTransport) RoundTrip(r *http.Request) (*http.Response, error
return resp, err
}
-// newRegistry is a wrapper for creating a registry client that is possibly
+// NewRepository is a wrapper for creating a registry client that is possibly
// rate-limited by using a custom HTTP round tripper method.
-func newRegistry(ep *RegistryEndpoint, opts registry.Options) (*registry.Registry, error) {
- url := strings.TrimSuffix(ep.RegistryAPI, "/")
- var transport http.RoundTripper
- if opts.Insecure {
- transport = &http.Transport{
- TLSClientConfig: &tls.Config{
- InsecureSkipVerify: true,
- },
- }
- } else {
- transport = http.DefaultTransport
+func (clt *registryClient) NewRepository(nameInRepository string) error {
+ urlToCall := strings.TrimSuffix(clt.endpoint.RegistryAPI, "/")
+ challengeManager1 := challenge.NewSimpleManager()
+ _, err := ping(challengeManager1, clt.endpoint.RegistryAPI+"/v2/", "")
+ if err != nil {
+ return err
}
- transport = registry.WrapTransport(transport, url, opts)
+ var transport http.RoundTripper = transport.NewTransport(
+ nil, auth.NewAuthorizer(
+ challengeManager1,
+ auth.NewTokenHandler(nil, clt.creds, nameInRepository, "pull")))
rlt := &rateLimitTransport{
- limiter: ep.Limiter,
+ limiter: clt.endpoint.Limiter,
transport: transport,
- endpoint: ep.RegistryAPI,
+ endpoint: clt.endpoint.RegistryAPI,
}
- logf := opts.Logf
- if logf == nil {
- logf = registry.Log
- }
- registry := &registry.Registry{
- URL: url,
- Client: &http.Client{
- Transport: rlt,
- },
- Logf: logf,
+ named, err := reference.WithName(nameInRepository)
+ if err != nil {
+ return err
}
- if opts.DoInitialPing {
- if err := registry.Ping(); err != nil {
- return nil, err
- }
+ clt.regClient, err = client.NewRepository(named, urlToCall, rlt)
+ if err != nil {
+ return err
}
- return registry, nil
-
+ return nil
}
// NewClient returns a new RegistryClient for the given endpoint information
func NewClient(endpoint *RegistryEndpoint, username, password string) (RegistryClient, error) {
-
if username == "" && endpoint.Username != "" {
username = endpoint.Username
}
if password == "" && endpoint.Password != "" {
password = endpoint.Password
}
-
- client, err := newRegistry(endpoint, registry.Options{
- DoInitialPing: endpoint.Ping,
- Logf: registry.Quiet,
- Username: username,
- Password: password,
- Insecure: endpoint.Insecure,
- })
- if err != nil {
- return nil, err
+ creds := credentials{
+ username: username,
+ password: password,
}
return &registryClient{
- regClient: client,
+ creds: creds,
+ endpoint: endpoint,
}, nil
}
// Tags returns a list of tags for given name in repository
-func (client *registryClient) Tags(nameInRepository string) ([]string, error) {
- return client.regClient.Tags(nameInRepository)
-}
-
-// 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)
+func (clt *registryClient) Tags() ([]string, error) {
+ tagService := clt.regClient.Tags(context.Background())
+ tTags, err := tagService.All(context.Background())
+ if err != nil {
+ return nil, err
+ }
+ return tTags, nil
}
-// 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)
+// Manifest returns a Manifest for a given tag in repository
+func (clt *registryClient) Manifest(tagStr string) (distribution.Manifest, error) {
+ manService, err := clt.regClient.Manifests(context.Background())
+ if err != nil {
+ return nil, err
+ }
+ mediaType := []string{ocischema.SchemaVersion.MediaType, schema1.SchemaVersion.MediaType, schema2.SchemaVersion.MediaType}
+ manifest, err := manService.Get(
+ context.Background(),
+ digest.FromString(tagStr),
+ distribution.WithTag(tagStr), distribution.WithManifestMediaTypes(mediaType))
+ if err != nil {
+ return nil, err
+ }
+ return manifest, nil
}
-// GetTagInfo retrieves metadata for a given manifest of given repository
-func (client *registryClient) TagMetadata(repository string, manifest distribution.Manifest) (*tag.TagInfo, error) {
+// TagMetadata retrieves metadata for a given manifest of given repository
+func (client *registryClient) TagMetadata(manifest distribution.Manifest) (*tag.TagInfo, error) {
ti := &tag.TagInfo{}
var info struct {
@@ -146,8 +174,8 @@ func (client *registryClient) TagMetadata(repository string, manifest distributi
Created string `json:"created"`
OS string `json:"os"`
}
-
- // We support both V1 and V2 manifest schemas. Everything else will trigger
+ //
+ // We support both V1,V2 AND OCI manifest schemas. Everything else will trigger
// an error.
switch deserialized := manifest.(type) {
@@ -177,26 +205,37 @@ func (client *registryClient) TagMetadata(repository string, manifest distributi
// 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)
+ blobReader, err := client.regClient.Blobs(context.Background()).Get(context.Background(), man.Config.Digest)
if err != nil {
- return nil, fmt.Errorf("could not get metadata: %v", err)
+ return nil, err
}
- blobReader, err := client.regClient.DownloadBlob(repository, man.Config.Digest)
- if err != nil {
+ if err := json.Unmarshal(blobReader, &info); err != nil {
+ return nil, err
+ }
+
+ if ti.CreatedAt, err = time.Parse(time.RFC3339Nano, info.Created); err != nil {
return nil, err
}
- defer blobReader.Close()
- blobBytes := bytes.Buffer{}
- n, err := blobBytes.ReadFrom(blobReader)
+ _, mBytes, err := manifest.Payload()
if err != nil {
return nil, err
}
+ ti.Digest = sha256.Sum256(mBytes)
+ log.Tracef("v2 SHA digest is %s", fmt.Sprintf("sha256:%x", ti.Digest))
+ return ti, nil
+ case *ocischema.DeserializedManifest:
+ var man ocischema.Manifest = deserialized.Manifest
- log.Tracef("read %d bytes of blob data for %s", n, repository)
+ // The data we require from a V2 manifest is in a blob that we need to
+ // fetch from the registry.
+ blobReader, err := client.regClient.Blobs(context.Background()).Get(context.Background(), man.Config.Digest)
+ if err != nil {
+ return nil, err
+ }
- if err := json.Unmarshal(blobBytes.Bytes(), &info); err != nil {
+ if err := json.Unmarshal(blobReader, &info); err != nil {
return nil, err
}
@@ -209,10 +248,25 @@ func (client *registryClient) TagMetadata(repository string, manifest distributi
return nil, err
}
ti.Digest = sha256.Sum256(mBytes)
- log.Tracef("v2 SHA digest is %s", fmt.Sprintf("sha256:%x", ti.Digest))
+ log.Tracef("oci SHA digest is %s", fmt.Sprintf("sha256:%x", ti.Digest))
return ti, nil
-
default:
return nil, fmt.Errorf("invalid manifest type")
}
}
+
+// Implementation of ping method to intialize the challenge list
+// Without this, tokenHandler and AuthorizationHandler won't work
+func ping(manager challenge.Manager, endpoint, versionHeader string) ([]auth.APIVersion, error) {
+ resp, err := http.Get(endpoint)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if err := manager.AddResponse(resp); err != nil {
+ return nil, err
+ }
+
+ return auth.APIVersions(resp, versionHeader), err
+}
diff --git a/pkg/registry/client_test.go b/pkg/registry/client_test.go
new file mode 100644
index 0000000..7d0c3a0
--- /dev/null
+++ b/pkg/registry/client_test.go
@@ -0,0 +1,82 @@
+package registry
+
+import (
+ "testing"
+
+ "github.com/distribution/distribution/v3/manifest/schema1"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_TagMetadata(t *testing.T) {
+ t.Run("Check for correct error handling when manifest contains no history", func(t *testing.T) {
+ meta1 := &schema1.SignedManifest{
+ Manifest: schema1.Manifest{
+ History: []schema1.History{},
+ },
+ }
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+ client, err := NewClient(ep, "", "")
+ require.NoError(t, err)
+ _, err = client.TagMetadata(meta1)
+ require.Error(t, err)
+ })
+
+ t.Run("Check for correct error handling when manifest contains invalid history", func(t *testing.T) {
+ meta1 := &schema1.SignedManifest{
+ Manifest: schema1.Manifest{
+ History: []schema1.History{
+ {
+ V1Compatibility: `{"created": {"something": "notastring"}}`,
+ },
+ },
+ },
+ }
+
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+ client, err := NewClient(ep, "", "")
+ require.NoError(t, err)
+ _, err = client.TagMetadata(meta1)
+ require.Error(t, err)
+ })
+
+ t.Run("Check for correct error handling when manifest contains invalid history", func(t *testing.T) {
+ meta1 := &schema1.SignedManifest{
+ Manifest: schema1.Manifest{
+ History: []schema1.History{
+ {
+ V1Compatibility: `{"something": "something"}`,
+ },
+ },
+ },
+ }
+
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+ client, err := NewClient(ep, "", "")
+ require.NoError(t, err)
+ _, err = client.TagMetadata(meta1)
+ require.Error(t, err)
+
+ })
+
+ t.Run("Check for correct error handling when time stamp cannot be parsed", func(t *testing.T) {
+ ts := "invalid"
+ meta1 := &schema1.SignedManifest{
+ Manifest: schema1.Manifest{
+ History: []schema1.History{
+ {
+ V1Compatibility: `{"created":"` + ts + `"}`,
+ },
+ },
+ },
+ }
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+ client, err := NewClient(ep, "", "")
+ require.NoError(t, err)
+ _, err = client.TagMetadata(meta1)
+ require.Error(t, err)
+ })
+}
diff --git a/pkg/registry/mocks/RegistryClient.go b/pkg/registry/mocks/RegistryClient.go
index 6352903..c6cdb72 100644
--- a/pkg/registry/mocks/RegistryClient.go
+++ b/pkg/registry/mocks/RegistryClient.go
@@ -3,13 +3,8 @@
package mocks
import (
- distribution "github.com/docker/distribution"
+ distribution "github.com/distribution/distribution/v3"
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"
)
@@ -18,22 +13,21 @@ type RegistryClient struct {
mock.Mock
}
-// ManifestV1 provides a mock function with given fields: repository, reference
-func (_m *RegistryClient) ManifestV1(repository string, reference string) (*schema1.SignedManifest, error) {
- ret := _m.Called(repository, reference)
+func (_m *RegistryClient) TagMetadata(manifest distribution.Manifest) (*tag.TagInfo, error) {
+ ret := _m.Called(manifest)
- var r0 *schema1.SignedManifest
- if rf, ok := ret.Get(0).(func(string, string) *schema1.SignedManifest); ok {
- r0 = rf(repository, reference)
+ var r0 *tag.TagInfo
+ if rf, ok := ret.Get(0).(func(distribution.Manifest) *tag.TagInfo); ok {
+ r0 = rf(manifest)
} else {
if ret.Get(0) != nil {
- r0 = ret.Get(0).(*schema1.SignedManifest)
+ r0 = ret.Get(0).(*tag.TagInfo)
}
}
var r1 error
- if rf, ok := ret.Get(1).(func(string, string) error); ok {
- r1 = rf(repository, reference)
+ if rf, ok := ret.Get(1).(func(distribution.Manifest) error); ok {
+ r1 = rf(manifest)
} else {
r1 = ret.Error(1)
}
@@ -41,22 +35,21 @@ 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)
+// Tags provides a mock function with given fields: nameInRepository
+func (_m *RegistryClient) Tags() ([]string, error) {
+ ret := _m.Called()
+ var r0 []string
+ if rf, ok := ret.Get(0).(func() []string); ok {
+ r0 = rf()
} else {
if ret.Get(0) != nil {
- r0 = ret.Get(0).(*schema2.DeserializedManifest)
+ r0 = ret.Get(0).([]string)
}
}
var r1 error
- if rf, ok := ret.Get(1).(func(string, string) error); ok {
- r1 = rf(repository, reference)
+ if rf, ok := ret.Get(1).(func() error); ok {
+ r1 = rf()
} else {
r1 = ret.Error(1)
}
@@ -64,22 +57,21 @@ func (_m *RegistryClient) ManifestV2(repository string, reference string) (*sche
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)
+func (_m *RegistryClient) Manifest(tagStr string) (distribution.Manifest, error) {
+ ret := _m.Called(tagStr)
- var r0 *tag.TagInfo
- if rf, ok := ret.Get(0).(func(string, distribution.Manifest) *tag.TagInfo); ok {
- r0 = rf(repository, manifest)
+ var r0 distribution.Manifest
+ if rf, ok := ret.Get(0).(func(string) distribution.Manifest); ok {
+ r0 = rf(tagStr)
} else {
if ret.Get(0) != nil {
- r0 = ret.Get(0).(*tag.TagInfo)
+ r0 = ret.Get(0).(distribution.Manifest)
}
}
var r1 error
- if rf, ok := ret.Get(1).(func(string, distribution.Manifest) error); ok {
- r1 = rf(repository, manifest)
+ if rf, ok := ret.Get(1).(func(string) error); ok {
+ r1 = rf(tagStr)
} else {
r1 = ret.Error(1)
}
@@ -87,25 +79,15 @@ func (_m *RegistryClient) TagMetadata(repository string, manifest distribution.M
return r0, r1
}
-// Tags provides a mock function with given fields: nameInRepository
-func (_m *RegistryClient) Tags(nameInRepository string) ([]string, error) {
+func (_m *RegistryClient) NewRepository(nameInRepository string) (error){
ret := _m.Called(nameInRepository)
- var r0 []string
- if rf, ok := ret.Get(0).(func(string) []string); ok {
- r0 = rf(nameInRepository)
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).([]string)
- }
- }
-
var r1 error
- if rf, ok := ret.Get(1).(func(string) error); ok {
+ if rf, ok := ret.Get(0).(func(string) error); ok {
r1 = rf(nameInRepository)
} else {
- r1 = ret.Error(1)
+ r1 = ret.Error(0)
}
- return r0, r1
+ return r1
}
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index e39f0d3..c26fe2a 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -12,7 +12,8 @@ import (
"sync"
"time"
- "github.com/docker/distribution"
+ "github.com/distribution/distribution/v3"
+
"golang.org/x/sync/semaphore"
"github.com/argoproj-labs/argocd-image-updater/pkg/image"
@@ -39,7 +40,11 @@ func (endpoint *RegistryEndpoint) GetTags(img *image.ContainerImage, regClient R
} else {
nameInRegistry = img.ImageName
}
- tTags, err := regClient.Tags(nameInRegistry)
+ err = regClient.NewRepository(nameInRegistry)
+ if err != nil {
+ return nil, err
+ }
+ tTags, err := regClient.Tags()
if err != nil {
return nil, err
}
@@ -131,17 +136,14 @@ func (endpoint *RegistryEndpoint) GetTags(img *image.ContainerImage, regClient R
// 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)
- return
- }
+ if ml, err = regClient.Manifest(tagStr); err != nil {
+ log.Errorf("Error fetching metadata for %s:%s - neither V1 or V2 or OCI manifest returned by registry: %v", nameInRegistry, tagStr, err)
+ return
}
// 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)
+ ti, err := regClient.TagMetadata(ml)
if err != nil {
log.Errorf("error fetching metadata for %s:%s: %v", nameInRegistry, tagStr, err)
return
diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go
index bf52748..fb61077 100644
--- a/pkg/registry/registry_test.go
+++ b/pkg/registry/registry_test.go
@@ -1,7 +1,6 @@
package registry
import (
- "fmt"
"os"
"testing"
"time"
@@ -10,8 +9,7 @@ import (
"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/distribution/distribution/v3/manifest/schema1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -21,6 +19,7 @@ func Test_GetTags(t *testing.T) {
t.Run("Check for correctly returned tags with semver sort", func(t *testing.T) {
regClient := mocks.RegistryClient{}
+ regClient.On("NewRepository", mock.Anything).Return(nil)
regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
ep, err := GetRegistryEndpoint("")
@@ -39,6 +38,7 @@ func Test_GetTags(t *testing.T) {
t.Run("Check for correctly returned tags with filter function applied", func(t *testing.T) {
regClient := mocks.RegistryClient{}
+ regClient.On("NewRepository", mock.Anything).Return(nil)
regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
ep, err := GetRegistryEndpoint("")
@@ -58,6 +58,7 @@ func Test_GetTags(t *testing.T) {
t.Run("Check for correctly returned tags with name sort", func(t *testing.T) {
regClient := mocks.RegistryClient{}
+ regClient.On("NewRepository", mock.Anything).Return(nil)
regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
ep, err := GetRegistryEndpoint("")
@@ -85,14 +86,11 @@ func Test_GetTags(t *testing.T) {
},
},
}
- meta2 := &schema2.DeserializedManifest{
- Manifest: schema2.Manifest{},
- }
regClient := mocks.RegistryClient{}
+ regClient.On("NewRepository", mock.Anything).Return(nil)
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(meta1, nil)
- regClient.On("ManifestV2", mock.Anything, mock.Anything).Return(meta2, nil)
+ regClient.On("Manifest", mock.Anything, mock.Anything).Return(meta1, nil)
regClient.On("TagMetadata", mock.Anything, mock.Anything).Return(&tag.TagInfo{}, nil)
ep, err := GetRegistryEndpoint("")
@@ -110,139 +108,6 @@ func Test_GetTags(t *testing.T) {
require.Equal(t, "1.2.1", tag.TagName)
})
- t.Run("Check for correct error handling when manifest contains no history", func(t *testing.T) {
- 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(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)
- ep.Cache.ClearCache()
-
- img := image.NewFromIdentifier("foo/bar:1.2.0")
- tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{SortMode: image.VersionSortLatest})
- require.NoError(t, err)
- assert.Empty(t, tl.Tags())
-
- tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
- require.NoError(t, err)
- require.Nil(t, tag)
- })
-
- t.Run("Check for correct error handling when manifest contains invalid history", func(t *testing.T) {
- meta1 := &schema1.SignedManifest{
- Manifest: schema1.Manifest{
- History: []schema1.History{
- {
- V1Compatibility: `{"created": {"something": "notastring"}}`,
- },
- },
- },
- }
- 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(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)
- ep.Cache.ClearCache()
-
- img := image.NewFromIdentifier("foo/bar:1.2.0")
- tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{SortMode: image.VersionSortLatest})
- require.NoError(t, err)
- assert.Empty(t, tl.Tags())
-
- tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
- require.NoError(t, err)
- require.Nil(t, tag)
- })
-
- t.Run("Check for correct error handling when manifest contains invalid history", func(t *testing.T) {
- meta1 := &schema1.SignedManifest{
- Manifest: schema1.Manifest{
- History: []schema1.History{
- {
- V1Compatibility: `{"something": "something"}`,
- },
- },
- },
- }
- 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(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)
- ep.Cache.ClearCache()
-
- img := image.NewFromIdentifier("foo/bar:1.2.0")
- tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{SortMode: image.VersionSortLatest})
- require.NoError(t, err)
- assert.Empty(t, tl.Tags())
-
- tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
- require.NoError(t, err)
- require.Nil(t, tag)
- })
-
- t.Run("Check for correct error handling when time stamp cannot be parsed", func(t *testing.T) {
- ts := "invalid"
- meta1 := &schema1.SignedManifest{
- Manifest: schema1.Manifest{
- History: []schema1.History{
- {
- V1Compatibility: `{"created":"` + ts + `"}`,
- },
- },
- },
- }
- 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(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)
- ep.Cache.ClearCache()
-
- img := image.NewFromIdentifier("foo/bar:1.2.0")
- tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{SortMode: image.VersionSortLatest})
- require.NoError(t, err)
- assert.Empty(t, tl.Tags())
-
- tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
- require.NoError(t, err)
- require.Nil(t, tag)
- })
-
}
func Test_ExpireCredentials(t *testing.T) {