github.com/chenbh/concourse/v6@v6.4.2/worker/runtime/spec/spec.go (about) 1 package spec 2 3 import ( 4 "fmt" 5 "path/filepath" 6 "strings" 7 8 "code.cloudfoundry.org/garden" 9 "github.com/imdario/mergo" 10 specs "github.com/opencontainers/runtime-spec/specs-go" 11 ) 12 13 const ( 14 SuperuserPath = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 15 Path = "PATH=/usr/local/bin:/usr/bin:/bin" 16 ) 17 18 const baseCgroupsPath = "garden" 19 20 // OciSpec converts a given `garden` container specification to an OCI spec. 21 // 22 func OciSpec(gdn garden.ContainerSpec, maxUid, maxGid uint32) (oci *specs.Spec, err error) { 23 if gdn.Handle == "" { 24 err = fmt.Errorf("handle must be specified") 25 return 26 } 27 28 if gdn.RootFSPath == "" { 29 gdn.RootFSPath = gdn.Image.URI 30 } 31 32 var rootfs string 33 rootfs, err = rootfsDir(gdn.RootFSPath) 34 if err != nil { 35 return 36 } 37 38 var mounts []specs.Mount 39 mounts, err = OciSpecBindMounts(gdn.BindMounts) 40 if err != nil { 41 return 42 } 43 44 resources := OciResources(gdn.Limits) 45 cgroupsPath := OciCgroupsPath(baseCgroupsPath, gdn.Handle, gdn.Privileged) 46 47 oci = merge( 48 defaultGardenOciSpec(gdn.Privileged, maxUid, maxGid), 49 &specs.Spec{ 50 Version: specs.Version, 51 Hostname: gdn.Handle, 52 Process: &specs.Process{ 53 Env: gdn.Env, 54 }, 55 Root: &specs.Root{Path: rootfs}, 56 Mounts: mounts, 57 Annotations: map[string]string(gdn.Properties), 58 Linux: &specs.Linux{ 59 Resources: resources, 60 CgroupsPath: cgroupsPath, 61 }, 62 }, 63 ) 64 65 oci.Process.Env = envWithDefaultPath(oci.Process.Env, gdn.Privileged) 66 67 return 68 } 69 70 // OciSpecBindMounts converts garden bindmounts to oci spec mounts. 71 // 72 func OciSpecBindMounts(bindMounts []garden.BindMount) (mounts []specs.Mount, err error) { 73 for _, bindMount := range bindMounts { 74 if bindMount.SrcPath == "" || bindMount.DstPath == "" { 75 err = fmt.Errorf("src and dst must not be empty") 76 return 77 } 78 79 if !filepath.IsAbs(bindMount.SrcPath) || !filepath.IsAbs(bindMount.DstPath) { 80 err = fmt.Errorf("src and dst must be absolute") 81 return 82 } 83 84 if bindMount.Origin != garden.BindMountOriginHost { 85 err = fmt.Errorf("unknown bind mount origin %d", bindMount.Origin) 86 return 87 } 88 89 mode := "ro" 90 switch bindMount.Mode { 91 case garden.BindMountModeRO: 92 case garden.BindMountModeRW: 93 mode = "rw" 94 default: 95 err = fmt.Errorf("unknown bind mount mode %d", bindMount.Mode) 96 return 97 } 98 99 mounts = append(mounts, specs.Mount{ 100 Source: bindMount.SrcPath, 101 Destination: bindMount.DstPath, 102 Type: "bind", 103 Options: []string{"bind", mode}, 104 }) 105 } 106 107 return 108 } 109 110 // OciIDMappings provides the uid/gid mappings for user namespaces (if 111 // necessary, based on `privileged`). 112 // 113 func OciIDMappings(privileged bool, max uint32) []specs.LinuxIDMapping { 114 if privileged { 115 return []specs.LinuxIDMapping{} 116 } 117 118 return []specs.LinuxIDMapping{ 119 { // "root" inside, but non-root outside 120 ContainerID: 0, 121 HostID: max, 122 Size: 1, 123 }, 124 { // anything else, not root inside & outside 125 ContainerID: 1, 126 HostID: 1, 127 Size: max - 1, 128 }, 129 } 130 } 131 132 func OciResources(limits garden.Limits) *specs.LinuxResources { 133 var ( 134 cpuResources *specs.LinuxCPU 135 memoryResources *specs.LinuxMemory 136 pidLimit *specs.LinuxPids 137 ) 138 shares := limits.CPU.LimitInShares 139 if limits.CPU.Weight > 0 { 140 shares = limits.CPU.Weight 141 } 142 143 if shares > 0 { 144 cpuResources = &specs.LinuxCPU{ 145 Shares: &shares, 146 } 147 } 148 149 memoryLimit := int64(limits.Memory.LimitInBytes) 150 if memoryLimit > 0 { 151 memoryResources = &specs.LinuxMemory{ 152 Limit: &memoryLimit, 153 Swap: &memoryLimit, 154 } 155 } 156 157 maxPids := int64(limits.Pid.Max) 158 if maxPids > 0 { 159 pidLimit = &specs.LinuxPids{ 160 Limit: maxPids, 161 } 162 } 163 164 if cpuResources == nil && memoryResources == nil && pidLimit == nil { 165 return nil 166 } 167 return &specs.LinuxResources{ 168 CPU: cpuResources, 169 Memory: memoryResources, 170 Pids: pidLimit, 171 } 172 } 173 174 func OciCgroupsPath(basePath, handle string, privileged bool) string { 175 if privileged { 176 return "" 177 } 178 return filepath.Join(basePath, handle) 179 } 180 181 // envWithDefaultPath returns the default PATH for a privileged/unprivileged 182 // user based on the existence of PATH already being set in the initial 183 // environment. 184 // 185 func envWithDefaultPath(env []string, privileged bool) []string { 186 for _, envVar := range env { 187 if strings.HasPrefix(envVar, "PATH=") { 188 return env 189 } 190 } 191 192 if privileged { 193 return append(env, SuperuserPath) 194 } 195 196 return append(env, Path) 197 } 198 199 // defaultGardenOciSpec represents a default set of properties necessary in 200 // order to satisfy the garden interface. 201 // 202 // ps.: this spec is NOT completed - it must be merged with more properties to 203 // form a properly working container. 204 // 205 func defaultGardenOciSpec(privileged bool, maxUid, maxGid uint32) *specs.Spec { 206 var ( 207 namespaces = OciNamespaces(privileged) 208 capabilities = OciCapabilities(privileged) 209 ) 210 211 devices := AnyContainerDevices 212 if privileged { 213 devices = append(PrivilegedOnlyDevices, devices...) 214 } 215 216 spec := &specs.Spec{ 217 Process: &specs.Process{ 218 Args: []string{"/tmp/gdn-init"}, 219 Capabilities: &capabilities, 220 Cwd: "/", 221 }, 222 Linux: &specs.Linux{ 223 Namespaces: namespaces, 224 Resources: &specs.LinuxResources{ 225 Devices: devices, 226 }, 227 UIDMappings: OciIDMappings(privileged, maxUid), 228 GIDMappings: OciIDMappings(privileged, maxGid), 229 }, 230 Mounts: AnyContainerMounts, 231 } 232 233 if !privileged { 234 spec.Linux.Seccomp = seccomp 235 } 236 237 return spec 238 } 239 240 // merge merges an OCI spec `dst` into `src`. 241 // 242 func merge(dst, src *specs.Spec) *specs.Spec { 243 err := mergo.Merge(dst, src, mergo.WithAppendSlice) 244 if err != nil { 245 panic(fmt.Errorf( 246 "failed to merge specs %v %v - programming mistake? %w", 247 dst, src, err, 248 )) 249 } 250 251 return dst 252 } 253 254 // rootfsDir takes a raw rootfs uri and extracts the directory that it points to, 255 // if using a valid scheme (`raw://`) 256 // 257 func rootfsDir(raw string) (directory string, err error) { 258 if raw == "" { 259 err = fmt.Errorf("rootfs must not be empty") 260 return 261 } 262 263 parts := strings.SplitN(raw, "://", 2) 264 if len(parts) != 2 { 265 err = fmt.Errorf("malformatted rootfs: must be of form 'scheme://<abs_dir>'") 266 return 267 } 268 269 var scheme string 270 scheme, directory = parts[0], parts[1] 271 if scheme != "raw" { 272 err = fmt.Errorf("unsupported scheme '%s'", scheme) 273 return 274 } 275 276 if !filepath.IsAbs(directory) { 277 err = fmt.Errorf("directory must be an absolute path") 278 return 279 } 280 281 return 282 }