github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/imgutil/commit/commit.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 commit 18 19 import ( 20 "bytes" 21 "context" 22 "crypto/rand" 23 "encoding/base64" 24 "encoding/json" 25 "fmt" 26 "runtime" 27 "strings" 28 "time" 29 30 "github.com/containerd/containerd" 31 "github.com/containerd/containerd/cio" 32 "github.com/containerd/containerd/content" 33 "github.com/containerd/containerd/diff" 34 "github.com/containerd/containerd/errdefs" 35 "github.com/containerd/containerd/images" 36 "github.com/containerd/containerd/leases" 37 "github.com/containerd/containerd/rootfs" 38 "github.com/containerd/containerd/snapshots" 39 "github.com/containerd/log" 40 imgutil "github.com/containerd/nerdctl/v2/pkg/imgutil" 41 "github.com/containerd/nerdctl/v2/pkg/labels" 42 "github.com/containerd/platforms" 43 "github.com/opencontainers/go-digest" 44 "github.com/opencontainers/image-spec/identity" 45 "github.com/opencontainers/image-spec/specs-go" 46 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 47 ) 48 49 type Changes struct { 50 CMD, Entrypoint []string 51 } 52 53 type Opts struct { 54 Author string 55 Message string 56 Ref string 57 Pause bool 58 Changes Changes 59 } 60 61 var ( 62 emptyGZLayer = digest.Digest("sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1") 63 emptyDigest = digest.Digest("") 64 ) 65 66 func Commit(ctx context.Context, client *containerd.Client, container containerd.Container, opts *Opts) (digest.Digest, error) { 67 id := container.ID() 68 info, err := container.Info(ctx) 69 if err != nil { 70 return emptyDigest, err 71 } 72 73 // NOTE: Moby uses provided rootfs to run container. It doesn't support 74 // to commit container created by moby. 75 baseImgWithoutPlatform, err := client.ImageService().Get(ctx, info.Image) 76 if err != nil { 77 return emptyDigest, fmt.Errorf("container %q lacks image (wasn't created by nerdctl?): %w", id, err) 78 } 79 platformLabel := info.Labels[labels.Platform] 80 if platformLabel == "" { 81 platformLabel = platforms.DefaultString() 82 log.G(ctx).Warnf("Image lacks label %q, assuming the platform to be %q", labels.Platform, platformLabel) 83 } 84 ocispecPlatform, err := platforms.Parse(platformLabel) 85 if err != nil { 86 return emptyDigest, err 87 } 88 log.G(ctx).Debugf("ocispecPlatform=%q", platforms.Format(ocispecPlatform)) 89 platformMC := platforms.Only(ocispecPlatform) 90 baseImg := containerd.NewImageWithPlatform(client, baseImgWithoutPlatform, platformMC) 91 92 baseImgConfig, _, err := imgutil.ReadImageConfig(ctx, baseImg) 93 if err != nil { 94 return emptyDigest, err 95 } 96 97 task, err := container.Task(ctx, cio.Load) 98 if err != nil { 99 return emptyDigest, err 100 } 101 102 if opts.Pause { 103 status, err := task.Status(ctx) 104 if err != nil { 105 return emptyDigest, err 106 } 107 108 switch status.Status { 109 case containerd.Paused, containerd.Created, containerd.Stopped: 110 default: 111 if err := task.Pause(ctx); err != nil { 112 return emptyDigest, fmt.Errorf("failed to pause container: %w", err) 113 } 114 115 defer func() { 116 if err := task.Resume(ctx); err != nil { 117 log.G(ctx).Warnf("failed to unpause container %v: %v", id, err) 118 } 119 }() 120 } 121 } 122 123 var ( 124 differ = client.DiffService() 125 snName = info.Snapshotter 126 sn = client.SnapshotService(snName) 127 ) 128 129 // Don't gc me and clean the dirty data after 1 hour! 130 ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) 131 if err != nil { 132 return emptyDigest, fmt.Errorf("failed to create lease for commit: %w", err) 133 } 134 defer done(ctx) 135 136 diffLayerDesc, diffID, err := createDiff(ctx, id, sn, client.ContentStore(), differ) 137 if err != nil { 138 return emptyDigest, fmt.Errorf("failed to export layer: %w", err) 139 } 140 141 imageConfig, err := generateCommitImageConfig(ctx, container, baseImg, diffID, opts) 142 if err != nil { 143 return emptyDigest, fmt.Errorf("failed to generate commit image config: %w", err) 144 } 145 146 rootfsID := identity.ChainID(imageConfig.RootFS.DiffIDs).String() 147 if err := applyDiffLayer(ctx, rootfsID, baseImgConfig, sn, differ, diffLayerDesc); err != nil { 148 return emptyDigest, fmt.Errorf("failed to apply diff: %w", err) 149 } 150 151 commitManifestDesc, configDigest, err := writeContentsForImage(ctx, snName, baseImg, imageConfig, diffLayerDesc) 152 if err != nil { 153 return emptyDigest, err 154 } 155 156 // image create 157 img := images.Image{ 158 Name: opts.Ref, 159 Target: commitManifestDesc, 160 CreatedAt: time.Now(), 161 } 162 163 if _, err := client.ImageService().Update(ctx, img); err != nil { 164 if !errdefs.IsNotFound(err) { 165 return emptyDigest, err 166 } 167 168 if _, err := client.ImageService().Create(ctx, img); err != nil { 169 return emptyDigest, fmt.Errorf("failed to create new image %s: %w", opts.Ref, err) 170 } 171 } 172 return configDigest, nil 173 } 174 175 // generateCommitImageConfig returns commit oci image config based on the container's image. 176 func generateCommitImageConfig(ctx context.Context, container containerd.Container, img containerd.Image, diffID digest.Digest, opts *Opts) (ocispec.Image, error) { 177 spec, err := container.Spec(ctx) 178 if err != nil { 179 return ocispec.Image{}, err 180 } 181 182 baseConfig, _, err := imgutil.ReadImageConfig(ctx, img) // aware of img.platform 183 if err != nil { 184 return ocispec.Image{}, err 185 } 186 187 // TODO(fuweid): support updating the USER/ENV/... fields? 188 if opts.Changes.CMD != nil { 189 baseConfig.Config.Cmd = opts.Changes.CMD 190 } 191 if opts.Changes.Entrypoint != nil { 192 baseConfig.Config.Entrypoint = opts.Changes.Entrypoint 193 } 194 if opts.Author == "" { 195 opts.Author = baseConfig.Author 196 } 197 198 createdBy := "" 199 if spec.Process != nil { 200 createdBy = strings.Join(spec.Process.Args, " ") 201 } 202 203 createdTime := time.Now() 204 arch := baseConfig.Architecture 205 if arch == "" { 206 arch = runtime.GOARCH 207 log.G(ctx).Warnf("assuming arch=%q", arch) 208 } 209 os := baseConfig.OS 210 if os == "" { 211 os = runtime.GOOS 212 log.G(ctx).Warnf("assuming os=%q", os) 213 } 214 log.G(ctx).Debugf("generateCommitImageConfig(): arch=%q, os=%q", arch, os) 215 return ocispec.Image{ 216 Platform: ocispec.Platform{ 217 Architecture: arch, 218 OS: os, 219 }, 220 221 Created: &createdTime, 222 Author: opts.Author, 223 Config: baseConfig.Config, 224 RootFS: ocispec.RootFS{ 225 Type: "layers", 226 DiffIDs: append(baseConfig.RootFS.DiffIDs, diffID), 227 }, 228 History: append(baseConfig.History, ocispec.History{ 229 Created: &createdTime, 230 CreatedBy: createdBy, 231 Author: opts.Author, 232 Comment: opts.Message, 233 EmptyLayer: (diffID == emptyGZLayer), 234 }), 235 }, nil 236 } 237 238 // writeContentsForImage will commit oci image config and manifest into containerd's content store. 239 func writeContentsForImage(ctx context.Context, snName string, baseImg containerd.Image, newConfig ocispec.Image, diffLayerDesc ocispec.Descriptor) (ocispec.Descriptor, digest.Digest, error) { 240 newConfigJSON, err := json.Marshal(newConfig) 241 if err != nil { 242 return ocispec.Descriptor{}, emptyDigest, err 243 } 244 245 configDesc := ocispec.Descriptor{ 246 MediaType: images.MediaTypeDockerSchema2Config, 247 Digest: digest.FromBytes(newConfigJSON), 248 Size: int64(len(newConfigJSON)), 249 } 250 251 cs := baseImg.ContentStore() 252 baseMfst, _, err := imgutil.ReadManifest(ctx, baseImg) 253 if err != nil { 254 return ocispec.Descriptor{}, emptyDigest, err 255 } 256 layers := append(baseMfst.Layers, diffLayerDesc) 257 258 newMfst := struct { 259 MediaType string `json:"mediaType,omitempty"` 260 ocispec.Manifest 261 }{ 262 MediaType: images.MediaTypeDockerSchema2Manifest, 263 Manifest: ocispec.Manifest{ 264 Versioned: specs.Versioned{ 265 SchemaVersion: 2, 266 }, 267 Config: configDesc, 268 Layers: layers, 269 }, 270 } 271 272 newMfstJSON, err := json.MarshalIndent(newMfst, "", " ") 273 if err != nil { 274 return ocispec.Descriptor{}, emptyDigest, err 275 } 276 277 newMfstDesc := ocispec.Descriptor{ 278 MediaType: images.MediaTypeDockerSchema2Manifest, 279 Digest: digest.FromBytes(newMfstJSON), 280 Size: int64(len(newMfstJSON)), 281 } 282 283 // new manifest should reference the layers and config content 284 labels := map[string]string{ 285 "containerd.io/gc.ref.content.0": configDesc.Digest.String(), 286 } 287 for i, l := range layers { 288 labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = l.Digest.String() 289 } 290 291 err = content.WriteBlob(ctx, cs, newMfstDesc.Digest.String(), bytes.NewReader(newMfstJSON), newMfstDesc, content.WithLabels(labels)) 292 if err != nil { 293 return ocispec.Descriptor{}, emptyDigest, err 294 } 295 296 // config should reference to snapshotter 297 labelOpt := content.WithLabels(map[string]string{ 298 fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snName): identity.ChainID(newConfig.RootFS.DiffIDs).String(), 299 }) 300 err = content.WriteBlob(ctx, cs, configDesc.Digest.String(), bytes.NewReader(newConfigJSON), configDesc, labelOpt) 301 if err != nil { 302 return ocispec.Descriptor{}, emptyDigest, err 303 } 304 305 return newMfstDesc, configDesc.Digest, nil 306 } 307 308 // createDiff creates a layer diff into containerd's content store. 309 func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer) (ocispec.Descriptor, digest.Digest, error) { 310 newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer) 311 if err != nil { 312 return ocispec.Descriptor{}, digest.Digest(""), err 313 } 314 315 info, err := cs.Info(ctx, newDesc.Digest) 316 if err != nil { 317 return ocispec.Descriptor{}, digest.Digest(""), err 318 } 319 320 diffIDStr, ok := info.Labels["containerd.io/uncompressed"] 321 if !ok { 322 return ocispec.Descriptor{}, digest.Digest(""), fmt.Errorf("invalid differ response with no diffID") 323 } 324 325 diffID, err := digest.Parse(diffIDStr) 326 if err != nil { 327 return ocispec.Descriptor{}, digest.Digest(""), err 328 } 329 330 return ocispec.Descriptor{ 331 MediaType: images.MediaTypeDockerSchema2LayerGzip, 332 Digest: newDesc.Digest, 333 Size: info.Size, 334 }, diffID, nil 335 } 336 337 // applyDiffLayer will apply diff layer content created by createDiff into the snapshotter. 338 func applyDiffLayer(ctx context.Context, name string, baseImg ocispec.Image, sn snapshots.Snapshotter, differ diff.Applier, diffDesc ocispec.Descriptor) (retErr error) { 339 var ( 340 key = uniquePart() + "-" + name 341 parent = identity.ChainID(baseImg.RootFS.DiffIDs).String() 342 ) 343 344 mount, err := sn.Prepare(ctx, key, parent) 345 if err != nil { 346 return err 347 } 348 349 defer func() { 350 if retErr != nil { 351 // NOTE: the snapshotter should be hold by lease. Even 352 // if the cleanup fails, the containerd gc can delete it. 353 if err := sn.Remove(ctx, key); err != nil { 354 log.G(ctx).Warnf("failed to cleanup aborted apply %s: %s", key, err) 355 } 356 } 357 }() 358 359 if _, err = differ.Apply(ctx, diffDesc, mount); err != nil { 360 return err 361 } 362 363 if err = sn.Commit(ctx, name, key); err != nil { 364 if errdefs.IsAlreadyExists(err) { 365 return nil 366 } 367 return err 368 } 369 return nil 370 } 371 372 // copied from github.com/containerd/containerd/rootfs/apply.go 373 func uniquePart() string { 374 t := time.Now() 375 var b [3]byte 376 // Ignore read failures, just decreases uniqueness 377 rand.Read(b[:]) 378 return fmt.Sprintf("%d-%s", t.Nanosecond(), base64.URLEncoding.EncodeToString(b[:])) 379 }