github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/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  	"github.com/opcr-io/oras-go/v2/errdef"
    28  	"github.com/opcr-io/oras-go/v2/registry"
    29  	"github.com/opcr-io/oras-go/v2/registry/remote/auth"
    30  	"github.com/opcr-io/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  // Ping checks whether or not the registry implement Docker Registry API V2 or
    77  // OCI Distribution Specification.
    78  // Ping can be used to check authentication when an auth client is configured.
    79  //
    80  // References:
    81  //   - https://docs.docker.com/registry/spec/api/#base
    82  //   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#api
    83  func (r *Registry) Ping(ctx context.Context) error {
    84  	url := buildRegistryBaseURL(r.PlainHTTP, r.Reference)
    85  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	resp, err := r.client().Do(req)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	defer resp.Body.Close()
    95  
    96  	switch resp.StatusCode {
    97  	case http.StatusOK:
    98  		return nil
    99  	case http.StatusNotFound:
   100  		return errdef.ErrNotFound
   101  	default:
   102  		return errutil.ParseErrorResponse(resp)
   103  	}
   104  }
   105  
   106  // Repositories lists the name of repositories available in the registry.
   107  // See also `RepositoryListPageSize`.
   108  //
   109  // If `last` is NOT empty, the entries in the response start after the
   110  // repo specified by `last`. Otherwise, the response starts from the top
   111  // of the Repositories list.
   112  //
   113  // Reference: https://docs.docker.com/registry/spec/api/#catalog
   114  func (r *Registry) Repositories(ctx context.Context, last string, fn func(repos []string) error) error {
   115  	ctx = auth.AppendScopes(ctx, auth.ScopeRegistryCatalog)
   116  	url := buildRegistryCatalogURL(r.PlainHTTP, r.Reference)
   117  	var err error
   118  	for err == nil {
   119  		url, err = r.repositories(ctx, last, fn, url)
   120  		// clear `last` for subsequent pages
   121  		last = ""
   122  	}
   123  	if err != errNoLink {
   124  		return err
   125  	}
   126  	return nil
   127  }
   128  
   129  // repositories returns a single page of repository list with the next link.
   130  func (r *Registry) repositories(ctx context.Context, last string, fn func(repos []string) error, url string) (string, error) {
   131  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   132  	if err != nil {
   133  		return "", err
   134  	}
   135  	if r.RepositoryListPageSize > 0 || last != "" {
   136  		q := req.URL.Query()
   137  		if r.RepositoryListPageSize > 0 {
   138  			q.Set("n", strconv.Itoa(r.RepositoryListPageSize))
   139  		}
   140  		if last != "" {
   141  			q.Set("last", last)
   142  		}
   143  		req.URL.RawQuery = q.Encode()
   144  	}
   145  	resp, err := r.client().Do(req)
   146  	if err != nil {
   147  		return "", err
   148  	}
   149  	defer resp.Body.Close()
   150  
   151  	if resp.StatusCode != http.StatusOK {
   152  		return "", errutil.ParseErrorResponse(resp)
   153  	}
   154  	var page struct {
   155  		Repositories []string `json:"repositories"`
   156  	}
   157  	lr := limitReader(resp.Body, r.MaxMetadataBytes)
   158  	if err := json.NewDecoder(lr).Decode(&page); err != nil {
   159  		return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
   160  	}
   161  	if err := fn(page.Repositories); err != nil {
   162  		return "", err
   163  	}
   164  
   165  	return parseLink(resp)
   166  }
   167  
   168  // Repository returns a repository reference by the given name.
   169  func (r *Registry) Repository(ctx context.Context, name string) (registry.Repository, error) {
   170  	ref := registry.Reference{
   171  		Registry:   r.Reference.Registry,
   172  		Repository: name,
   173  	}
   174  	return newRepositoryWithOptions(ref, &r.RepositoryOptions)
   175  }