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