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 }