oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/registry/remote/registry.go (about)

     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  
    16  // Package remote provides a client to the remote registry.
    17  // Reference: https://github.com/distribution/distribution
    18  package remote
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"net/http"
    25  	"strconv"
    26  
    27  	"oras.land/oras-go/v2/errdef"
    28  	"oras.land/oras-go/v2/registry"
    29  	"oras.land/oras-go/v2/registry/remote/auth"
    30  	"oras.land/oras-go/v2/registry/remote/internal/errutil"
    31  )
    32  
    33  // RepositoryOptions is an alias of Repository to avoid name conflicts.
    34  // It also hides all methods associated with Repository.
    35  type RepositoryOptions Repository
    36  
    37  // Registry is an HTTP client to a remote registry.
    38  type Registry struct {
    39  	// RepositoryOptions contains common options for Registry and Repository.
    40  	// It is also used as a template for derived repositories.
    41  	RepositoryOptions
    42  
    43  	// RepositoryListPageSize specifies the page size when invoking the catalog
    44  	// API.
    45  	// If zero, the page size is determined by the remote registry.
    46  	// Reference: https://docs.docker.com/registry/spec/api/#catalog
    47  	RepositoryListPageSize int
    48  }
    49  
    50  // NewRegistry creates a client to the remote registry with the specified domain
    51  // name.
    52  // Example: localhost:5000
    53  func NewRegistry(name string) (*Registry, error) {
    54  	ref := registry.Reference{
    55  		Registry: name,
    56  	}
    57  	if err := ref.ValidateRegistry(); err != nil {
    58  		return nil, err
    59  	}
    60  	return &Registry{
    61  		RepositoryOptions: RepositoryOptions{
    62  			Reference: ref,
    63  		},
    64  	}, nil
    65  }
    66  
    67  // client returns an HTTP client used to access the remote registry.
    68  // A default HTTP client is return if the client is not configured.
    69  func (r *Registry) client() Client {
    70  	if r.Client == nil {
    71  		return auth.DefaultClient
    72  	}
    73  	return r.Client
    74  }
    75  
    76  // do sends an HTTP request and returns an HTTP response using the HTTP client
    77  // returned by r.client().
    78  func (r *Registry) do(req *http.Request) (*http.Response, error) {
    79  	if r.HandleWarning == nil {
    80  		return r.client().Do(req)
    81  	}
    82  
    83  	resp, err := r.client().Do(req)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	handleWarningHeaders(resp.Header.Values(headerWarning), r.HandleWarning)
    88  	return resp, nil
    89  }
    90  
    91  // Ping checks whether or not the registry implement Docker Registry API V2 or
    92  // OCI Distribution Specification.
    93  // Ping can be used to check authentication when an auth client is configured.
    94  //
    95  // References:
    96  //   - https://docs.docker.com/registry/spec/api/#base
    97  //   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#api
    98  func (r *Registry) Ping(ctx context.Context) error {
    99  	url := buildRegistryBaseURL(r.PlainHTTP, r.Reference)
   100  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	resp, err := r.do(req)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	defer resp.Body.Close()
   110  
   111  	switch resp.StatusCode {
   112  	case http.StatusOK:
   113  		return nil
   114  	case http.StatusNotFound:
   115  		return errdef.ErrNotFound
   116  	default:
   117  		return errutil.ParseErrorResponse(resp)
   118  	}
   119  }
   120  
   121  // Repositories lists the name of repositories available in the registry.
   122  // See also `RepositoryListPageSize`.
   123  //
   124  // If `last` is NOT empty, the entries in the response start after the
   125  // repo specified by `last`. Otherwise, the response starts from the top
   126  // of the Repositories list.
   127  //
   128  // Reference: https://docs.docker.com/registry/spec/api/#catalog
   129  func (r *Registry) Repositories(ctx context.Context, last string, fn func(repos []string) error) error {
   130  	ctx = auth.AppendScopesForHost(ctx, r.Reference.Host(), auth.ScopeRegistryCatalog)
   131  	url := buildRegistryCatalogURL(r.PlainHTTP, r.Reference)
   132  	var err error
   133  	for err == nil {
   134  		url, err = r.repositories(ctx, last, fn, url)
   135  		// clear `last` for subsequent pages
   136  		last = ""
   137  	}
   138  	if err != errNoLink {
   139  		return err
   140  	}
   141  	return nil
   142  }
   143  
   144  // repositories returns a single page of repository list with the next link.
   145  func (r *Registry) repositories(ctx context.Context, last string, fn func(repos []string) error, url string) (string, error) {
   146  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   147  	if err != nil {
   148  		return "", err
   149  	}
   150  	if r.RepositoryListPageSize > 0 || last != "" {
   151  		q := req.URL.Query()
   152  		if r.RepositoryListPageSize > 0 {
   153  			q.Set("n", strconv.Itoa(r.RepositoryListPageSize))
   154  		}
   155  		if last != "" {
   156  			q.Set("last", last)
   157  		}
   158  		req.URL.RawQuery = q.Encode()
   159  	}
   160  	resp, err := r.do(req)
   161  	if err != nil {
   162  		return "", err
   163  	}
   164  	defer resp.Body.Close()
   165  
   166  	if resp.StatusCode != http.StatusOK {
   167  		return "", errutil.ParseErrorResponse(resp)
   168  	}
   169  	var page struct {
   170  		Repositories []string `json:"repositories"`
   171  	}
   172  	lr := limitReader(resp.Body, r.MaxMetadataBytes)
   173  	if err := json.NewDecoder(lr).Decode(&page); err != nil {
   174  		return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
   175  	}
   176  	if err := fn(page.Repositories); err != nil {
   177  		return "", err
   178  	}
   179  
   180  	return parseLink(resp)
   181  }
   182  
   183  // Repository returns a repository reference by the given name.
   184  func (r *Registry) Repository(ctx context.Context, name string) (registry.Repository, error) {
   185  	ref := registry.Reference{
   186  		Registry:   r.Reference.Registry,
   187  		Repository: name,
   188  	}
   189  	return newRepositoryWithOptions(ref, &r.RepositoryOptions)
   190  }