@@ -37,6 +37,7 @@ import (
3737 "github.com/containerd/containerd/v2/core/containers"
3838 "github.com/containerd/containerd/v2/core/leases"
3939 "github.com/containerd/containerd/v2/core/mount"
40+ "github.com/containerd/containerd/v2/core/snapshots"
4041 "github.com/containerd/containerd/v2/pkg/oci"
4142 "github.com/containerd/continuity/fs"
4243 "github.com/containerd/errdefs"
@@ -122,17 +123,83 @@ func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCr
122123 return parsed , nil
123124}
124125
126+ // gcRootLabel marks a snapshot as a GC root so containerd does not reclaim it.
127+ const gcRootLabel = "containerd.io/gc.root"
128+
129+ // setupImageMount ensures and unpacks ref, then creates a read-only GC-rooted
130+ // snapshot view of its rootfs. It returns the OCI mount for destination and the
131+ // view's snapshot key.
132+ func setupImageMount (ctx context.Context , client * containerd.Client , options types.ContainerCreateOptions , ref , destination string ) (specs.Mount , string , error ) {
133+ ensured , err := imgutil .EnsureImage (ctx , client , ref , options .ImagePullOpt )
134+ if err != nil {
135+ return specs.Mount {}, "" , fmt .Errorf ("failed to ensure image %q for image mount: %w" , ref , err )
136+ }
137+ if err := ensured .Image .Unpack (ctx , options .GOptions .Snapshotter ); err != nil {
138+ return specs.Mount {}, "" , fmt .Errorf ("failed to unpack image %q for image mount: %w" , ref , err )
139+ }
140+ diffIDs , err := ensured .Image .RootFS (ctx )
141+ if err != nil {
142+ return specs.Mount {}, "" , fmt .Errorf ("failed to get rootfs of image %q for image mount: %w" , ref , err )
143+ }
144+ chainID := identity .ChainID (diffIDs ).String ()
145+
146+ snapshotKey := idgen .GenerateID () + "-image-mount"
147+ s := client .SnapshotService (options .GOptions .Snapshotter )
148+ mounts , err := s .View (ctx , snapshotKey , chainID , snapshots .WithLabels (map [string ]string {
149+ gcRootLabel : time .Now ().UTC ().Format (time .RFC3339 ),
150+ }))
151+ if err != nil {
152+ return specs.Mount {}, "" , fmt .Errorf ("failed to create read-only view of image %q: %w" , ref , err )
153+ }
154+ // overlayfs and native snapshotters each yield a single mount for a view.
155+ if len (mounts ) != 1 {
156+ if rmErr := s .Remove (ctx , snapshotKey ); rmErr != nil && ! errdefs .IsNotFound (rmErr ) {
157+ log .G (ctx ).WithError (rmErr ).Warnf ("failed to remove image-mount snapshot %q" , snapshotKey )
158+ }
159+ return specs.Mount {}, "" , fmt .Errorf ("image mount expects exactly one mount from the snapshotter, got %d" , len (mounts ))
160+ }
161+
162+ m := mounts [0 ]
163+ opts := m .Options
164+ // A view without an upper dir is already read-only; make it explicit for
165+ // bind-backed snapshotters.
166+ if ! strutil .InStringSlice (opts , "ro" ) {
167+ opts = append (opts , "ro" )
168+ }
169+ return specs.Mount {
170+ Type : m .Type ,
171+ Source : m .Source ,
172+ Destination : destination ,
173+ Options : opts ,
174+ }, snapshotKey , nil
175+ }
176+
177+ // removeImageMountViews removes the snapshotter views created for type=image
178+ // mounts. NotFound is ignored; other failures are logged but not fatal.
179+ func removeImageMountViews (ctx context.Context , s snapshots.Snapshotter , keys []string ) {
180+ for _ , k := range keys {
181+ if err := s .Remove (ctx , k ); err != nil && ! errdefs .IsNotFound (err ) {
182+ log .G (ctx ).WithError (err ).Warnf ("failed to remove image-mount snapshot %q" , k )
183+ }
184+ }
185+ }
186+
125187// generateMountOpts generates volume-related mount opts.
126188// Other mounts such as procfs mount are not handled here.
127189func generateMountOpts (ctx context.Context , client * containerd.Client , ensuredImage * imgutil.EnsuredImage ,
128- volStore volumestore.VolumeStore , options types.ContainerCreateOptions ) ([]oci.SpecOpts , []string , []* mountutil.Processed , error ) {
190+ volStore volumestore.VolumeStore , options types.ContainerCreateOptions ) (opts []oci.SpecOpts , anonVolumes []string , mountPoints []* mountutil.Processed , retErr error ) {
129191 //nolint:prealloc
130192 var (
131- opts []oci.SpecOpts
132- anonVolumes []string
133- userMounts []specs.Mount
134- mountPoints []* mountutil.Processed
193+ userMounts []specs.Mount
194+ imageMountViews []string
135195 )
196+ // Remove any image-mount views created here if this function fails, so a
197+ // partial setup does not leak snapshots.
198+ defer func () {
199+ if retErr != nil && len (imageMountViews ) > 0 {
200+ removeImageMountViews (ctx , client .SnapshotService (options .GOptions .Snapshotter ), imageMountViews )
201+ }
202+ }()
136203 mounted := make (map [string ]struct {})
137204 var imageVolumes map [string ]struct {}
138205 var tempDir string
@@ -229,6 +296,20 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm
229296 } else if len (parsed ) > 0 {
230297 ociMounts := make ([]specs.Mount , len (parsed ))
231298 for i , x := range parsed {
299+ // type=image: build the read-only view now and record its snapshot
300+ // key for cleanup on container removal.
301+ if x .Type == mountutil .Image {
302+ m , snapshotKey , err := setupImageMount (ctx , client , options , x .Mount .Source , x .Mount .Destination )
303+ if err != nil {
304+ return nil , nil , nil , err
305+ }
306+ imageMountViews = append (imageMountViews , snapshotKey )
307+ ociMounts [i ] = m
308+ x .ImageMountSnapshot = snapshotKey
309+ mounted [filepath .Clean (x .Mount .Destination )] = struct {}{}
310+ continue
311+ }
312+
232313 ociMounts [i ] = x .Mount
233314 mounted [filepath .Clean (x .Mount .Destination )] = struct {}{}
234315
0 commit comments