1package spec
2
3import (
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
13const (
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
18const baseCgroupsPath = "garden"
19
20// OciSpec converts a given `garden` container specification to an OCI spec.
21//
22func 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//
72func 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//
113func 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
132func 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
174func 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//
185func 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//
205func 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//
242func 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//
257func 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}
283