...

Source file src/oras.land/oras-go/pkg/registry/remote/repository.go

Documentation: oras.land/oras-go/pkg/registry/remote

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  package remote
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"net/http"
    22  	"strconv"
    23  
    24  	errdef "oras.land/oras-go/pkg/content"
    25  	"oras.land/oras-go/pkg/registry"
    26  	"oras.land/oras-go/pkg/registry/remote/auth"
    27  	"oras.land/oras-go/pkg/registry/remote/internal/errutil"
    28  )
    29  
    30  // Client is an interface for a HTTP client.
    31  type Client interface {
    32  	// Do sends an HTTP request and returns an HTTP response.
    33  	//
    34  	// Unlike http.RoundTripper, Client can attempt to interpret the response
    35  	// and handle higher-level protocol details such as redirects and
    36  	// authentication.
    37  	//
    38  	// Like http.RoundTripper, Client should not modify the request, and must
    39  	// always close the request body.
    40  	Do(*http.Request) (*http.Response, error)
    41  }
    42  
    43  // Repository is an HTTP client to a remote repository.
    44  type Repository struct {
    45  	// Client is the underlying HTTP client used to access the remote registry.
    46  	// If nil, auth.DefaultClient is used.
    47  	Client Client
    48  
    49  	// Reference references the remote repository.
    50  	Reference registry.Reference
    51  
    52  	// PlainHTTP signals the transport to access the remote repository via HTTP
    53  	// instead of HTTPS.
    54  	PlainHTTP bool
    55  
    56  	// ManifestMediaTypes is used in `Accept` header for resolving manifests from
    57  	// references. It is also used in identifying manifests and blobs from
    58  	// descriptors.
    59  	// If an empty list is present, default manifest media types are used.
    60  	ManifestMediaTypes []string
    61  
    62  	// TagListPageSize specifies the page size when invoking the tag list API.
    63  	// If zero, the page size is determined by the remote registry.
    64  	// Reference: https://docs.docker.com/registry/spec/api/#tags
    65  	TagListPageSize int
    66  
    67  	// ReferrerListPageSize specifies the page size when invoking the Referrers
    68  	// API.
    69  	// If zero, the page size is determined by the remote registry.
    70  	// Reference: https://github.com/oras-project/artifacts-spec/blob/main/manifest-referrers-api.md
    71  	ReferrerListPageSize int
    72  
    73  	// MaxMetadataBytes specifies a limit on how many response bytes are allowed
    74  	// in the server's response to the metadata APIs, such as catalog list, tag
    75  	// list, and referrers list.
    76  	// If zero, a default (currently 4MiB) is used.
    77  	MaxMetadataBytes int64
    78  }
    79  
    80  // NewRepository creates a client to the remote repository identified by a
    81  // reference.
    82  // Example: localhost:5000/hello-world
    83  func NewRepository(reference string) (*Repository, error) {
    84  	ref, err := registry.ParseReference(reference)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	return &Repository{
    89  		Reference: ref,
    90  	}, nil
    91  }
    92  
    93  // client returns an HTTP client used to access the remote repository.
    94  // A default HTTP client is return if the client is not configured.
    95  func (r *Repository) client() Client {
    96  	if r.Client == nil {
    97  		return auth.DefaultClient
    98  	}
    99  	return r.Client
   100  }
   101  
   102  // parseReference validates the reference.
   103  // Both simplified or fully qualified references are accepted as input.
   104  // A fully qualified reference is returned on success.
   105  func (r *Repository) parseReference(reference string) (registry.Reference, error) {
   106  	ref, err := registry.ParseReference(reference)
   107  	if err != nil {
   108  		ref = registry.Reference{
   109  			Registry:   r.Reference.Registry,
   110  			Repository: r.Reference.Repository,
   111  			Reference:  reference,
   112  		}
   113  		if err = ref.ValidateReference(); err != nil {
   114  			return registry.Reference{}, err
   115  		}
   116  		return ref, nil
   117  	}
   118  	if ref.Registry == r.Reference.Registry && ref.Repository == r.Reference.Repository {
   119  		return ref, nil
   120  	}
   121  	return registry.Reference{}, fmt.Errorf("%w %q: expect %q", errdef.ErrInvalidReference, ref, r.Reference)
   122  }
   123  
   124  // Tags lists the tags available in the repository.
   125  func (r *Repository) Tags(ctx context.Context, fn func(tags []string) error) error {
   126  	ctx = withScopeHint(ctx, r.Reference, auth.ActionPull)
   127  	url := buildRepositoryTagListURL(r.PlainHTTP, r.Reference)
   128  	var err error
   129  	for err == nil {
   130  		url, err = r.tags(ctx, fn, url)
   131  	}
   132  	if err != errNoLink {
   133  		return err
   134  	}
   135  	return nil
   136  }
   137  
   138  // tags returns a single page of tag list with the next link.
   139  func (r *Repository) tags(ctx context.Context, fn func(tags []string) error, url string) (string, error) {
   140  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   141  	if err != nil {
   142  		return "", err
   143  	}
   144  	if r.TagListPageSize > 0 {
   145  		q := req.URL.Query()
   146  		q.Set("n", strconv.Itoa(r.TagListPageSize))
   147  		req.URL.RawQuery = q.Encode()
   148  	}
   149  
   150  	resp, err := r.client().Do(req)
   151  	if err != nil {
   152  		return "", err
   153  	}
   154  	defer resp.Body.Close()
   155  
   156  	if resp.StatusCode != http.StatusOK {
   157  		return "", errutil.ParseErrorResponse(resp)
   158  	}
   159  	var page struct {
   160  		Tags []string `json:"tags"`
   161  	}
   162  	lr := limitReader(resp.Body, r.MaxMetadataBytes)
   163  	if err := json.NewDecoder(lr).Decode(&page); err != nil {
   164  		return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
   165  	}
   166  	if err := fn(page.Tags); err != nil {
   167  		return "", err
   168  	}
   169  
   170  	return parseLink(resp)
   171  }
   172  

View as plain text