diff options
| author | Mercier Ludovic <ludovic.mercier.lm@gmail.com> | 2021-09-17 12:19:55 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-09-17 12:19:55 +0200 |
| commit | ab7f7f1cd6fe6d02887e0fe9d1e89bbbbca6a575 (patch) | |
| tree | 1a54b33a047ab945527580dc99c5a63f10e76682 /pkg | |
| parent | 9aa295f9992d92a367964a254441addc147cfbda (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.go | 29 | ||||
| -rw-r--r-- | pkg/image/credentials.go | 4 | ||||
| -rw-r--r-- | pkg/registry/client.go | 220 | ||||
| -rw-r--r-- | pkg/registry/client_test.go | 82 | ||||
| -rw-r--r-- | pkg/registry/mocks/RegistryClient.go | 78 | ||||
| -rw-r--r-- | pkg/registry/registry.go | 20 | ||||
| -rw-r--r-- | pkg/registry/registry_test.go | 147 |
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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 := ®istry.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 ®istryClient{ - 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, ®Client, &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, ®Client, &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, ®Client, &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, ®Client, &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) { |
