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 }