github.com/containerd/nerdctl@v1.7.7/pkg/imgutil/imgutil.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package imgutil 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "reflect" 25 26 "github.com/containerd/containerd" 27 "github.com/containerd/containerd/content" 28 "github.com/containerd/containerd/images" 29 "github.com/containerd/containerd/remotes" 30 "github.com/containerd/containerd/snapshots" 31 "github.com/containerd/imgcrypt" 32 "github.com/containerd/imgcrypt/images/encryption" 33 "github.com/containerd/log" 34 "github.com/containerd/nerdctl/pkg/errutil" 35 "github.com/containerd/nerdctl/pkg/idutil/imagewalker" 36 "github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver" 37 "github.com/containerd/nerdctl/pkg/imgutil/pull" 38 "github.com/containerd/platforms" 39 refdocker "github.com/distribution/reference" 40 "github.com/docker/docker/errdefs" 41 "github.com/opencontainers/image-spec/identity" 42 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 43 ) 44 45 // EnsuredImage contains the image existed in containerd and its metadata. 46 type EnsuredImage struct { 47 Ref string 48 Image containerd.Image 49 ImageConfig ocispec.ImageConfig 50 Snapshotter string 51 Remote bool // true for stargz or overlaybd 52 } 53 54 // PullMode is either one of "always", "missing", "never" 55 type PullMode = string 56 57 // GetExistingImage returns the specified image if exists in containerd. Return errdefs.NotFound() if not exists. 58 func GetExistingImage(ctx context.Context, client *containerd.Client, snapshotter, rawRef string, platform ocispec.Platform) (*EnsuredImage, error) { 59 var res *EnsuredImage 60 imagewalker := &imagewalker.ImageWalker{ 61 Client: client, 62 OnFound: func(ctx context.Context, found imagewalker.Found) error { 63 if res != nil { 64 return nil 65 } 66 image := containerd.NewImageWithPlatform(client, found.Image, platforms.OnlyStrict(platform)) 67 imgConfig, err := getImageConfig(ctx, image) 68 if err != nil { 69 // Image found but blob not found for foreign arch 70 // Ignore err and return nil, so that the walker can visit the next candidate. 71 return nil 72 } 73 res = &EnsuredImage{ 74 Ref: found.Image.Name, 75 Image: image, 76 ImageConfig: *imgConfig, 77 Snapshotter: snapshotter, 78 Remote: getSnapshotterOpts(snapshotter).isRemote(), 79 } 80 if unpacked, err := image.IsUnpacked(ctx, snapshotter); err == nil && !unpacked { 81 if err := image.Unpack(ctx, snapshotter); err != nil { 82 return err 83 } 84 } 85 return nil 86 }, 87 } 88 count, err := imagewalker.Walk(ctx, rawRef) 89 if err != nil { 90 return nil, err 91 } 92 if count == 0 { 93 return nil, errdefs.NotFound(fmt.Errorf("got count 0 after walking")) 94 } 95 if res == nil { 96 return nil, errdefs.NotFound(fmt.Errorf("got nil res after walking")) 97 } 98 return res, nil 99 } 100 101 // EnsureImage ensures the image. 102 // 103 // # When insecure is set, skips verifying certs, and also falls back to HTTP when the registry does not speak HTTPS 104 // 105 // FIXME: this func has too many args 106 func EnsureImage(ctx context.Context, client *containerd.Client, stdout, stderr io.Writer, snapshotter, rawRef string, mode PullMode, insecure bool, hostsDirs []string, ocispecPlatforms []ocispec.Platform, unpack *bool, quiet bool, rFlags RemoteSnapshotterFlags) (*EnsuredImage, error) { 107 switch mode { 108 case "always", "missing", "never": 109 // NOP 110 default: 111 return nil, fmt.Errorf("unexpected pull mode: %q", mode) 112 } 113 114 // if not `always` pull and given one platform and image found locally, return existing image directly. 115 if mode != "always" && len(ocispecPlatforms) == 1 { 116 if res, err := GetExistingImage(ctx, client, snapshotter, rawRef, ocispecPlatforms[0]); err == nil { 117 return res, nil 118 } else if !errdefs.IsNotFound(err) { 119 return nil, err 120 } 121 } 122 123 if mode == "never" { 124 return nil, fmt.Errorf("image not available: %q", rawRef) 125 } 126 127 named, err := refdocker.ParseDockerRef(rawRef) 128 if err != nil { 129 return nil, err 130 } 131 ref := named.String() 132 refDomain := refdocker.Domain(named) 133 134 var dOpts []dockerconfigresolver.Opt 135 if insecure { 136 log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", refDomain) 137 dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) 138 } 139 dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(hostsDirs)) 140 resolver, err := dockerconfigresolver.New(ctx, refDomain, dOpts...) 141 if err != nil { 142 return nil, err 143 } 144 145 img, err := PullImage(ctx, client, stdout, stderr, snapshotter, resolver, ref, ocispecPlatforms, unpack, quiet, rFlags) 146 if err != nil { 147 // In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp <port>: connection refused". 148 if !errutil.IsErrHTTPResponseToHTTPSClient(err) && !errutil.IsErrConnectionRefused(err) { 149 return nil, err 150 } 151 if insecure { 152 log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain) 153 dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true)) 154 resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...) 155 if err != nil { 156 return nil, err 157 } 158 return PullImage(ctx, client, stdout, stderr, snapshotter, resolver, ref, ocispecPlatforms, unpack, quiet, rFlags) 159 } 160 log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain) 161 log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)") 162 return nil, err 163 164 } 165 return img, nil 166 } 167 168 // ResolveDigest resolves `rawRef` and returns its descriptor digest. 169 func ResolveDigest(ctx context.Context, rawRef string, insecure bool, hostsDirs []string) (string, error) { 170 named, err := refdocker.ParseDockerRef(rawRef) 171 if err != nil { 172 return "", err 173 } 174 ref := named.String() 175 refDomain := refdocker.Domain(named) 176 177 var dOpts []dockerconfigresolver.Opt 178 if insecure { 179 log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", refDomain) 180 dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) 181 } 182 dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(hostsDirs)) 183 resolver, err := dockerconfigresolver.New(ctx, refDomain, dOpts...) 184 if err != nil { 185 return "", err 186 } 187 188 _, desc, err := resolver.Resolve(ctx, ref) 189 if err != nil { 190 return "", err 191 } 192 193 return desc.Digest.String(), nil 194 } 195 196 // PullImage pulls an image using the specified resolver. 197 func PullImage(ctx context.Context, client *containerd.Client, stdout, stderr io.Writer, snapshotter string, resolver remotes.Resolver, ref string, ocispecPlatforms []ocispec.Platform, unpack *bool, quiet bool, rFlags RemoteSnapshotterFlags) (*EnsuredImage, error) { 198 ctx, done, err := client.WithLease(ctx) 199 if err != nil { 200 return nil, err 201 } 202 defer done(ctx) 203 204 var containerdImage containerd.Image 205 config := &pull.Config{ 206 Resolver: resolver, 207 RemoteOpts: []containerd.RemoteOpt{}, 208 Platforms: ocispecPlatforms, // empty for all-platforms 209 } 210 if !quiet { 211 config.ProgressOutput = stderr 212 } 213 214 // unpack(B) if given 1 platform unless specified by `unpack` 215 unpackB := len(ocispecPlatforms) == 1 216 if unpack != nil { 217 unpackB = *unpack 218 if unpackB && len(ocispecPlatforms) != 1 { 219 return nil, fmt.Errorf("unpacking requires a single platform to be specified (e.g., --platform=amd64)") 220 } 221 } 222 223 snOpt := getSnapshotterOpts(snapshotter) 224 if unpackB { 225 log.G(ctx).Debugf("The image will be unpacked for platform %q, snapshotter %q.", ocispecPlatforms[0], snapshotter) 226 imgcryptPayload := imgcrypt.Payload{} 227 imgcryptUnpackOpt := encryption.WithUnpackConfigApplyOpts(encryption.WithDecryptedUnpack(&imgcryptPayload)) 228 config.RemoteOpts = append(config.RemoteOpts, 229 containerd.WithPullUnpack, 230 containerd.WithUnpackOpts([]containerd.UnpackOpt{imgcryptUnpackOpt})) 231 232 // different remote snapshotters will update pull.Config separately 233 snOpt.apply(config, ref, rFlags) 234 } else { 235 log.G(ctx).Debugf("The image will not be unpacked. Platforms=%v.", ocispecPlatforms) 236 } 237 238 containerdImage, err = pull.Pull(ctx, client, ref, config) 239 if err != nil { 240 return nil, err 241 } 242 imgConfig, err := getImageConfig(ctx, containerdImage) 243 if err != nil { 244 return nil, err 245 } 246 res := &EnsuredImage{ 247 Ref: ref, 248 Image: containerdImage, 249 ImageConfig: *imgConfig, 250 Snapshotter: snapshotter, 251 Remote: snOpt.isRemote(), 252 } 253 return res, nil 254 255 } 256 257 func getImageConfig(ctx context.Context, image containerd.Image) (*ocispec.ImageConfig, error) { 258 desc, err := image.Config(ctx) 259 if err != nil { 260 return nil, err 261 } 262 switch desc.MediaType { 263 case ocispec.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config: 264 var ocispecImage ocispec.Image 265 b, err := content.ReadBlob(ctx, image.ContentStore(), desc) 266 if err != nil { 267 return nil, err 268 } 269 270 if err := json.Unmarshal(b, &ocispecImage); err != nil { 271 return nil, err 272 } 273 return &ocispecImage.Config, nil 274 default: 275 return nil, fmt.Errorf("unknown media type %q", desc.MediaType) 276 } 277 } 278 279 // ReadIndex returns image index, or nil for non-indexed image. 280 func ReadIndex(ctx context.Context, img containerd.Image) (*ocispec.Index, *ocispec.Descriptor, error) { 281 desc := img.Target() 282 if !images.IsIndexType(desc.MediaType) { 283 return nil, nil, nil 284 } 285 b, err := content.ReadBlob(ctx, img.ContentStore(), desc) 286 if err != nil { 287 return nil, &desc, err 288 } 289 var idx ocispec.Index 290 if err := json.Unmarshal(b, &idx); err != nil { 291 return nil, &desc, err 292 } 293 294 return &idx, &desc, nil 295 } 296 297 // ReadManifest returns the manifest for img.platform, or nil if no manifest was found. 298 func ReadManifest(ctx context.Context, img containerd.Image) (*ocispec.Manifest, *ocispec.Descriptor, error) { 299 cs := img.ContentStore() 300 targetDesc := img.Target() 301 if images.IsManifestType(targetDesc.MediaType) { 302 b, err := content.ReadBlob(ctx, img.ContentStore(), targetDesc) 303 if err != nil { 304 return nil, &targetDesc, err 305 } 306 var mani ocispec.Manifest 307 if err := json.Unmarshal(b, &mani); err != nil { 308 return nil, &targetDesc, err 309 } 310 return &mani, &targetDesc, nil 311 } 312 if images.IsIndexType(targetDesc.MediaType) { 313 idx, _, err := ReadIndex(ctx, img) 314 if err != nil { 315 return nil, nil, err 316 } 317 configDesc, err := img.Config(ctx) // aware of img.platform 318 if err != nil { 319 return nil, nil, err 320 } 321 // We can't access the private `img.platform` variable. 322 // So, we find the manifest object by comparing the config desc. 323 for _, maniDesc := range idx.Manifests { 324 maniDesc := maniDesc 325 // ignore non-nil err 326 if b, err := content.ReadBlob(ctx, cs, maniDesc); err == nil { 327 var mani ocispec.Manifest 328 if err := json.Unmarshal(b, &mani); err != nil { 329 return nil, nil, err 330 } 331 if reflect.DeepEqual(configDesc, mani.Config) { 332 return &mani, &maniDesc, nil 333 } 334 } 335 } 336 } 337 // no manifest was found 338 return nil, nil, nil 339 } 340 341 // ReadImageConfig reads the config spec (`application/vnd.oci.image.config.v1+json`) for img.platform from content store. 342 func ReadImageConfig(ctx context.Context, img containerd.Image) (ocispec.Image, ocispec.Descriptor, error) { 343 var config ocispec.Image 344 345 configDesc, err := img.Config(ctx) // aware of img.platform 346 if err != nil { 347 return config, configDesc, err 348 } 349 p, err := content.ReadBlob(ctx, img.ContentStore(), configDesc) 350 if err != nil { 351 return config, configDesc, err 352 } 353 if err := json.Unmarshal(p, &config); err != nil { 354 return config, configDesc, err 355 } 356 return config, configDesc, nil 357 } 358 359 // ParseRepoTag parses raw `imgName` to repository and tag. 360 func ParseRepoTag(imgName string) (string, string) { 361 log.L.Debugf("raw image name=%q", imgName) 362 363 ref, err := refdocker.ParseDockerRef(imgName) 364 if err != nil { 365 log.L.WithError(err).Debugf("unparsable image name %q", imgName) 366 return "", "" 367 } 368 369 var tag string 370 371 if tagged, ok := ref.(refdocker.Tagged); ok { 372 tag = tagged.Tag() 373 } 374 repository := refdocker.FamiliarName(ref) 375 376 return repository, tag 377 } 378 379 type snapshotKey string 380 381 // recursive function to calculate total usage of key's parent 382 func (key snapshotKey) add(ctx context.Context, s snapshots.Snapshotter, usage *snapshots.Usage) error { 383 if key == "" { 384 return nil 385 } 386 u, err := s.Usage(ctx, string(key)) 387 if err != nil { 388 return err 389 } 390 391 usage.Add(u) 392 393 info, err := s.Stat(ctx, string(key)) 394 if err != nil { 395 return err 396 } 397 398 key = snapshotKey(info.Parent) 399 return key.add(ctx, s, usage) 400 } 401 402 // UnpackedImageSize is the size of the unpacked snapshots. 403 // Does not contain the size of the blobs in the content store. (Corresponds to Docker). 404 func UnpackedImageSize(ctx context.Context, s snapshots.Snapshotter, img containerd.Image) (int64, error) { 405 diffIDs, err := img.RootFS(ctx) 406 if err != nil { 407 return 0, err 408 } 409 410 chainID := identity.ChainID(diffIDs).String() 411 usage, err := s.Usage(ctx, chainID) 412 if err != nil { 413 if errdefs.IsNotFound(err) { 414 log.G(ctx).WithError(err).Debugf("image %q seems not unpacked", img.Name()) 415 return 0, nil 416 } 417 return 0, err 418 } 419 420 info, err := s.Stat(ctx, chainID) 421 if err != nil { 422 return 0, err 423 } 424 425 //add ChainID's parent usage to the total usage 426 if err := snapshotKey(info.Parent).add(ctx, s, &usage); err != nil { 427 return 0, err 428 } 429 return usage.Size, nil 430 }