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