github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_commit.go (about) 1 package containerd 2 3 import ( 4 "context" 5 "crypto/rand" 6 "encoding/base64" 7 "encoding/json" 8 "fmt" 9 "runtime" 10 "strings" 11 "time" 12 13 "github.com/containerd/containerd/content" 14 "github.com/containerd/containerd/diff" 15 cerrdefs "github.com/containerd/containerd/errdefs" 16 "github.com/containerd/containerd/leases" 17 "github.com/containerd/containerd/mount" 18 "github.com/containerd/containerd/pkg/cleanup" 19 "github.com/containerd/containerd/snapshots" 20 "github.com/containerd/log" 21 "github.com/docker/docker/api/types/backend" 22 "github.com/docker/docker/image" 23 "github.com/docker/docker/internal/compatcontext" 24 "github.com/docker/docker/pkg/archive" 25 imagespec "github.com/moby/docker-image-spec/specs-go/v1" 26 "github.com/opencontainers/go-digest" 27 "github.com/opencontainers/image-spec/identity" 28 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 29 "github.com/pkg/errors" 30 ) 31 32 /* 33 This code is based on `commit` support in nerdctl, under Apache License 34 https://github.com/containerd/nerdctl/blob/master/pkg/imgutil/commit/commit.go 35 with adaptations to match the Moby data model and services. 36 */ 37 38 // CommitImage creates a new image from a commit config. 39 func (i *ImageService) CommitImage(ctx context.Context, cc backend.CommitConfig) (image.ID, error) { 40 container := i.containers.Get(cc.ContainerID) 41 cs := i.content 42 43 var parentManifest ocispec.Manifest 44 var parentImage imagespec.DockerOCIImage 45 46 // ImageManifest can be nil when committing an image with base FROM scratch 47 if container.ImageManifest != nil { 48 imageManifestBytes, err := content.ReadBlob(ctx, cs, *container.ImageManifest) 49 if err != nil { 50 return "", err 51 } 52 53 if err := json.Unmarshal(imageManifestBytes, &parentManifest); err != nil { 54 return "", err 55 } 56 57 imageConfigBytes, err := content.ReadBlob(ctx, cs, parentManifest.Config) 58 if err != nil { 59 return "", err 60 } 61 if err := json.Unmarshal(imageConfigBytes, &parentImage); err != nil { 62 return "", err 63 } 64 } 65 66 var ( 67 differ = i.client.DiffService() 68 sn = i.client.SnapshotService(container.Driver) 69 ) 70 71 // Don't gc me and clean the dirty data after 1 hour! 72 ctx, release, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) 73 if err != nil { 74 return "", fmt.Errorf("failed to create lease for commit: %w", err) 75 } 76 defer func() { 77 if err := release(compatcontext.WithoutCancel(ctx)); err != nil { 78 log.G(ctx).WithError(err).Warn("failed to release lease created for commit") 79 } 80 }() 81 82 diffLayerDesc, diffID, err := i.createDiff(ctx, cc.ContainerID, sn, cs, differ) 83 if err != nil { 84 return "", fmt.Errorf("failed to export layer: %w", err) 85 } 86 imageConfig := generateCommitImageConfig(parentImage, diffID, cc) 87 88 layers := parentManifest.Layers 89 if diffLayerDesc != nil { 90 rootfsID := identity.ChainID(imageConfig.RootFS.DiffIDs).String() 91 92 if err := i.applyDiffLayer(ctx, rootfsID, cc.ContainerID, sn, differ, *diffLayerDesc); err != nil { 93 return "", fmt.Errorf("failed to apply diff: %w", err) 94 } 95 96 layers = append(layers, *diffLayerDesc) 97 } 98 99 return i.createImageOCI(ctx, imageConfig, digest.Digest(cc.ParentImageID), layers, *cc.ContainerConfig) 100 } 101 102 // generateCommitImageConfig generates an OCI Image config based on the 103 // container's image and the CommitConfig options. 104 func generateCommitImageConfig(baseConfig imagespec.DockerOCIImage, diffID digest.Digest, opts backend.CommitConfig) imagespec.DockerOCIImage { 105 if opts.Author == "" { 106 opts.Author = baseConfig.Author 107 } 108 109 createdTime := time.Now() 110 arch := baseConfig.Architecture 111 if arch == "" { 112 arch = runtime.GOARCH 113 log.G(context.TODO()).Warnf("assuming arch=%q", arch) 114 } 115 os := baseConfig.OS 116 if os == "" { 117 os = runtime.GOOS 118 log.G(context.TODO()).Warnf("assuming os=%q", os) 119 } 120 log.G(context.TODO()).Debugf("generateCommitImageConfig(): arch=%q, os=%q", arch, os) 121 122 diffIds := baseConfig.RootFS.DiffIDs 123 if diffID != "" { 124 diffIds = append(diffIds, diffID) 125 } 126 127 return imagespec.DockerOCIImage{ 128 Image: ocispec.Image{ 129 Platform: ocispec.Platform{ 130 Architecture: arch, 131 OS: os, 132 }, 133 Created: &createdTime, 134 Author: opts.Author, 135 RootFS: ocispec.RootFS{ 136 Type: "layers", 137 DiffIDs: diffIds, 138 }, 139 History: append(baseConfig.History, ocispec.History{ 140 Created: &createdTime, 141 CreatedBy: strings.Join(opts.ContainerConfig.Cmd, " "), 142 Author: opts.Author, 143 Comment: opts.Comment, 144 EmptyLayer: diffID == "", 145 }), 146 }, 147 Config: containerConfigToDockerOCIImageConfig(opts.Config), 148 } 149 } 150 151 // createDiff creates a layer diff into containerd's content store. 152 // If the diff is empty it returns nil empty digest and no error. 153 func (i *ImageService) createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer) (*ocispec.Descriptor, digest.Digest, error) { 154 info, err := sn.Stat(ctx, name) 155 if err != nil { 156 return nil, "", err 157 } 158 159 var upper []mount.Mount 160 if !i.idMapping.Empty() { 161 // The rootfs of the container is remapped if an id mapping exists, we 162 // need to "unremap" it before committing the snapshot 163 rootPair := i.idMapping.RootPair() 164 usernsID := fmt.Sprintf("%s-%d-%d-%s", name, rootPair.UID, rootPair.GID, uniquePart()) 165 remappedID := usernsID + remapSuffix 166 baseName := name 167 168 if info.Kind == snapshots.KindActive { 169 source, err := sn.Mounts(ctx, name) 170 if err != nil { 171 return nil, "", err 172 } 173 174 // No need to use parent since the whole snapshot is copied. 175 // Using parent would require doing diff/apply while starting 176 // from empty can just copy the whole snapshot. 177 // TODO: Optimize this for overlay mounts, can use parent 178 // and just copy upper directories without mounting 179 upper, err = sn.Prepare(ctx, remappedID, "") 180 if err != nil { 181 return nil, "", err 182 } 183 184 if err := i.copyAndUnremapRootFS(ctx, upper, source); err != nil { 185 return nil, "", err 186 } 187 } else { 188 upper, err = sn.Prepare(ctx, remappedID, baseName) 189 if err != nil { 190 return nil, "", err 191 } 192 193 if err := i.unremapRootFS(ctx, upper); err != nil { 194 return nil, "", err 195 } 196 } 197 } else { 198 if info.Kind == snapshots.KindActive { 199 upper, err = sn.Mounts(ctx, name) 200 if err != nil { 201 return nil, "", err 202 } 203 } else { 204 upperKey := fmt.Sprintf("%s-view-%s", name, uniquePart()) 205 upper, err = sn.View(ctx, upperKey, name) 206 if err != nil { 207 return nil, "", err 208 } 209 defer cleanup.Do(ctx, func(ctx context.Context) { 210 sn.Remove(ctx, upperKey) 211 }) 212 } 213 } 214 215 lowerKey := fmt.Sprintf("%s-parent-view-%s", info.Parent, uniquePart()) 216 lower, err := sn.View(ctx, lowerKey, info.Parent) 217 if err != nil { 218 return nil, "", err 219 } 220 defer cleanup.Do(ctx, func(ctx context.Context) { 221 sn.Remove(ctx, lowerKey) 222 }) 223 224 newDesc, err := comparer.Compare(ctx, lower, upper) 225 if err != nil { 226 return nil, "", errors.Wrap(err, "CreateDiff") 227 } 228 229 ra, err := cs.ReaderAt(ctx, newDesc) 230 if err != nil { 231 return nil, "", fmt.Errorf("failed to read diff archive: %w", err) 232 } 233 defer ra.Close() 234 235 empty, err := archive.IsEmpty(content.NewReader(ra)) 236 if err != nil { 237 return nil, "", fmt.Errorf("failed to check if archive is empty: %w", err) 238 } 239 if empty { 240 return nil, "", nil 241 } 242 243 cinfo, err := cs.Info(ctx, newDesc.Digest) 244 if err != nil { 245 return nil, "", fmt.Errorf("failed to get content info: %w", err) 246 } 247 248 diffIDStr, ok := cinfo.Labels["containerd.io/uncompressed"] 249 if !ok { 250 return nil, "", fmt.Errorf("invalid differ response with no diffID") 251 } 252 253 diffID, err := digest.Parse(diffIDStr) 254 if err != nil { 255 return nil, "", err 256 } 257 258 return &ocispec.Descriptor{ 259 MediaType: ocispec.MediaTypeImageLayerGzip, 260 Digest: newDesc.Digest, 261 Size: cinfo.Size, 262 }, diffID, nil 263 } 264 265 // applyDiffLayer will apply diff layer content created by createDiff into the snapshotter. 266 func (i *ImageService) applyDiffLayer(ctx context.Context, name string, containerID string, sn snapshots.Snapshotter, differ diff.Applier, diffDesc ocispec.Descriptor) (retErr error) { 267 // Let containerd know that this snapshot is only for diff-applying. 268 key := snapshots.UnpackKeyPrefix + "-" + uniquePart() + "-" + name 269 270 info, err := sn.Stat(ctx, containerID) 271 if err != nil { 272 return err 273 } 274 275 mounts, err := sn.Prepare(ctx, key, info.Parent) 276 if err != nil { 277 return fmt.Errorf("failed to prepare snapshot: %w", err) 278 } 279 280 defer func() { 281 if retErr != nil { 282 // NOTE: the snapshotter should be held by lease. Even 283 // if the cleanup fails, the containerd gc can delete it. 284 if err := sn.Remove(ctx, key); err != nil { 285 log.G(ctx).Warnf("failed to cleanup aborted apply %s: %s", key, err) 286 } 287 } 288 }() 289 290 if _, err = differ.Apply(ctx, diffDesc, mounts); err != nil { 291 return err 292 } 293 294 if err = sn.Commit(ctx, name, key); err != nil { 295 if cerrdefs.IsAlreadyExists(err) { 296 return nil 297 } 298 return err 299 } 300 301 return nil 302 } 303 304 // copied from github.com/containerd/containerd/rootfs/apply.go 305 func uniquePart() string { 306 t := time.Now() 307 var b [3]byte 308 // Ignore read failures, just decreases uniqueness 309 rand.Read(b[:]) 310 return fmt.Sprintf("%d-%s", t.Nanosecond(), base64.URLEncoding.EncodeToString(b[:])) 311 } 312 313 // CommitBuildStep is used by the builder to create an image for each step in 314 // the build. 315 // 316 // This method is different from CreateImageFromContainer: 317 // - it doesn't attempt to validate container state 318 // - it doesn't send a commit action to metrics 319 // - it doesn't log a container commit event 320 // 321 // This is a temporary shim. Should be removed when builder stops using commit. 322 func (i *ImageService) CommitBuildStep(ctx context.Context, c backend.CommitConfig) (image.ID, error) { 323 ctr := i.containers.Get(c.ContainerID) 324 if ctr == nil { 325 // TODO: use typed error 326 return "", fmt.Errorf("container not found: %s", c.ContainerID) 327 } 328 c.ContainerMountLabel = ctr.MountLabel 329 c.ContainerOS = ctr.OS 330 c.ParentImageID = string(ctr.ImageID) 331 return i.CommitImage(ctx, c) 332 }