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