github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/registry/client/fetcher.go (about) 1 package client 2 3 import ( 4 "context" 5 "encoding/json" 6 7 "github.com/docker/cli/cli/manifest/types" 8 "github.com/docker/distribution" 9 "github.com/docker/distribution/manifest/manifestlist" 10 "github.com/docker/distribution/manifest/ocischema" 11 "github.com/docker/distribution/manifest/schema2" 12 "github.com/docker/distribution/reference" 13 "github.com/docker/distribution/registry/api/errcode" 14 v2 "github.com/docker/distribution/registry/api/v2" 15 distclient "github.com/docker/distribution/registry/client" 16 "github.com/docker/docker/registry" 17 "github.com/opencontainers/go-digest" 18 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 19 "github.com/pkg/errors" 20 "github.com/sirupsen/logrus" 21 ) 22 23 // fetchManifest pulls a manifest from a registry and returns it. An error 24 // is returned if no manifest is found matching namedRef. 25 func fetchManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (types.ImageManifest, error) { 26 manifest, err := getManifest(ctx, repo, ref) 27 if err != nil { 28 return types.ImageManifest{}, err 29 } 30 31 switch v := manifest.(type) { 32 // Removed Schema 1 support 33 case *schema2.DeserializedManifest: 34 imageManifest, err := pullManifestSchemaV2(ctx, ref, repo, *v) 35 if err != nil { 36 return types.ImageManifest{}, err 37 } 38 return imageManifest, nil 39 case *ocischema.DeserializedManifest: 40 imageManifest, err := pullManifestOCISchema(ctx, ref, repo, *v) 41 if err != nil { 42 return types.ImageManifest{}, err 43 } 44 return imageManifest, nil 45 case *manifestlist.DeserializedManifestList: 46 return types.ImageManifest{}, errors.Errorf("%s is a manifest list", ref) 47 } 48 return types.ImageManifest{}, errors.Errorf("%s is not a manifest", ref) 49 } 50 51 func fetchList(ctx context.Context, repo distribution.Repository, ref reference.Named) ([]types.ImageManifest, error) { 52 manifest, err := getManifest(ctx, repo, ref) 53 if err != nil { 54 return nil, err 55 } 56 57 switch v := manifest.(type) { 58 case *manifestlist.DeserializedManifestList: 59 imageManifests, err := pullManifestList(ctx, ref, repo, *v) 60 if err != nil { 61 return nil, err 62 } 63 return imageManifests, nil 64 default: 65 return nil, errors.Errorf("unsupported manifest format: %v", v) 66 } 67 } 68 69 func getManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (distribution.Manifest, error) { 70 manSvc, err := repo.Manifests(ctx) 71 if err != nil { 72 return nil, err 73 } 74 75 dgst, opts, err := getManifestOptionsFromReference(ref) 76 if err != nil { 77 return nil, errors.Errorf("image manifest for %q does not exist", ref) 78 } 79 return manSvc.Get(ctx, dgst, opts...) 80 } 81 82 func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst schema2.DeserializedManifest) (types.ImageManifest, error) { 83 manifestDesc, err := validateManifestDigest(ref, mfst) 84 if err != nil { 85 return types.ImageManifest{}, err 86 } 87 configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo) 88 if err != nil { 89 return types.ImageManifest{}, err 90 } 91 92 if manifestDesc.Platform == nil { 93 manifestDesc.Platform = &ocispec.Platform{} 94 } 95 96 // Fill in os and architecture fields from config JSON 97 if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil { 98 return types.ImageManifest{}, err 99 } 100 101 return types.NewImageManifest(ref, manifestDesc, &mfst), nil 102 } 103 104 func pullManifestOCISchema(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst ocischema.DeserializedManifest) (types.ImageManifest, error) { 105 manifestDesc, err := validateManifestDigest(ref, mfst) 106 if err != nil { 107 return types.ImageManifest{}, err 108 } 109 configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo) 110 if err != nil { 111 return types.ImageManifest{}, err 112 } 113 114 if manifestDesc.Platform == nil { 115 manifestDesc.Platform = &ocispec.Platform{} 116 } 117 118 // Fill in os and architecture fields from config JSON 119 if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil { 120 return types.ImageManifest{}, err 121 } 122 123 return types.NewOCIImageManifest(ref, manifestDesc, &mfst), nil 124 } 125 126 func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) { 127 blobs := repo.Blobs(ctx) 128 configJSON, err := blobs.Get(ctx, dgst) 129 if err != nil { 130 return nil, err 131 } 132 133 verifier := dgst.Verifier() 134 if _, err := verifier.Write(configJSON); err != nil { 135 return nil, err 136 } 137 if !verifier.Verified() { 138 return nil, errors.Errorf("image config verification failed for digest %s", dgst) 139 } 140 return configJSON, nil 141 } 142 143 // validateManifestDigest computes the manifest digest, and, if pulling by 144 // digest, ensures that it matches the requested digest. 145 func validateManifestDigest(ref reference.Named, mfst distribution.Manifest) (ocispec.Descriptor, error) { 146 mediaType, canonical, err := mfst.Payload() 147 if err != nil { 148 return ocispec.Descriptor{}, err 149 } 150 desc := ocispec.Descriptor{ 151 Digest: digest.FromBytes(canonical), 152 Size: int64(len(canonical)), 153 MediaType: mediaType, 154 } 155 156 // If pull by digest, then verify the manifest digest. 157 if digested, isDigested := ref.(reference.Canonical); isDigested { 158 if digested.Digest() != desc.Digest { 159 err := errors.Errorf("manifest verification failed for digest %s", digested.Digest()) 160 return ocispec.Descriptor{}, err 161 } 162 } 163 164 return desc, nil 165 } 166 167 // pullManifestList handles "manifest lists" which point to various 168 // platform-specific manifests. 169 func pullManifestList(ctx context.Context, ref reference.Named, repo distribution.Repository, mfstList manifestlist.DeserializedManifestList) ([]types.ImageManifest, error) { 170 infos := []types.ImageManifest{} 171 172 if _, err := validateManifestDigest(ref, mfstList); err != nil { 173 return nil, err 174 } 175 176 for _, manifestDescriptor := range mfstList.Manifests { 177 manSvc, err := repo.Manifests(ctx) 178 if err != nil { 179 return nil, err 180 } 181 manifest, err := manSvc.Get(ctx, manifestDescriptor.Digest) 182 if err != nil { 183 return nil, err 184 } 185 186 manifestRef, err := reference.WithDigest(ref, manifestDescriptor.Digest) 187 if err != nil { 188 return nil, err 189 } 190 191 var imageManifest types.ImageManifest 192 switch v := manifest.(type) { 193 case *schema2.DeserializedManifest: 194 imageManifest, err = pullManifestSchemaV2(ctx, manifestRef, repo, *v) 195 case *ocischema.DeserializedManifest: 196 imageManifest, err = pullManifestOCISchema(ctx, manifestRef, repo, *v) 197 default: 198 err = errors.Errorf("unsupported manifest type: %T", manifest) 199 } 200 if err != nil { 201 return nil, err 202 } 203 204 // Replace platform from config 205 imageManifest.Descriptor.Platform = types.OCIPlatform(&manifestDescriptor.Platform) 206 207 infos = append(infos, imageManifest) 208 } 209 return infos, nil 210 } 211 212 func continueOnError(err error) bool { 213 switch v := err.(type) { 214 case errcode.Errors: 215 if len(v) == 0 { 216 return true 217 } 218 return continueOnError(v[0]) 219 case errcode.Error: 220 e := err.(errcode.Error) 221 switch e.Code { 222 case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown: 223 return true 224 } 225 return false 226 case *distclient.UnexpectedHTTPResponseError: 227 return true 228 } 229 return false 230 } 231 232 func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, each func(context.Context, distribution.Repository, reference.Named) (bool, error)) error { 233 endpoints, err := allEndpoints(namedRef, c.insecureRegistry) 234 if err != nil { 235 return err 236 } 237 238 repoInfo, err := registry.ParseRepositoryInfo(namedRef) 239 if err != nil { 240 return err 241 } 242 243 confirmedTLSRegistries := make(map[string]bool) 244 for _, endpoint := range endpoints { 245 if endpoint.Version == registry.APIVersion1 { 246 logrus.Debugf("skipping v1 endpoint %s", endpoint.URL) 247 continue 248 } 249 250 if endpoint.URL.Scheme != "https" { 251 if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS { 252 logrus.Debugf("skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL) 253 continue 254 } 255 } 256 257 if c.insecureRegistry { 258 endpoint.TLSConfig.InsecureSkipVerify = true 259 } 260 repoEndpoint := repositoryEndpoint{endpoint: endpoint, info: repoInfo} 261 repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint) 262 if err != nil { 263 logrus.Debugf("error %s with repo endpoint %+v", err, repoEndpoint) 264 if _, ok := err.(ErrHTTPProto); ok { 265 continue 266 } 267 return err 268 } 269 270 if endpoint.URL.Scheme == "http" && !c.insecureRegistry { 271 logrus.Debugf("skipping non-tls registry endpoint: %s", endpoint.URL) 272 continue 273 } 274 done, err := each(ctx, repo, namedRef) 275 if err != nil { 276 if continueOnError(err) { 277 if endpoint.URL.Scheme == "https" { 278 confirmedTLSRegistries[endpoint.URL.Host] = true 279 } 280 logrus.Debugf("continuing on error (%T) %s", err, err) 281 continue 282 } 283 logrus.Debugf("not continuing on error (%T) %s", err, err) 284 return err 285 } 286 if done { 287 return nil 288 } 289 } 290 return newNotFoundError(namedRef.String()) 291 } 292 293 // allEndpoints returns a list of endpoints ordered by priority (v2, https, v1). 294 func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoint, error) { 295 repoInfo, err := registry.ParseRepositoryInfo(namedRef) 296 if err != nil { 297 return nil, err 298 } 299 300 var serviceOpts registry.ServiceOptions 301 if insecure { 302 logrus.Debugf("allowing insecure registry for: %s", reference.Domain(namedRef)) 303 serviceOpts.InsecureRegistries = []string{reference.Domain(namedRef)} 304 } 305 registryService, err := registry.NewService(serviceOpts) 306 if err != nil { 307 return []registry.APIEndpoint{}, err 308 } 309 endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name)) 310 logrus.Debugf("endpoints for %s: %v", namedRef, endpoints) 311 return endpoints, err 312 } 313 314 func newNotFoundError(ref string) *notFoundError { 315 return ¬FoundError{err: errors.New("no such manifest: " + ref)} 316 } 317 318 type notFoundError struct { 319 err error 320 } 321 322 func (n *notFoundError) Error() string { 323 return n.err.Error() 324 } 325 326 // NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound 327 func (n *notFoundError) NotFound() {}