github.com/MerlinKodo/gvisor@v0.0.0-20231110090155-957f62ecf90e/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  	"strings"
    20  
    21  	"github.com/MerlinKodo/gvisor/pkg/log"
    22  	"github.com/MerlinKodo/gvisor/pkg/sentry/fsimpl/tmpfs"
    23  	"github.com/MerlinKodo/gvisor/runsc/config"
    24  	"github.com/MerlinKodo/gvisor/runsc/specutils"
    25  	specs "github.com/opencontainers/runtime-spec/specs-go"
    26  )
    27  
    28  // MountPrefix is the annotation prefix for mount hints.
    29  const MountPrefix = "dev.gvisor.spec.mount."
    30  
    31  // ShareType indicates who can access/mutate the volume contents.
    32  type ShareType int
    33  
    34  const (
    35  	invalid ShareType = iota
    36  
    37  	// container shareType indicates that the mount is used by a single
    38  	// container. There are no external observers.
    39  	container
    40  
    41  	// pod shareType indicates that the mount is used by more than one container
    42  	// inside the pod. There are no external observers.
    43  	pod
    44  
    45  	// shared shareType indicates that the mount can also be shared with a process
    46  	// outside the pod, e.g. NFS.
    47  	shared
    48  )
    49  
    50  func (s ShareType) String() string {
    51  	switch s {
    52  	case invalid:
    53  		return "invalid"
    54  	case container:
    55  		return "container"
    56  	case pod:
    57  		return "pod"
    58  	case shared:
    59  		return "shared"
    60  	default:
    61  		return fmt.Sprintf("invalid share value %d", s)
    62  	}
    63  }
    64  
    65  // LifecycleType indicates whether creation/deletion of the volume is tied to
    66  // the pod or container's lifecycle.
    67  type LifecycleType int
    68  
    69  const (
    70  	// sharedLife indicates that the volume's lifecycle is not tied to the pod.
    71  	// The volume persists beyond the pod's life. This is the safe default.
    72  	sharedLife LifecycleType = iota
    73  
    74  	// podLife indicates that the volume's lifecycle is tied to the pod's
    75  	// lifecycle. The volume is destroyed with the pod.
    76  	podLife
    77  
    78  	// containerLife indicates that the volume's lifecycle is tied to the
    79  	// container's lifecycle. The volume is destroyed with the container.
    80  	containerLife
    81  )
    82  
    83  func (o LifecycleType) String() string {
    84  	switch o {
    85  	case sharedLife:
    86  		return "shared"
    87  	case podLife:
    88  		return "pod"
    89  	case containerLife:
    90  		return "container"
    91  	default:
    92  		return fmt.Sprintf("invalid lifecycle value %d", o)
    93  	}
    94  }
    95  
    96  // PodMountHints contains a collection of mountHints for the pod.
    97  type PodMountHints struct {
    98  	Mounts map[string]*MountHint `json:"mounts"`
    99  }
   100  
   101  // NewPodMountHints instantiates PodMountHints using spec.
   102  func NewPodMountHints(spec *specs.Spec) (*PodMountHints, error) {
   103  	mnts := make(map[string]*MountHint)
   104  	for k, v := range spec.Annotations {
   105  		// Look for 'dev.gvisor.spec.mount' annotations and parse them.
   106  		if strings.HasPrefix(k, MountPrefix) {
   107  			// Remove the prefix and split the rest.
   108  			parts := strings.Split(k[len(MountPrefix):], ".")
   109  			if len(parts) != 2 {
   110  				return nil, fmt.Errorf("invalid mount annotation: %s=%s", k, v)
   111  			}
   112  			name := parts[0]
   113  			if len(name) == 0 {
   114  				return nil, fmt.Errorf("invalid mount name: %s", name)
   115  			}
   116  			mnt := mnts[name]
   117  			if mnt == nil {
   118  				mnt = &MountHint{Name: name}
   119  				mnts[name] = mnt
   120  			}
   121  			if err := mnt.setField(parts[1], v); err != nil {
   122  				log.Warningf("ignoring invalid mount annotation (name = %q, key = %q, value = %q): %v", name, parts[1], v, err)
   123  			}
   124  		}
   125  	}
   126  
   127  	// Validate all the parsed hints.
   128  	for name, m := range mnts {
   129  		log.Infof("Mount annotation found, name: %s, source: %q, type: %s, share: %v", name, m.Mount.Source, m.Mount.Type, m.Share)
   130  		if m.Share == invalid || len(m.Mount.Source) == 0 || len(m.Mount.Type) == 0 {
   131  			log.Warningf("ignoring mount annotations for %q because of missing required field(s)", name)
   132  			delete(mnts, name)
   133  			continue
   134  		}
   135  
   136  		// Check for duplicate mount sources.
   137  		for name2, m2 := range mnts {
   138  			if name != name2 && m.Mount.Source == m2.Mount.Source {
   139  				return nil, fmt.Errorf("mounts %q and %q have the same mount source %q", m.Name, m2.Name, m.Mount.Source)
   140  			}
   141  		}
   142  	}
   143  
   144  	return &PodMountHints{Mounts: mnts}, nil
   145  }
   146  
   147  // MountHint represents extra information about mounts that are provided via
   148  // annotations. They can override mount type, and provide sharing information
   149  // so that mounts can be correctly shared inside the pod.
   150  // It is part of the sandbox.Sandbox struct, so it must be serializable.
   151  type MountHint struct {
   152  	Name      string        `json:"name"`
   153  	Share     ShareType     `json:"share"`
   154  	Mount     specs.Mount   `json:"mount"`
   155  	Lifecycle LifecycleType `json:"lifecycle"`
   156  }
   157  
   158  func (m *MountHint) setField(key, val string) error {
   159  	switch key {
   160  	case "source":
   161  		if len(val) == 0 {
   162  			return fmt.Errorf("source cannot be empty")
   163  		}
   164  		m.Mount.Source = val
   165  	case "type":
   166  		return m.setType(val)
   167  	case "share":
   168  		return m.setShare(val)
   169  	case "options":
   170  		m.Mount.Options = specutils.FilterMountOptions(strings.Split(val, ","))
   171  	case "lifecycle":
   172  		return m.setLifecycle(val)
   173  	default:
   174  		return fmt.Errorf("invalid mount annotation: %s=%s", key, val)
   175  	}
   176  	return nil
   177  }
   178  
   179  func (m *MountHint) setType(val string) error {
   180  	switch val {
   181  	case tmpfs.Name, Bind:
   182  		m.Mount.Type = val
   183  	default:
   184  		return fmt.Errorf("invalid type %q", val)
   185  	}
   186  	return nil
   187  }
   188  
   189  func (m *MountHint) setShare(val string) error {
   190  	switch val {
   191  	case container.String():
   192  		m.Share = container
   193  	case pod.String():
   194  		m.Share = pod
   195  	case shared.String():
   196  		m.Share = shared
   197  	default:
   198  		return fmt.Errorf("invalid share value %q", val)
   199  	}
   200  	return nil
   201  }
   202  
   203  func (m *MountHint) setLifecycle(val string) error {
   204  	switch val {
   205  	case containerLife.String():
   206  		m.Lifecycle = containerLife
   207  	case podLife.String():
   208  		m.Lifecycle = podLife
   209  	case sharedLife.String():
   210  		m.Lifecycle = sharedLife
   211  	default:
   212  		return fmt.Errorf("invalid lifecycle %q", val)
   213  	}
   214  	return nil
   215  }
   216  
   217  // shouldShareMount returns true if this mount should be configured as a shared
   218  // mount that is shared among multiple containers in a pod.
   219  func (m *MountHint) shouldShareMount() bool {
   220  	// TODO(b/142076984): Only support tmpfs for now. Bind mounts require a
   221  	// common gofer to mount all shared volumes.
   222  	return m.Mount.Type == tmpfs.Name && m.Share == pod
   223  }
   224  
   225  // ShouldOverlay returns true if this mount should be overlaid.
   226  func (m *MountHint) ShouldOverlay() bool {
   227  	// TODO(b/142076984): Only support share=container for now. Once shared gofer
   228  	// support is added, we can overlay shared bind mounts too.
   229  	return m.Mount.Type == Bind && m.Share == container && m.Lifecycle != sharedLife
   230  }
   231  
   232  // checkCompatible verifies that shared mount is compatible with master.
   233  // Master options must be the same or less restrictive than the container mount,
   234  // e.g. master can be 'rw' while container mounts as 'ro'.
   235  func (m *MountHint) checkCompatible(replica *specs.Mount) error {
   236  	masterOpts := ParseMountOptions(m.Mount.Options)
   237  	replicaOpts := ParseMountOptions(replica.Options)
   238  
   239  	if masterOpts.ReadOnly && !replicaOpts.ReadOnly {
   240  		return fmt.Errorf("cannot mount read-write shared mount because master is read-only, mount: %+v", replica)
   241  	}
   242  	if masterOpts.Flags.NoExec && !replicaOpts.Flags.NoExec {
   243  		return fmt.Errorf("cannot mount exec enabled shared mount because master is noexec, mount: %+v", replica)
   244  	}
   245  	if masterOpts.Flags.NoATime && !replicaOpts.Flags.NoATime {
   246  		return fmt.Errorf("cannot mount atime enabled shared mount because master is noatime, mount: %+v", replica)
   247  	}
   248  	return nil
   249  }
   250  
   251  // Precondition: m.mount.Type == Bind.
   252  func (m *MountHint) fileAccessType() config.FileAccessType {
   253  	if m.Share == shared {
   254  		return config.FileAccessShared
   255  	}
   256  	if m.shouldShareMount() {
   257  		return config.FileAccessExclusive
   258  	}
   259  	if m.Share == container {
   260  		return config.FileAccessExclusive
   261  	}
   262  	return config.FileAccessShared
   263  }
   264  
   265  // FindMount finds the MountHint that applies to this mount.
   266  func (p *PodMountHints) FindMount(mount *specs.Mount) *MountHint {
   267  	for _, m := range p.Mounts {
   268  		if m.Mount.Source == mount.Source {
   269  			return m
   270  		}
   271  	}
   272  	return nil
   273  }