mirror of
				https://github.com/linuxkit/linuxkit.git
				synced 2025-10-31 20:37:59 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			510 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			510 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"archive/tar"
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"path/filepath"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 
 | |
| 	log "github.com/Sirupsen/logrus"
 | |
| 	"github.com/docker/docker/api/types"
 | |
| 	"github.com/opencontainers/runtime-spec/specs-go"
 | |
| 	"github.com/xeipuuv/gojsonschema"
 | |
| 	"gopkg.in/yaml.v2"
 | |
| )
 | |
| 
 | |
| // Moby is the type of a Moby config file
 | |
| type Moby struct {
 | |
| 	Kernel struct {
 | |
| 		Image   string
 | |
| 		Cmdline string
 | |
| 	}
 | |
| 	Init     []string
 | |
| 	Onboot   []MobyImage
 | |
| 	Services []MobyImage
 | |
| 	Trust    TrustConfig
 | |
| 	Files    []struct {
 | |
| 		Path      string
 | |
| 		Directory bool
 | |
| 		Symlink   string
 | |
| 		Contents  string
 | |
| 	}
 | |
| 	Outputs []struct {
 | |
| 		Format string
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // TrustConfig is the type of a content trust config
 | |
| type TrustConfig struct {
 | |
| 	Image []string
 | |
| 	Org   []string
 | |
| }
 | |
| 
 | |
| // MobyImage is the type of an image config
 | |
| type MobyImage struct {
 | |
| 	Name              string
 | |
| 	Image             string
 | |
| 	Capabilities      []string
 | |
| 	Mounts            []specs.Mount
 | |
| 	Binds             []string
 | |
| 	Tmpfs             []string
 | |
| 	Command           []string
 | |
| 	Env               []string
 | |
| 	Cwd               string
 | |
| 	Net               string
 | |
| 	Pid               string
 | |
| 	Ipc               string
 | |
| 	Uts               string
 | |
| 	Readonly          bool
 | |
| 	MaskedPaths       []string `yaml:"maskedPaths"`
 | |
| 	ReadonlyPaths     []string `yaml:"readonlyPaths"`
 | |
| 	UID               uint32   `yaml:"uid"`
 | |
| 	GID               uint32   `yaml:"gid"`
 | |
| 	AdditionalGids    []uint32 `yaml:"additionalGids"`
 | |
| 	NoNewPrivileges   bool     `yaml:"noNewPrivileges"`
 | |
| 	Hostname          string
 | |
| 	OomScoreAdj       int    `yaml:"oomScoreAdj"`
 | |
| 	DisableOOMKiller  bool   `yaml:"disableOOMKiller"`
 | |
| 	RootfsPropagation string `yaml:"rootfsPropagation"`
 | |
| 	CgroupsPath       string `yaml:"cgroupsPath"`
 | |
| 	Sysctl            map[string]string
 | |
| }
 | |
| 
 | |
| // github.com/go-yaml/yaml treats map keys as interface{} while encoding/json
 | |
| // requires them to be strings, integers or to implement encoding.TextMarshaler.
 | |
| // Fix this up by recursively mapping all map[interface{}]interface{} types into
 | |
| // map[string]interface{}.
 | |
| // see http://stackoverflow.com/questions/40737122/convert-yaml-to-json-without-struct-golang#answer-40737676
 | |
| func convert(i interface{}) interface{} {
 | |
| 	switch x := i.(type) {
 | |
| 	case map[interface{}]interface{}:
 | |
| 		m2 := map[string]interface{}{}
 | |
| 		for k, v := range x {
 | |
| 			m2[k.(string)] = convert(v)
 | |
| 		}
 | |
| 		return m2
 | |
| 	case []interface{}:
 | |
| 		for i, v := range x {
 | |
| 			x[i] = convert(v)
 | |
| 		}
 | |
| 	}
 | |
| 	return i
 | |
| }
 | |
| 
 | |
| // NewConfig parses a config file
 | |
| func NewConfig(config []byte) (*Moby, error) {
 | |
| 	m := Moby{}
 | |
| 
 | |
| 	// Parse raw yaml
 | |
| 	var rawYaml interface{}
 | |
| 	err := yaml.Unmarshal(config, &rawYaml)
 | |
| 	if err != nil {
 | |
| 		return &m, err
 | |
| 	}
 | |
| 
 | |
| 	// Convert to raw JSON
 | |
| 	rawJSON := convert(rawYaml)
 | |
| 
 | |
| 	// Validate raw yaml with JSON schema
 | |
| 	schemaLoader := gojsonschema.NewStringLoader(schema)
 | |
| 	documentLoader := gojsonschema.NewGoLoader(rawJSON)
 | |
| 	result, err := gojsonschema.Validate(schemaLoader, documentLoader)
 | |
| 	if err != nil {
 | |
| 		return &m, err
 | |
| 	}
 | |
| 	if !result.Valid() {
 | |
| 		fmt.Printf("The configuration file is invalid:\n")
 | |
| 		for _, desc := range result.Errors() {
 | |
| 			fmt.Printf("- %s\n", desc)
 | |
| 		}
 | |
| 		return &m, fmt.Errorf("invalid configuration file")
 | |
| 	}
 | |
| 
 | |
| 	// Parse yaml
 | |
| 	err = yaml.Unmarshal(config, &m)
 | |
| 	if err != nil {
 | |
| 		return &m, err
 | |
| 	}
 | |
| 
 | |
| 	return &m, nil
 | |
| }
 | |
| 
 | |
| // ConfigToOCI converts a config specification to an OCI config file
 | |
| func ConfigToOCI(image *MobyImage) ([]byte, error) {
 | |
| 
 | |
| 	// TODO pass through same docker client to all functions
 | |
| 	cli, err := dockerClient()
 | |
| 	if err != nil {
 | |
| 		return []byte{}, err
 | |
| 	}
 | |
| 
 | |
| 	inspect, err := dockerInspectImage(cli, image.Image)
 | |
| 	if err != nil {
 | |
| 		return []byte{}, err
 | |
| 	}
 | |
| 
 | |
| 	return ConfigInspectToOCI(image, inspect)
 | |
| }
 | |
| 
 | |
| func defaultMountpoint(tp string) string {
 | |
| 	switch tp {
 | |
| 	case "proc":
 | |
| 		return "/proc"
 | |
| 	case "devpts":
 | |
| 		return "/dev/pts"
 | |
| 	case "sysfs":
 | |
| 		return "/sys"
 | |
| 	case "cgroup":
 | |
| 		return "/sys/fs/cgroup"
 | |
| 	case "mqueue":
 | |
| 		return "/dev/mqueue"
 | |
| 	default:
 | |
| 		return ""
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Sort mounts by number of path components so /dev/pts is listed after /dev
 | |
| type mlist []specs.Mount
 | |
| 
 | |
| func (m mlist) Len() int {
 | |
| 	return len(m)
 | |
| }
 | |
| func (m mlist) Less(i, j int) bool {
 | |
| 	return m.parts(i) < m.parts(j)
 | |
| }
 | |
| func (m mlist) Swap(i, j int) {
 | |
| 	m[i], m[j] = m[j], m[i]
 | |
| }
 | |
| func (m mlist) parts(i int) int {
 | |
| 	return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator))
 | |
| }
 | |
| 
 | |
| // ConfigInspectToOCI converts a config and the output of image inspect to an OCI config file
 | |
| func ConfigInspectToOCI(image *MobyImage, inspect types.ImageInspect) ([]byte, error) {
 | |
| 	oci := specs.Spec{}
 | |
| 
 | |
| 	config := inspect.Config
 | |
| 	if config == nil {
 | |
| 		return []byte{}, errors.New("empty image config")
 | |
| 	}
 | |
| 
 | |
| 	args := append(config.Entrypoint, config.Cmd...)
 | |
| 	if len(image.Command) != 0 {
 | |
| 		args = image.Command
 | |
| 	}
 | |
| 	env := config.Env
 | |
| 	if len(image.Env) != 0 {
 | |
| 		env = image.Env
 | |
| 	}
 | |
| 	cwd := config.WorkingDir
 | |
| 	if image.Cwd != "" {
 | |
| 		cwd = image.Cwd
 | |
| 	}
 | |
| 	if cwd == "" {
 | |
| 		cwd = "/"
 | |
| 	}
 | |
| 	// default options match what Docker does
 | |
| 	procOptions := []string{"nosuid", "nodev", "noexec", "relatime"}
 | |
| 	devOptions := []string{"nosuid", "strictatime", "mode=755", "size=65536k"}
 | |
| 	if image.Readonly {
 | |
| 		devOptions = append(devOptions, "ro")
 | |
| 	}
 | |
| 	ptsOptions := []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"}
 | |
| 	sysOptions := []string{"nosuid", "noexec", "nodev"}
 | |
| 	if image.Readonly {
 | |
| 		sysOptions = append(sysOptions, "ro")
 | |
| 	}
 | |
| 	cgroupOptions := []string{"nosuid", "noexec", "nodev", "relatime", "ro"}
 | |
| 	// note omits "standard" /dev/shm and /dev/mqueue
 | |
| 	mounts := map[string]specs.Mount{
 | |
| 		"/proc":          {Destination: "/proc", Type: "proc", Source: "proc", Options: procOptions},
 | |
| 		"/dev":           {Destination: "/dev", Type: "tmpfs", Source: "tmpfs", Options: devOptions},
 | |
| 		"/dev/pts":       {Destination: "/dev/pts", Type: "devpts", Source: "devpts", Options: ptsOptions},
 | |
| 		"/sys":           {Destination: "/sys", Type: "sysfs", Source: "sysfs", Options: sysOptions},
 | |
| 		"/sys/fs/cgroup": {Destination: "/sys/fs/cgroup", Type: "cgroup", Source: "cgroup", Options: cgroupOptions},
 | |
| 	}
 | |
| 	for _, t := range image.Tmpfs {
 | |
| 		parts := strings.Split(t, ":")
 | |
| 		if len(parts) > 2 {
 | |
| 			return []byte{}, fmt.Errorf("Cannot parse tmpfs, too many ':': %s", t)
 | |
| 		}
 | |
| 		dest := parts[0]
 | |
| 		opts := []string{}
 | |
| 		if len(parts) == 2 {
 | |
| 			opts = strings.Split(parts[2], ",")
 | |
| 		}
 | |
| 		mounts[dest] = specs.Mount{Destination: dest, Type: "tmpfs", Source: "tmpfs", Options: opts}
 | |
| 	}
 | |
| 	for _, b := range image.Binds {
 | |
| 		parts := strings.Split(b, ":")
 | |
| 		if len(parts) < 2 {
 | |
| 			return []byte{}, fmt.Errorf("Cannot parse bind, missing ':': %s", b)
 | |
| 		}
 | |
| 		if len(parts) > 3 {
 | |
| 			return []byte{}, fmt.Errorf("Cannot parse bind, too many ':': %s", b)
 | |
| 		}
 | |
| 		src := parts[0]
 | |
| 		dest := parts[1]
 | |
| 		opts := []string{"rw", "rbind", "rprivate"}
 | |
| 		if len(parts) == 3 {
 | |
| 			opts = strings.Split(parts[2], ",")
 | |
| 		}
 | |
| 		mounts[dest] = specs.Mount{Destination: dest, Type: "bind", Source: src, Options: opts}
 | |
| 	}
 | |
| 	for _, m := range image.Mounts {
 | |
| 		tp := m.Type
 | |
| 		src := m.Source
 | |
| 		dest := m.Destination
 | |
| 		opts := m.Options
 | |
| 		if tp == "" {
 | |
| 			switch src {
 | |
| 			case "mqueue", "devpts", "proc", "sysfs", "cgroup":
 | |
| 				tp = src
 | |
| 			}
 | |
| 		}
 | |
| 		if tp == "" && dest == "/dev" {
 | |
| 			tp = "tmpfs"
 | |
| 		}
 | |
| 		if tp == "" {
 | |
| 			return []byte{}, fmt.Errorf("Mount for destination %s is missing type", dest)
 | |
| 		}
 | |
| 		if src == "" {
 | |
| 			// usually sane, eg proc, tmpfs etc
 | |
| 			src = tp
 | |
| 		}
 | |
| 		if dest == "" {
 | |
| 			dest = defaultMountpoint(tp)
 | |
| 		}
 | |
| 		if dest == "" {
 | |
| 			return []byte{}, fmt.Errorf("Mount type %s is missing destination", tp)
 | |
| 		}
 | |
| 		mounts[dest] = specs.Mount{Destination: dest, Type: tp, Source: src, Options: opts}
 | |
| 	}
 | |
| 	mountList := mlist{}
 | |
| 	for _, m := range mounts {
 | |
| 		mountList = append(mountList, m)
 | |
| 	}
 | |
| 	sort.Sort(mountList)
 | |
| 
 | |
| 	namespaces := []specs.LinuxNamespace{}
 | |
| 	// to attach to an existing namespace, easiest to bind mount with nsfs in a system container
 | |
| 	if image.Net != "host" {
 | |
| 		namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.NetworkNamespace, Path: image.Net})
 | |
| 	}
 | |
| 	if image.Pid != "host" {
 | |
| 		namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.PIDNamespace, Path: image.Pid})
 | |
| 	}
 | |
| 	if image.Ipc != "host" {
 | |
| 		namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.IPCNamespace, Path: image.Ipc})
 | |
| 	}
 | |
| 	if image.Uts != "host" {
 | |
| 		namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.UTSNamespace, Path: image.Uts})
 | |
| 	}
 | |
| 	// TODO user, cgroup namespaces, maybe mount=host if useful
 | |
| 	namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.MountNamespace})
 | |
| 
 | |
| 	caps := image.Capabilities
 | |
| 	if len(caps) == 1 {
 | |
| 		switch cap := strings.ToLower(caps[0]); cap {
 | |
| 		case "none":
 | |
| 			caps = []string{}
 | |
| 		case "all":
 | |
| 			caps = []string{
 | |
| 				"CAP_AUDIT_CONTROL",
 | |
| 				"CAP_AUDIT_READ",
 | |
| 				"CAP_AUDIT_WRITE",
 | |
| 				"CAP_BLOCK_SUSPEND",
 | |
| 				"CAP_CHOWN",
 | |
| 				"CAP_DAC_OVERRIDE",
 | |
| 				"CAP_DAC_READ_SEARCH",
 | |
| 				"CAP_FOWNER",
 | |
| 				"CAP_FSETID",
 | |
| 				"CAP_IPC_LOCK",
 | |
| 				"CAP_IPC_OWNER",
 | |
| 				"CAP_KILL",
 | |
| 				"CAP_LEASE",
 | |
| 				"CAP_LINUX_IMMUTABLE",
 | |
| 				"CAP_MAC_ADMIN",
 | |
| 				"CAP_MAC_OVERRIDE",
 | |
| 				"CAP_MKNOD",
 | |
| 				"CAP_NET_ADMIN",
 | |
| 				"CAP_NET_BIND_SERVICE",
 | |
| 				"CAP_NET_BROADCAST",
 | |
| 				"CAP_NET_RAW",
 | |
| 				"CAP_SETFCAP",
 | |
| 				"CAP_SETGID",
 | |
| 				"CAP_SETPCAP",
 | |
| 				"CAP_SETUID",
 | |
| 				"CAP_SYSLOG",
 | |
| 				"CAP_SYS_ADMIN",
 | |
| 				"CAP_SYS_BOOT",
 | |
| 				"CAP_SYS_CHROOT",
 | |
| 				"CAP_SYS_MODULE",
 | |
| 				"CAP_SYS_NICE",
 | |
| 				"CAP_SYS_PACCT",
 | |
| 				"CAP_SYS_PTRACE",
 | |
| 				"CAP_SYS_RAWIO",
 | |
| 				"CAP_SYS_RESOURCE",
 | |
| 				"CAP_SYS_TIME",
 | |
| 				"CAP_SYS_TTY_CONFIG",
 | |
| 				"CAP_WAKE_ALARM",
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	oci.Version = specs.Version
 | |
| 
 | |
| 	oci.Platform = specs.Platform{
 | |
| 		OS:   inspect.Os,
 | |
| 		Arch: inspect.Architecture,
 | |
| 	}
 | |
| 
 | |
| 	oci.Process = specs.Process{
 | |
| 		Terminal: false,
 | |
| 		//ConsoleSize
 | |
| 		User: specs.User{
 | |
| 			UID:            image.UID,
 | |
| 			GID:            image.GID,
 | |
| 			AdditionalGids: image.AdditionalGids,
 | |
| 			// Username (Windows)
 | |
| 		},
 | |
| 		Args: args,
 | |
| 		Env:  env,
 | |
| 		Cwd:  cwd,
 | |
| 		Capabilities: &specs.LinuxCapabilities{
 | |
| 			Bounding:    caps,
 | |
| 			Effective:   caps,
 | |
| 			Inheritable: caps,
 | |
| 			Permitted:   caps,
 | |
| 			Ambient:     []string{},
 | |
| 		},
 | |
| 		Rlimits:         []specs.LinuxRlimit{},
 | |
| 		NoNewPrivileges: image.NoNewPrivileges,
 | |
| 		// ApparmorProfile
 | |
| 		// SelinuxLabel
 | |
| 	}
 | |
| 
 | |
| 	oci.Root = specs.Root{
 | |
| 		Path:     "rootfs",
 | |
| 		Readonly: image.Readonly,
 | |
| 	}
 | |
| 
 | |
| 	oci.Hostname = image.Hostname
 | |
| 	oci.Mounts = mountList
 | |
| 
 | |
| 	oci.Linux = &specs.Linux{
 | |
| 		// UIDMappings
 | |
| 		// GIDMappings
 | |
| 		Sysctl: image.Sysctl,
 | |
| 		Resources: &specs.LinuxResources{
 | |
| 			// Devices
 | |
| 			DisableOOMKiller: &image.DisableOOMKiller,
 | |
| 			// Memory
 | |
| 			// CPU
 | |
| 			// Pids
 | |
| 			// BlockIO
 | |
| 			// HugepageLimits
 | |
| 			// Network
 | |
| 		},
 | |
| 		CgroupsPath: image.CgroupsPath,
 | |
| 		Namespaces:  namespaces,
 | |
| 		// Devices
 | |
| 		// Seccomp
 | |
| 		RootfsPropagation: image.RootfsPropagation,
 | |
| 		MaskedPaths:       image.MaskedPaths,
 | |
| 		ReadonlyPaths:     image.ReadonlyPaths,
 | |
| 		// MountLabel
 | |
| 		// IntelRdt
 | |
| 	}
 | |
| 
 | |
| 	return json.MarshalIndent(oci, "", "    ")
 | |
| }
 | |
| 
 | |
| func filesystem(m *Moby) (*bytes.Buffer, error) {
 | |
| 	buf := new(bytes.Buffer)
 | |
| 	tw := tar.NewWriter(buf)
 | |
| 	defer tw.Close()
 | |
| 
 | |
| 	if len(m.Files) != 0 {
 | |
| 		log.Infof("Add files:")
 | |
| 	}
 | |
| 	for _, f := range m.Files {
 | |
| 		log.Infof("  %s", f.Path)
 | |
| 		if f.Path == "" {
 | |
| 			return buf, errors.New("Did not specify path for file")
 | |
| 		}
 | |
| 		if !f.Directory && f.Contents == "" && f.Symlink == "" {
 | |
| 			return buf, errors.New("Contents of file not specified")
 | |
| 		}
 | |
| 		// we need all the leading directories
 | |
| 		parts := strings.Split(path.Dir(f.Path), "/")
 | |
| 		root := ""
 | |
| 		for _, p := range parts {
 | |
| 			if p == "." || p == "/" {
 | |
| 				continue
 | |
| 			}
 | |
| 			if root == "" {
 | |
| 				root = p
 | |
| 			} else {
 | |
| 				root = root + "/" + p
 | |
| 			}
 | |
| 			hdr := &tar.Header{
 | |
| 				Name:     root,
 | |
| 				Typeflag: tar.TypeDir,
 | |
| 				Mode:     0700,
 | |
| 			}
 | |
| 			err := tw.WriteHeader(hdr)
 | |
| 			if err != nil {
 | |
| 				return buf, err
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if f.Directory {
 | |
| 			if f.Contents != "" {
 | |
| 				return buf, errors.New("Directory with contents not allowed")
 | |
| 			}
 | |
| 			hdr := &tar.Header{
 | |
| 				Name:     f.Path,
 | |
| 				Typeflag: tar.TypeDir,
 | |
| 				Mode:     0700,
 | |
| 			}
 | |
| 			err := tw.WriteHeader(hdr)
 | |
| 			if err != nil {
 | |
| 				return buf, err
 | |
| 			}
 | |
| 		} else if f.Symlink != "" {
 | |
| 			hdr := &tar.Header{
 | |
| 				Name:     f.Path,
 | |
| 				Typeflag: tar.TypeSymlink,
 | |
| 				Mode:     0600,
 | |
| 				Linkname: f.Symlink,
 | |
| 			}
 | |
| 			err := tw.WriteHeader(hdr)
 | |
| 			if err != nil {
 | |
| 				return buf, err
 | |
| 			}
 | |
| 		} else {
 | |
| 			hdr := &tar.Header{
 | |
| 				Name: f.Path,
 | |
| 				Mode: 0600,
 | |
| 				Size: int64(len(f.Contents)),
 | |
| 			}
 | |
| 			err := tw.WriteHeader(hdr)
 | |
| 			if err != nil {
 | |
| 				return buf, err
 | |
| 			}
 | |
| 			_, err = tw.Write([]byte(f.Contents))
 | |
| 			if err != nil {
 | |
| 				return buf, err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return buf, nil
 | |
| }
 |