gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/runsc/boot/mount_hints.go (about)

     1  // Copyright 2022 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package boot
    16  
    17  import (
    18  	"fmt"
    19  	"path/filepath"
    20  	"strings"
    21  
    22  	specs "github.com/opencontainers/runtime-spec/specs-go"
    23  	"gvisor.dev/gvisor/pkg/log"
    24  	"gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs"
    25  	"gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
    26  	"gvisor.dev/gvisor/runsc/config"
    27  	"gvisor.dev/gvisor/runsc/specutils"
    28  )
    29  
    30  const (
    31  	// MountPrefix is the annotation prefix for mount hints applied at the pod level.
    32  	MountPrefix = "dev.gvisor.spec.mount."
    33  
    34  	// RootfsPrefix is the annotation prefix for rootfs hint applied at the container level.
    35  	RootfsPrefix = "dev.gvisor.spec.rootfs."
    36  )
    37  
    38  // ShareType indicates who can access/mutate the volume contents.
    39  type ShareType int
    40  
    41  const (
    42  	invalid ShareType = iota
    43  
    44  	// container shareType indicates that the mount is used by a single
    45  	// container. There are no external observers.
    46  	container
    47  
    48  	// pod shareType indicates that the mount is used by more than one container
    49  	// inside the pod. There are no external observers.
    50  	pod
    51  
    52  	// shared shareType indicates that the mount can also be shared with a process
    53  	// outside the pod, e.g. NFS.
    54  	shared
    55  )
    56  
    57  func (s ShareType) String() string {
    58  	switch s {
    59  	case invalid:
    60  		return "invalid"
    61  	case container:
    62  		return "container"
    63  	case pod:
    64  		return "pod"
    65  	case shared:
    66  		return "shared"
    67  	default:
    68  		return fmt.Sprintf("invalid share value %d", s)
    69  	}
    70  }
    71  
    72  // PodMountHints contains a collection of mountHints for the pod.
    73  type PodMountHints struct {
    74  	Mounts map[string]*MountHint `json:"mounts"`
    75  }
    76  
    77  // NewPodMountHints instantiates PodMountHints using spec.
    78  func NewPodMountHints(spec *specs.Spec) (*PodMountHints, error) {
    79  	mnts := make(map[string]*MountHint)
    80  	for k, v := range spec.Annotations {
    81  		// Look for 'dev.gvisor.spec.mount' annotations and parse them.
    82  		if strings.HasPrefix(k, MountPrefix) {
    83  			// Remove the prefix and split the rest.
    84  			parts := strings.Split(k[len(MountPrefix):], ".")
    85  			if len(parts) != 2 {
    86  				return nil, fmt.Errorf("invalid mount annotation: %s=%s", k, v)
    87  			}
    88  			name := parts[0]
    89  			if len(name) == 0 {
    90  				return nil, fmt.Errorf("invalid mount name: %s", name)
    91  			}
    92  			mnt := mnts[name]
    93  			if mnt == nil {
    94  				mnt = &MountHint{Name: name}
    95  				mnts[name] = mnt
    96  			}
    97  			if err := mnt.setField(parts[1], v); err != nil {
    98  				log.Warningf("ignoring invalid mount annotation (name = %q, key = %q, value = %q): %v", name, parts[1], v, err)
    99  			}
   100  		}
   101  	}
   102  
   103  	// Validate all the parsed hints.
   104  	for name, m := range mnts {
   105  		log.Infof("Mount annotation found, name: %s, source: %q, type: %s, share: %v", name, m.Mount.Source, m.Mount.Type, m.Share)
   106  		if m.Share == invalid || len(m.Mount.Source) == 0 || len(m.Mount.Type) == 0 {
   107  			log.Warningf("ignoring mount annotations for %q because of missing required field(s)", name)
   108  			delete(mnts, name)
   109  			continue
   110  		}
   111  
   112  		// Check for duplicate mount sources.
   113  		for name2, m2 := range mnts {
   114  			if name != name2 && m.Mount.Source == m2.Mount.Source {
   115  				return nil, fmt.Errorf("mounts %q and %q have the same mount source %q", m.Name, m2.Name, m.Mount.Source)
   116  			}
   117  		}
   118  	}
   119  
   120  	return &PodMountHints{Mounts: mnts}, nil
   121  }
   122  
   123  // MountHint represents extra information about mounts that are provided via
   124  // annotations. They can override mount type, and provide sharing information
   125  // so that mounts can be correctly shared inside the pod.
   126  // It is part of the sandbox.Sandbox struct, so it must be serializable.
   127  type MountHint struct {
   128  	Name  string      `json:"name"`
   129  	Share ShareType   `json:"share"`
   130  	Mount specs.Mount `json:"mount"`
   131  }
   132  
   133  func (m *MountHint) setField(key, val string) error {
   134  	switch key {
   135  	case "source":
   136  		if len(val) == 0 {
   137  			return fmt.Errorf("source cannot be empty")
   138  		}
   139  		m.Mount.Source = val
   140  	case "type":
   141  		return m.setType(val)
   142  	case "share":
   143  		return m.setShare(val)
   144  	case "options":
   145  		m.Mount.Options = specutils.FilterMountOptions(strings.Split(val, ","))
   146  	default:
   147  		return fmt.Errorf("invalid mount annotation: %s=%s", key, val)
   148  	}
   149  	return nil
   150  }
   151  
   152  func (m *MountHint) setType(val string) error {
   153  	switch val {
   154  	case tmpfs.Name, Bind:
   155  		m.Mount.Type = val
   156  	default:
   157  		return fmt.Errorf("invalid type %q", val)
   158  	}
   159  	return nil
   160  }
   161  
   162  func (m *MountHint) setShare(val string) error {
   163  	switch val {
   164  	case container.String():
   165  		m.Share = container
   166  	case pod.String():
   167  		m.Share = pod
   168  	case shared.String():
   169  		m.Share = shared
   170  	default:
   171  		return fmt.Errorf("invalid share value %q", val)
   172  	}
   173  	return nil
   174  }
   175  
   176  // ShouldShareMount returns true if this mount should be configured as a shared
   177  // mount that is shared among multiple containers in a pod.
   178  func (m *MountHint) ShouldShareMount() bool {
   179  	// Only support tmpfs for now. Bind mounts require a common gofer to mount
   180  	// all shared volumes.
   181  	return m.Mount.Type == tmpfs.Name &&
   182  		// A shared mount should be configured for share=container too so:
   183  		// 1. Restarting the container does not lose the tmpfs data.
   184  		// 2. Repeated mounts in the container reuse the same tmpfs instance.
   185  		(m.Share == container || m.Share == pod)
   186  }
   187  
   188  // checkCompatible verifies that shared mount is compatible with master.
   189  // Master options must be the same or less restrictive than the container mount,
   190  // e.g. master can be 'rw' while container mounts as 'ro'.
   191  func (m *MountHint) checkCompatible(replica *specs.Mount) error {
   192  	masterOpts := ParseMountOptions(m.Mount.Options)
   193  	replicaOpts := ParseMountOptions(replica.Options)
   194  
   195  	if masterOpts.ReadOnly && !replicaOpts.ReadOnly {
   196  		return fmt.Errorf("cannot mount read-write shared mount because master is read-only, mount: %+v", replica)
   197  	}
   198  	if masterOpts.Flags.NoExec && !replicaOpts.Flags.NoExec {
   199  		return fmt.Errorf("cannot mount exec enabled shared mount because master is noexec, mount: %+v", replica)
   200  	}
   201  	if masterOpts.Flags.NoATime && !replicaOpts.Flags.NoATime {
   202  		return fmt.Errorf("cannot mount atime enabled shared mount because master is noatime, mount: %+v", replica)
   203  	}
   204  	return nil
   205  }
   206  
   207  func (m *MountHint) fileAccessType() config.FileAccessType {
   208  	if m.Share == shared {
   209  		return config.FileAccessShared
   210  	}
   211  	if m.ShouldShareMount() {
   212  		return config.FileAccessExclusive
   213  	}
   214  	if m.Share == container {
   215  		return config.FileAccessExclusive
   216  	}
   217  	return config.FileAccessShared
   218  }
   219  
   220  // FindMount finds the MountHint that applies to this mount.
   221  func (p *PodMountHints) FindMount(mountSrc string) *MountHint {
   222  	for _, m := range p.Mounts {
   223  		if m.Mount.Source == mountSrc {
   224  			return m
   225  		}
   226  	}
   227  	return nil
   228  }
   229  
   230  // RootfsHint represents extra information about rootfs that are provided via
   231  // annotations. They can provide mount source, mount type and overlay config.
   232  type RootfsHint struct {
   233  	Mount   specs.Mount
   234  	Overlay config.OverlayMedium
   235  }
   236  
   237  func (r *RootfsHint) setSource(val string) error {
   238  	if !filepath.IsAbs(val) {
   239  		return fmt.Errorf("source should be an absolute path, got %q", val)
   240  	}
   241  	r.Mount.Source = val
   242  	return nil
   243  }
   244  
   245  func (r *RootfsHint) setType(val string) error {
   246  	switch val {
   247  	case erofs.Name, Bind:
   248  		r.Mount.Type = val
   249  	default:
   250  		return fmt.Errorf("invalid type %q", val)
   251  	}
   252  	return nil
   253  }
   254  
   255  func (r *RootfsHint) setField(key, val string) error {
   256  	switch key {
   257  	case "source":
   258  		return r.setSource(val)
   259  	case "type":
   260  		return r.setType(val)
   261  	case "overlay":
   262  		return r.Overlay.Set(val)
   263  	default:
   264  		return fmt.Errorf("invalid rootfs annotation: %s=%s", key, val)
   265  	}
   266  }
   267  
   268  // NewRootfsHint instantiates RootfsHint using spec.
   269  func NewRootfsHint(spec *specs.Spec) (*RootfsHint, error) {
   270  	var hint *RootfsHint
   271  	for k, v := range spec.Annotations {
   272  		// Look for 'dev.gvisor.spec.rootfs' annotations and parse them.
   273  		if !strings.HasPrefix(k, RootfsPrefix) {
   274  			continue
   275  		}
   276  		// Remove the prefix.
   277  		k = k[len(RootfsPrefix):]
   278  		if hint == nil {
   279  			hint = &RootfsHint{}
   280  		}
   281  		if err := hint.setField(k, v); err != nil {
   282  			return nil, fmt.Errorf("invalid rootfs annotation (key = %q, value = %q): %v", k, v, err)
   283  		}
   284  	}
   285  	// Validate the parsed hint.
   286  	if hint != nil {
   287  		log.Infof("Rootfs annotations found, source: %q, type: %q, overlay: %q", hint.Mount.Source, hint.Mount.Type, hint.Overlay)
   288  		if len(hint.Mount.Source) == 0 || len(hint.Mount.Type) == 0 {
   289  			return nil, fmt.Errorf("rootfs annotations missing required field(s): %+v", hint)
   290  		}
   291  	}
   292  	return hint, nil
   293  }