Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Repository.Manifest() now returns a ManifestStore #300

Merged
merged 3 commits into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions content/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ type Resolver interface {
Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error)
}

// Tagger tags reference tags.
type Tagger interface {
// Tag tags a descriptor with a reference string.
Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error
}

// TagResolver provides reference tag indexing services.
type TagResolver interface {
Tagger
Resolver

// Tag tags a descriptor with a reference string.
Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error
}
12 changes: 11 additions & 1 deletion registry/remote/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func (r *Repository) Blobs() registry.BlobStore {
}

// Manifests provides access to the manifest CAS only.
func (r *Repository) Manifests() registry.BlobStore {
func (r *Repository) Manifests() registry.ManifestStore {
return &manifestStore{repo: r}
}

Expand Down Expand Up @@ -951,6 +951,16 @@ func (s *manifestStore) FetchReference(ctx context.Context, reference string) (d
}
}

// Tag tags a manifest descriptor with a reference string.
func (s *manifestStore) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
return s.repo.Tag(ctx, desc, reference)
}

// PushReference pushes the manifest with a reference tag.
func (s *manifestStore) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
return s.repo.PushReference(ctx, expected, content, reference)
}

// generateDescriptor returns a descriptor generated from the response.
// See the truth table at the top of `repository_test.go`
func (s *manifestStore) generateDescriptor(resp *http.Response, ref registry.Reference, httpMethod string) (ocispec.Descriptor, error) {
Expand Down
137 changes: 137 additions & 0 deletions registry/remote/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3020,6 +3020,143 @@ func Test_ManifestStore_FetchReference(t *testing.T) {
}
}

func Test_ManifestStore_Tag(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
var gotIndex []byte
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+blobDesc.Digest.String():
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodPut &&
r.URL.Path == "/v2/test/manifests/"+ref || r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusForbidden)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}

repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
store := repo.Manifests()
repo.PlainHTTP = true
ctx := context.Background()

err = store.Tag(ctx, blobDesc, ref)
if err == nil {
t.Errorf("Repository.Tag() error = %v, wantErr %v", err, true)
}

err = store.Tag(ctx, indexDesc, ref)
if err != nil {
t.Fatalf("Repository.Tag() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index)
}

gotIndex = nil
err = store.Tag(ctx, indexDesc, indexDesc.Digest.String())
if err != nil {
t.Fatalf("Repository.Tag() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index)
}
}

func Test_ManifestStore_PushReference(t *testing.T) {
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
var gotIndex []byte
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+ref:
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusForbidden)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}

repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
store := repo.Manifests()
repo.PlainHTTP = true
ctx := context.Background()
err = store.PushReference(ctx, indexDesc, bytes.NewReader(index), ref)
if err != nil {
t.Fatalf("Repository.PushReference() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.PushReference() = %v, want %v", gotIndex, index)
}
}

func Test_ManifestStore_generateDescriptorWithVariousDockerContentDigestHeaders(t *testing.T) {
reference := registry.Reference{
Registry: "eastern.haan.com",
Expand Down
16 changes: 13 additions & 3 deletions registry/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,21 @@ import (
// Since a repository is an union of the blob and the manifest CASs, all
// operations defined in the `BlobStore` are executed depending on the media
// type of the given descriptor accordingly.
// Furthurmore, this interface also provides the ability to enforce the
// Furthermore, this interface also provides the ability to enforce the
// separation of the blob and the manifests CASs.
type Repository interface {
BlobStore
content.Storage
content.Deleter
content.TagResolver
ReferenceFetcher
ReferencePusher

// Blobs provides access to the blob CAS only, which contains config blobs,
// layers, and other generic blobs.
Blobs() BlobStore

// Manifests provides access to the manifest CAS only.
Manifests() BlobStore
Manifests() ManifestStore

// Tags lists the tags available in the repository.
// Since the returned tag list may be paginated by the underlying
Expand Down Expand Up @@ -72,6 +74,14 @@ type BlobStore interface {
ReferenceFetcher
}

// ManifestStore is a CAS with the ability to stat and delete its content.
// Besides, ManifestStore provides reference tagging.
type ManifestStore interface {
BlobStore
content.Tagger
ReferencePusher
}

// ReferencePusher provides advanced push with the tag service.
type ReferencePusher interface {
// PushReference pushes the manifest with a reference tag.
Expand Down