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  }