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