Files
linuxkit/cmd/moby/config.go
Justin Cormack c734b47e9c Add support for override of parameters using a label
Using the label `org.mobyproject.config` will use that JSON
(or yaml, but it is very hard to get yaml into a label as newlines are
not respected) for parameters that are not explicitly set in the yaml file.

Had to change parameter definitions so override behaves as expected.

fix #16

Signed-off-by: Justin Cormack <justin.cormack@docker.com>
2017-05-18 17:48:15 +01:00

741 lines
19 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/docker/docker/api/types/container"
"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 `yaml:"name" json:"name"`
Image string `yaml:"image" json:"image"`
Capabilities *[]string `yaml:"capabilities" json:"capabilities,omitempty"`
Mounts *[]specs.Mount `yaml:"mounts" json:"mounts,omitempty"`
Binds *[]string `yaml:"binds" json:"binds,omitempty"`
Tmpfs *[]string `yaml:"tmpfs" json:"tmpfs,omitempty"`
Command *[]string `yaml:"command" json:"command,omitempty"`
Env *[]string `yaml:"env" json:"env,omitempty"`
Cwd string `yaml:"cwd" json:"cwd"`
Net string `yaml:"net" json:"net"`
Pid string `yaml:"pid" json:"pid"`
Ipc string `yaml:"ipc" json:"ipc"`
Uts string `yaml:"uts" json:"uts"`
Hostname string `yaml:"hostname" json:"hostname"`
Readonly *bool `yaml:"readonly" json:"readonly,omitempty"`
MaskedPaths *[]string `yaml:"maskedPaths" json:"maskedPaths,omitempty"`
ReadonlyPaths *[]string `yaml:"readonlyPaths" json:"readonlyPaths,omitempty"`
UID *uint32 `yaml:"uid" json:"uid,omitempty"`
GID *uint32 `yaml:"gid" json:"gid,omitempty"`
AdditionalGids *[]uint32 `yaml:"additionalGids" json:"additionalGids,omitempty"`
NoNewPrivileges *bool `yaml:"noNewPrivileges" json:"noNewPrivileges,omitempty"`
OOMScoreAdj *int `yaml:"oomScoreAdj" json:"oomScoreAdj,omitempty"`
DisableOOMKiller *bool `yaml:"disableOOMKiller" json:"disableOOMKiller,omitempty"`
RootfsPropagation *string `yaml:"rootfsPropagation" json:"rootfsPropagation,omitempty"`
CgroupsPath *string `yaml:"cgroupsPath" json:"cgroupsPath,omitempty"`
Sysctl *map[string]string `yaml:"sysctl" json:"sysctl,omitempty"`
}
// 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
}
// NewImage validates an parses yaml or json for a MobyImage
func NewImage(config []byte) (MobyImage, error) {
log.Debugf("Reading label config: %s", string(config))
mi := MobyImage{}
// Parse raw yaml
var rawYaml interface{}
err := yaml.Unmarshal(config, &rawYaml)
if err != nil {
return mi, err
}
// Convert to raw JSON
rawJSON := convert(rawYaml)
// check it is an object not an array
jsonObject, ok := rawJSON.(map[string]interface{})
if !ok {
return mi, fmt.Errorf("JSON is an array not an object: %s", string(config))
}
// add a dummy name and image to pass validation
var dummyName interface{}
var dummyImage interface{}
dummyName = "dummyname"
dummyImage = "dummyimage"
jsonObject["name"] = dummyName
jsonObject["image"] = dummyImage
// Validate it as {"services": [config]}
var services [1]interface{}
services[0] = rawJSON
serviceJSON := map[string]interface{}{"services": services}
// Validate serviceJSON with JSON schema
schemaLoader := gojsonschema.NewStringLoader(schema)
documentLoader := gojsonschema.NewGoLoader(serviceJSON)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return mi, err
}
if !result.Valid() {
fmt.Printf("The org.mobyproject.config label is invalid:\n")
for _, desc := range result.Errors() {
fmt.Printf("- %s\n", desc)
}
return mi, fmt.Errorf("invalid configuration label")
}
// Parse yaml
err = yaml.Unmarshal(config, &mi)
if err != nil {
return mi, err
}
if mi.Name != "" {
return mi, fmt.Errorf("name cannot be set in metadata label")
}
if mi.Image != "" {
return mi, fmt.Errorf("image cannot be set in metadata label")
}
return mi, 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
}
oci, err := ConfigInspectToOCI(image, inspect)
if err != nil {
return []byte{}, err
}
return json.MarshalIndent(oci, "", " ")
}
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))
}
// assignBool does ordered overrides from JSON bool pointers
func assignBool(v1, v2 *bool) bool {
if v2 != nil {
return *v2
}
if v1 != nil {
return *v1
}
return false
}
// assignBoolPtr does ordered overrides from JSON bool pointers
func assignBoolPtr(v1, v2 *bool) *bool {
if v2 != nil {
return v2
}
if v1 != nil {
return v1
}
return nil
}
// assignIntPtr does ordered overrides from JSON int pointers
func assignIntPtr(v1, v2 *int) *int {
if v2 != nil {
return v2
}
if v1 != nil {
return v1
}
return nil
}
// assignUint32 does ordered overrides from JSON uint32 pointers
func assignUint32(v1, v2 *uint32) uint32 {
if v2 != nil {
return *v2
}
if v1 != nil {
return *v1
}
return 0
}
// assignUint32Array does ordered overrides from JSON uint32 array pointers
func assignUint32Array(v1, v2 *[]uint32) []uint32 {
if v2 != nil {
return *v2
}
if v1 != nil {
return *v1
}
return []uint32{}
}
// assignStrings does ordered overrides from JSON string array pointers
func assignStrings(v1, v2 *[]string) []string {
if v2 != nil {
return *v2
}
if v1 != nil {
return *v1
}
return []string{}
}
// assignStrings3 does ordered overrides from JSON string array pointers
func assignStrings3(v1 []string, v2, v3 *[]string) []string {
if v3 != nil {
return *v3
}
if v2 != nil {
return *v2
}
return v1
}
// assignMaps does ordered overrides from JSON string map pointers
func assignMaps(v1, v2 *map[string]string) map[string]string {
if v2 != nil {
return *v2
}
if v1 != nil {
return *v1
}
return map[string]string{}
}
// assignBinds does ordered overrides from JSON Bind array pointers
func assignBinds(v1, v2 *[]specs.Mount) []specs.Mount {
if v2 != nil {
return *v2
}
if v1 != nil {
return *v1
}
return []specs.Mount{}
}
// assignString does ordered overrides from JSON string pointers
func assignString(v1, v2 *string) string {
if v2 != nil {
return *v2
}
if v1 != nil {
return *v1
}
return ""
}
// assignStringEmpty does ordered overrides if strings are empty, for
// values where there is always an explicit override eg "none"
func assignStringEmpty(v1, v2 string) string {
if v2 != "" {
return v2
}
return v1
}
// assign StringEmpty4 does ordered overrides if strings are empty, for
// values where there is always an explicit override eg "none"
func assignStringEmpty4(v1, v2, v3, v4 string) string {
if v4 != "" {
return v4
}
if v3 != "" {
return v3
}
if v2 != "" {
return v2
}
return v1
}
// emptyNone replaces "none" with the empty string
func emptyNone(v string) string {
if v == "none" {
return ""
}
return v
}
// ConfigInspectToOCI converts a config and the output of image inspect to an OCI config
func ConfigInspectToOCI(yaml MobyImage, inspect types.ImageInspect) (specs.Spec, error) {
oci := specs.Spec{}
var inspectConfig container.Config
if inspect.Config != nil {
inspectConfig = *inspect.Config
}
// look for org.mobyproject.config label
var label MobyImage
labelString := inspectConfig.Labels["org.mobyproject.config"]
if labelString != "" {
var err error
label, err = NewImage([]byte(labelString))
if err != nil {
return oci, err
}
}
// command, env and cwd can be taken from image, as they are commonly specified in Dockerfile
// TODO we could handle entrypoint and cmd independently more like Docker
inspectCommand := append(inspectConfig.Entrypoint, inspect.Config.Cmd...)
args := assignStrings3(inspectCommand, label.Command, yaml.Command)
env := assignStrings3(inspectConfig.Env, label.Env, yaml.Env)
// empty Cwd not allowed in OCI, must be / in that case
cwd := assignStringEmpty4("/", inspectConfig.WorkingDir, label.Cwd, yaml.Cwd)
// the other options will never be in the image config, but may be in label or yaml
readonly := assignBool(label.Readonly, yaml.Readonly)
// default options match what Docker does
procOptions := []string{"nosuid", "nodev", "noexec", "relatime"}
devOptions := []string{"nosuid", "strictatime", "mode=755", "size=65536k"}
if readonly {
devOptions = append(devOptions, "ro")
}
ptsOptions := []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620"}
sysOptions := []string{"nosuid", "noexec", "nodev"}
if 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 assignStrings(label.Tmpfs, yaml.Tmpfs) {
parts := strings.Split(t, ":")
if len(parts) > 2 {
return oci, 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 assignStrings(label.Binds, yaml.Binds) {
parts := strings.Split(b, ":")
if len(parts) < 2 {
return oci, fmt.Errorf("Cannot parse bind, missing ':': %s", b)
}
if len(parts) > 3 {
return oci, 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 assignBinds(label.Mounts, yaml.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 oci, 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 oci, 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
netNS := assignStringEmpty(label.Net, yaml.Net)
if netNS != "host" {
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.NetworkNamespace, Path: emptyNone(netNS)})
}
pidNS := assignStringEmpty(label.Pid, yaml.Pid)
if pidNS != "host" {
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.PIDNamespace, Path: emptyNone(pidNS)})
}
ipcNS := assignStringEmpty(label.Ipc, yaml.Ipc)
if ipcNS != "host" {
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.IPCNamespace, Path: emptyNone(ipcNS)})
}
utsNS := assignStringEmpty(label.Uts, yaml.Uts)
if utsNS != "host" {
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.UTSNamespace, Path: emptyNone(utsNS)})
}
// TODO user, cgroup namespaces, maybe mount=host if useful
namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.MountNamespace})
caps := assignStrings(label.Capabilities, yaml.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: assignUint32(label.UID, yaml.UID),
GID: assignUint32(label.GID, yaml.GID),
AdditionalGids: assignUint32Array(label.AdditionalGids, yaml.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: assignBool(label.NoNewPrivileges, yaml.NoNewPrivileges),
// ApparmorProfile
// TODO FIXME this has moved in runc spec and needs a revendor and update
//OOMScoreAdj: assignIntPtr(label.OOMScoreAdj, yaml.OOMScoreAdj),
// SelinuxLabel
}
oci.Root = specs.Root{
Path: "rootfs",
Readonly: readonly,
}
oci.Hostname = assignStringEmpty(label.Hostname, yaml.Hostname)
oci.Mounts = mountList
oci.Linux = &specs.Linux{
// UIDMappings
// GIDMappings
Sysctl: assignMaps(label.Sysctl, yaml.Sysctl),
Resources: &specs.LinuxResources{
// Devices
DisableOOMKiller: assignBoolPtr(label.DisableOOMKiller, yaml.DisableOOMKiller),
// Memory
// CPU
// Pids
// BlockIO
// HugepageLimits
// Network
},
CgroupsPath: assignString(label.CgroupsPath, yaml.CgroupsPath),
Namespaces: namespaces,
// Devices
// Seccomp
RootfsPropagation: assignString(label.RootfsPropagation, yaml.RootfsPropagation),
MaskedPaths: assignStrings(label.MaskedPaths, yaml.MaskedPaths),
ReadonlyPaths: assignStrings(label.ReadonlyPaths, yaml.ReadonlyPaths),
// MountLabel
// IntelRdt
}
return oci, nil
}
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
}