github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/container/run_mount.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 container 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "os" 24 "path/filepath" 25 "runtime" 26 "sort" 27 "strings" 28 "time" 29 30 "github.com/containerd/containerd" 31 "github.com/containerd/containerd/containers" 32 "github.com/containerd/containerd/errdefs" 33 "github.com/containerd/containerd/leases" 34 "github.com/containerd/containerd/mount" 35 "github.com/containerd/containerd/oci" 36 "github.com/containerd/containerd/pkg/userns" 37 "github.com/containerd/continuity/fs" 38 "github.com/containerd/log" 39 "github.com/containerd/nerdctl/v2/pkg/api/types" 40 "github.com/containerd/nerdctl/v2/pkg/cmd/volume" 41 "github.com/containerd/nerdctl/v2/pkg/idgen" 42 "github.com/containerd/nerdctl/v2/pkg/imgutil" 43 "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" 44 "github.com/containerd/nerdctl/v2/pkg/labels" 45 "github.com/containerd/nerdctl/v2/pkg/mountutil" 46 "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" 47 "github.com/containerd/nerdctl/v2/pkg/strutil" 48 securejoin "github.com/cyphar/filepath-securejoin" 49 "github.com/opencontainers/image-spec/identity" 50 "github.com/opencontainers/runtime-spec/specs-go" 51 ) 52 53 // copy from https://github.com/containerd/containerd/blob/v1.6.0-rc.1/pkg/cri/opts/spec_linux.go#L129-L151 54 func withMounts(mounts []specs.Mount) oci.SpecOpts { 55 return func(ctx context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error { 56 // Copy all mounts from default mounts, except for 57 // - mounts overridden by supplied mount; 58 // - all mounts under /dev if a supplied /dev is present. 59 mountSet := make(map[string]struct{}) 60 for _, m := range mounts { 61 mountSet[filepath.Clean(m.Destination)] = struct{}{} 62 } 63 64 defaultMounts := s.Mounts 65 s.Mounts = nil 66 67 for _, m := range defaultMounts { 68 dst := filepath.Clean(m.Destination) 69 if _, ok := mountSet[dst]; ok { 70 // filter out mount overridden by a supplied mount 71 continue 72 } 73 if _, mountDev := mountSet["/dev"]; mountDev && strings.HasPrefix(dst, "/dev/") { 74 // filter out everything under /dev if /dev is a supplied mount 75 continue 76 } 77 s.Mounts = append(s.Mounts, m) 78 } 79 80 s.Mounts = append(s.Mounts, mounts...) 81 82 sort.Slice(s.Mounts, func(i, j int) bool { 83 // Consistent with the less function in Docker. 84 // https://github.com/moby/moby/blob/0db417451313474133c5ed62bbf95e2d3c92444d/daemon/volumes.go#L34 85 return strings.Count(filepath.Clean(s.Mounts[i].Destination), string(os.PathSeparator)) < strings.Count(filepath.Clean(s.Mounts[j].Destination), string(os.PathSeparator)) 86 }) 87 88 return nil 89 } 90 } 91 92 // parseMountFlags parses --volume, --mount and --tmpfs. 93 func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCreateOptions) ([]*mountutil.Processed, error) { 94 var parsed []*mountutil.Processed //nolint:prealloc 95 for _, v := range strutil.DedupeStrSlice(options.Volume) { 96 // createDir=true for -v option to allow creation of directory on host if not found. 97 x, err := mountutil.ProcessFlagV(v, volStore, true) 98 if err != nil { 99 return nil, err 100 } 101 parsed = append(parsed, x) 102 } 103 104 for _, v := range strutil.DedupeStrSlice(options.Tmpfs) { 105 x, err := mountutil.ProcessFlagTmpfs(v) 106 if err != nil { 107 return nil, err 108 } 109 parsed = append(parsed, x) 110 } 111 112 for _, v := range strutil.DedupeStrSlice(options.Mount) { 113 x, err := mountutil.ProcessFlagMount(v, volStore) 114 if err != nil { 115 return nil, err 116 } 117 parsed = append(parsed, x) 118 } 119 120 return parsed, nil 121 } 122 123 // generateMountOpts generates volume-related mount opts. 124 // Other mounts such as procfs mount are not handled here. 125 func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredImage *imgutil.EnsuredImage, options types.ContainerCreateOptions) ([]oci.SpecOpts, []string, []*mountutil.Processed, error) { 126 // volume store is corresponds to a directory like `/var/lib/nerdctl/1935db59/volumes/default` 127 volStore, err := volume.Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address) 128 if err != nil { 129 return nil, nil, nil, err 130 } 131 132 //nolint:golint,prealloc 133 var ( 134 opts []oci.SpecOpts 135 anonVolumes []string 136 userMounts []specs.Mount 137 mountPoints []*mountutil.Processed 138 ) 139 mounted := make(map[string]struct{}) 140 var imageVolumes map[string]struct{} 141 var tempDir string 142 if ensuredImage != nil { 143 imageVolumes = ensuredImage.ImageConfig.Volumes 144 145 if err := ensuredImage.Image.Unpack(ctx, options.GOptions.Snapshotter); err != nil { 146 return nil, nil, nil, fmt.Errorf("error unpacking image: %w", err) 147 } 148 149 diffIDs, err := ensuredImage.Image.RootFS(ctx) 150 if err != nil { 151 return nil, nil, nil, err 152 } 153 chainID := identity.ChainID(diffIDs).String() 154 155 s := client.SnapshotService(options.GOptions.Snapshotter) 156 tempDir, err = os.MkdirTemp("", "initialC") 157 if err != nil { 158 return nil, nil, nil, err 159 } 160 // We use Remove here instead of RemoveAll. 161 // The RemoveAll will delete the temp dir and all children it contains. 162 // When the Unmount fails, RemoveAll will incorrectly delete data from the mounted dir 163 defer os.Remove(tempDir) 164 165 // Add a lease of 1 hour to the view so that it is not garbage collected 166 // Note(gsamfira): should we make this shorter? 167 ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) 168 if err != nil { 169 return nil, nil, nil, fmt.Errorf("failed to create lease: %w", err) 170 } 171 defer done(ctx) 172 173 var mounts []mount.Mount 174 mounts, err = s.View(ctx, tempDir, chainID) 175 if err != nil { 176 return nil, nil, nil, err 177 } 178 179 // windows has additional steps for mounting see 180 // https://github.com/containerd/containerd/commit/791e175c79930a34cfbb2048fbcaa8493fd2c86b 181 unmounter := func(mountPath string) { 182 if uerr := mount.Unmount(mountPath, 0); uerr != nil { 183 log.G(ctx).Debugf("Failed to unmount snapshot %q", tempDir) 184 if err == nil { 185 err = uerr 186 } 187 } 188 } 189 190 if runtime.GOOS == "linux" { 191 defer unmounter(tempDir) 192 for _, m := range mounts { 193 m := m 194 if m.Type == "bind" && userns.RunningInUserNS() { 195 // For https://github.com/containerd/nerdctl/issues/2056 196 unpriv, err := mountutil.UnprivilegedMountFlags(m.Source) 197 if err != nil { 198 return nil, nil, nil, err 199 } 200 m.Options = strutil.DedupeStrSlice(append(m.Options, unpriv...)) 201 } 202 if err := m.Mount(tempDir); err != nil { 203 if rmErr := s.Remove(ctx, tempDir); rmErr != nil && !errdefs.IsNotFound(rmErr) { 204 return nil, nil, nil, rmErr 205 } 206 return nil, nil, nil, fmt.Errorf("failed to mount %+v on %q: %w", m, tempDir, err) 207 } 208 } 209 } else { 210 defer unmounter(tempDir) 211 if err := mount.All(mounts, tempDir); err != nil { 212 if err := s.Remove(ctx, tempDir); err != nil && !errdefs.IsNotFound(err) { 213 return nil, nil, nil, err 214 } 215 return nil, nil, nil, err 216 } 217 } 218 } 219 220 if parsed, err := parseMountFlags(volStore, options); err != nil { 221 return nil, nil, nil, err 222 } else if len(parsed) > 0 { 223 ociMounts := make([]specs.Mount, len(parsed)) 224 for i, x := range parsed { 225 ociMounts[i] = x.Mount 226 mounted[filepath.Clean(x.Mount.Destination)] = struct{}{} 227 228 target, err := securejoin.SecureJoin(tempDir, x.Mount.Destination) 229 if err != nil { 230 return nil, nil, nil, err 231 } 232 233 // Copying content in AnonymousVolume and namedVolume 234 if x.Type == "volume" { 235 if err := copyExistingContents(target, x.Mount.Source); err != nil { 236 return nil, nil, nil, err 237 } 238 } 239 if x.AnonymousVolume != "" { 240 anonVolumes = append(anonVolumes, x.AnonymousVolume) 241 } 242 opts = append(opts, x.Opts...) 243 } 244 userMounts = append(userMounts, ociMounts...) 245 246 // add parsed user specified bind-mounts/volume/tmpfs to mountPoints 247 mountPoints = append(mountPoints, parsed...) 248 } 249 250 // imageVolumes are defined in Dockerfile "VOLUME" instruction 251 for imgVolRaw := range imageVolumes { 252 imgVol := filepath.Clean(imgVolRaw) 253 switch imgVol { 254 case "/", "/dev", "/sys", "proc": 255 return nil, nil, nil, fmt.Errorf("invalid VOLUME: %q", imgVolRaw) 256 } 257 if _, ok := mounted[imgVol]; ok { 258 continue 259 } 260 anonVolName := idgen.GenerateID() 261 262 log.G(ctx).Debugf("creating anonymous volume %q, for \"VOLUME %s\"", 263 anonVolName, imgVolRaw) 264 anonVol, err := volStore.Create(anonVolName, []string{}) 265 if err != nil { 266 return nil, nil, nil, err 267 } 268 269 target, err := securejoin.SecureJoin(tempDir, imgVol) 270 if err != nil { 271 return nil, nil, nil, err 272 } 273 274 //copying up initial contents of the mount point directory 275 if err := copyExistingContents(target, anonVol.Mountpoint); err != nil { 276 return nil, nil, nil, err 277 } 278 279 m := specs.Mount{ 280 Type: "none", 281 Source: anonVol.Mountpoint, 282 Destination: imgVol, 283 Options: []string{"rbind"}, 284 } 285 userMounts = append(userMounts, m) 286 anonVolumes = append(anonVolumes, anonVolName) 287 288 mountPoint := &mountutil.Processed{ 289 Type: "volume", 290 AnonymousVolume: anonVolName, 291 Mount: m, 292 } 293 mountPoints = append(mountPoints, mountPoint) 294 } 295 296 opts = append(opts, withMounts(userMounts)) 297 298 containers, err := client.Containers(ctx) 299 if err != nil { 300 return nil, nil, nil, err 301 } 302 303 vfSet := strutil.SliceToSet(options.VolumesFrom) 304 var vfMountPoints []dockercompat.MountPoint 305 var vfAnonVolumes []string 306 307 for _, c := range containers { 308 ls, err := c.Labels(ctx) 309 if err != nil { 310 return nil, nil, nil, err 311 } 312 _, idMatch := vfSet[c.ID()] 313 nameMatch := false 314 if name, found := ls[labels.Name]; found { 315 _, nameMatch = vfSet[name] 316 } 317 318 if idMatch || nameMatch { 319 if av, found := ls[labels.AnonymousVolumes]; found { 320 err = json.Unmarshal([]byte(av), &vfAnonVolumes) 321 if err != nil { 322 return nil, nil, nil, err 323 } 324 } 325 if m, found := ls[labels.Mounts]; found { 326 err = json.Unmarshal([]byte(m), &vfMountPoints) 327 if err != nil { 328 return nil, nil, nil, err 329 } 330 } 331 332 ps := processeds(vfMountPoints) 333 s, err := c.Spec(ctx) 334 if err != nil { 335 return nil, nil, nil, err 336 } 337 opts = append(opts, withMounts(s.Mounts)) 338 anonVolumes = append(anonVolumes, vfAnonVolumes...) 339 mountPoints = append(mountPoints, ps...) 340 } 341 } 342 343 return opts, anonVolumes, mountPoints, nil 344 } 345 346 // copyExistingContents copies from the source to the destination and 347 // ensures the ownership is appropriately set. 348 func copyExistingContents(source, destination string) error { 349 if _, err := os.Stat(source); os.IsNotExist(err) { 350 return nil 351 } 352 dstList, err := os.ReadDir(destination) 353 if err != nil { 354 return err 355 } 356 if len(dstList) != 0 { 357 log.L.Debugf("volume at %q is not initially empty, skipping copying", destination) 358 return nil 359 } 360 return fs.CopyDir(destination, source) 361 }