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 }