diff --git a/src/cmd/moby/build.go b/src/cmd/moby/build.go index 5fffb34f6..17d7a3350 100644 --- a/src/cmd/moby/build.go +++ b/src/cmd/moby/build.go @@ -117,7 +117,7 @@ func buildInternal(name string, pull bool, conf string) { log.Infof(" Create OCI config for %s", image.Image) config, err := ConfigToOCI(&image) if err != nil { - log.Fatalf("Failed to run riddler to get config.json for %s: %v", image.Image, err) + log.Fatalf("Failed to create config.json for %s: %v", image.Image, err) } so := fmt.Sprintf("%03d", i) path := "containers/system/" + so + "-" + image.Name @@ -141,7 +141,7 @@ func buildInternal(name string, pull bool, conf string) { log.Infof(" Create OCI config for %s", image.Image) config, err := ConfigToOCI(&image) if err != nil { - log.Fatalf("Failed to run riddler to get config.json for %s: %v", image.Image, err) + log.Fatalf("Failed to create config.json for %s: %v", image.Image, err) } path := "containers/daemon/" + image.Name out, err := ImageBundle(path, image.Image, config) diff --git a/src/cmd/moby/config.go b/src/cmd/moby/config.go index de0966bff..2ea34e57a 100644 --- a/src/cmd/moby/config.go +++ b/src/cmd/moby/config.go @@ -3,13 +3,14 @@ package main import ( "archive/tar" "bytes" + "encoding/json" "errors" "fmt" "path" - "strconv" "strings" log "github.com/Sirupsen/logrus" + "github.com/opencontainers/runtime-spec/specs-go" "gopkg.in/yaml.v2" ) @@ -36,23 +37,31 @@ type Moby struct { } } -// MobyImage is the type of an image config, based on Compose +// MobyImage is the type of an image config type MobyImage struct { - Name string - Image string - Capabilities []string - Binds []string - OomScoreAdj int64 `yaml:"oom_score_adj"` - Command []string - NetworkMode string `yaml:"network_mode"` - Pid string - Ipc string - Uts string - ReadOnly bool `yaml:"read_only"` + Name string + Image string + Capabilities []string + Mounts []specs.Mount + Binds []string + Tmpfs []string + Args []string + Env []string + Cwd string + Net string + Pid string + Ipc string + Uts string + Readonly bool + 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"` } -const riddler = "mobylinux/riddler:decf6c9e24b579175a038a76f9721e7aca507abd@sha256:9d24a7c48204b94b5d76cc3d6cf70f779d87d08d8a893169292c98d0e19ab579" - // NewConfig parses a config file func NewConfig(config []byte) (*Moby, error) { m := Moby{} @@ -66,53 +75,183 @@ func NewConfig(config []byte) (*Moby, error) { } // ConfigToOCI converts a config specification to an OCI config file -func ConfigToOCI(image *MobyImage) (string, error) { - // riddler arguments - args := []string{"-v", "/var/run/docker.sock:/var/run/docker.sock", riddler, image.Image} - // docker arguments - args = append(args, "--cap-drop", "all") - for _, cap := range image.Capabilities { - if strings.ToUpper(cap)[0:4] == "CAP_" { - cap = cap[4:] - } - args = append(args, "--cap-add", cap) - } - if image.OomScoreAdj != 0 { - args = append(args, "--oom-score-adj", strconv.FormatInt(image.OomScoreAdj, 10)) - } - if image.NetworkMode != "" { - // TODO only "host" supported - args = append(args, "--net="+image.NetworkMode) - } - if image.Pid != "" { - // TODO only "host" supported - args = append(args, "--pid="+image.Pid) - } - if image.Ipc != "" { - // TODO only "host" supported - args = append(args, "--ipc="+image.Ipc) - } - if image.Uts != "" { - // TODO only "host" supported - args = append(args, "--uts="+image.Uts) - } - for _, bind := range image.Binds { - args = append(args, "-v", bind) - } - if image.ReadOnly { - args = append(args, "--read-only") - } - // image - args = append(args, image.Image) - // command - args = append(args, image.Command...) +func ConfigToOCI(image *MobyImage) ([]byte, error) { + oci := specs.Spec{} - config, err := dockerRun(args...) + // TODO pass through same docker client to all functions + cli, err := dockerClient() if err != nil { - return "", fmt.Errorf("Failed to run riddler to get config.json: %v", err) + return []byte{}, err } - return string(config), nil + inspect, err := dockerInspectImage(cli, image.Image) + if err != nil { + return []byte{}, err + } + + config := inspect.Config + if config == nil { + return []byte{}, errors.New("empty image config") + } + + args := append(config.Entrypoint, config.Cmd...) + if len(image.Args) != 0 { + args = image.Args + } + env := config.Env + if len(image.Env) != 0 { + env = image.Env + } + cwd := config.WorkingDir + if image.Cwd != "" { + cwd = image.Cwd + } + if cwd == "" { + cwd = "/" + } + 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 := []specs.Mount{ + {Destination: "/proc", Type: "proc", Source: "proc"}, + {Destination: "/dev", Type: "tmpfs", Source: "tmpfs", Options: devOptions}, + {Destination: "/dev/pts", Type: "devpts", Source: "devpts", Options: ptsOptions}, + {Destination: "/sys", Type: "sysfs", Source: "sysfs", Options: sysOptions}, + {Destination: "/sys/fs/cgroup", Type: "cgroup", Source: "cgroup", Options: cgroupOptions}, + } + // TODO if any standard mount points supplied, remove from above, so can change options + mounts = append(mounts, image.Mounts...) + 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 = append(mounts, 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 = append(mounts, specs.Mount{Destination: dest, Type: "bind", Source: src, Options: opts}) + } + + namespaces := []specs.LinuxNamespace{} + if image.Net != "" && image.Net != "host" { + return []byte{}, fmt.Errorf("invalid net namespace: %s", image.Net) + } + if image.Net == "" { + namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.NetworkNamespace}) + } + if image.Pid != "" && image.Pid != "host" { + return []byte{}, fmt.Errorf("invalid pid namespace: %s", image.Pid) + } + if image.Pid == "" { + namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.PIDNamespace}) + } + if image.Ipc != "" && image.Ipc != "host" { + return []byte{}, fmt.Errorf("invalid ipc namespace: %s", image.Ipc) + } + if image.Ipc == "" { + namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.IPCNamespace}) + } + if image.Uts != "" && image.Uts != "host" { + return []byte{}, fmt.Errorf("invalid uts namespace: %s", image.Uts) + } + if image.Uts == "" { + namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.UTSNamespace}) + } + // TODO user, cgroup namespaces, maybe mount=host if useful + namespaces = append(namespaces, specs.LinuxNamespace{Type: specs.MountNamespace}) + + 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: image.Capabilities, + Effective: image.Capabilities, + Inheritable: image.Capabilities, + Permitted: image.Capabilities, + 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 = mounts + + oci.Linux = &specs.Linux{ + // UIDMappings + // GIDMappings + // Sysctl + Resources: &specs.LinuxResources{ + // Devices + DisableOOMKiller: &image.DisableOOMKiller, + // Memory + // CPU + // Pids + // BlockIO + // HugepageLimits + // Network + }, + // CgroupsPath + Namespaces: namespaces, + // Devices + // Seccomp + // RootfsPropagation + // MaskedPaths + // ReadonlyPaths + // MountLabel + // IntelRdt + } + + return json.MarshalIndent(oci, "", " ") } func filesystem(m *Moby) (*bytes.Buffer, error) { diff --git a/src/cmd/moby/docker.go b/src/cmd/moby/docker.go index 096f11347..a10de3e1a 100644 --- a/src/cmd/moby/docker.go +++ b/src/cmd/moby/docker.go @@ -8,10 +8,14 @@ import ( "fmt" "io" "io/ioutil" + "os" "os/exec" "strings" log "github.com/Sirupsen/logrus" + "github.com/docker/engine-api/client" + "github.com/docker/engine-api/types" + "golang.org/x/net/context" ) func dockerRun(args ...string) ([]byte, error) { @@ -274,3 +278,36 @@ func dockerPull(image string) error { log.Debugf("docker pull: %s...Done", image) return nil } + +func dockerClient() (*client.Client, error) { + // for maximum compatibility as we use nothing new + err := os.Setenv("DOCKER_API_VERSION", "1.23") + if err != nil { + return nil, err + } + return client.NewEnvClient() +} + +func dockerInspectImage(cli *client.Client, image string) (types.ImageInspect, error) { + log.Debugf("docker inspect image: %s", image) + + inspect, _, err := cli.ImageInspectWithRaw(context.Background(), image, false) + if err != nil { + if client.IsErrImageNotFound(err) { + pullErr := dockerPull(image) + if pullErr != nil { + return types.ImageInspect{}, pullErr + } + inspect, _, err = cli.ImageInspectWithRaw(context.Background(), image, false) + if err != nil { + return types.ImageInspect{}, err + } + } else { + return types.ImageInspect{}, err + } + } + + log.Debugf("docker inspect image: %s...Done", image) + + return inspect, nil +} diff --git a/src/cmd/moby/image.go b/src/cmd/moby/image.go index c7c303e2e..c00f0b528 100644 --- a/src/cmd/moby/image.go +++ b/src/cmd/moby/image.go @@ -143,8 +143,8 @@ func imageTar(image, prefix string, tw *tar.Writer) error { } // ImageBundle produces an OCI bundle at the given path in a tarball, given an image and a config.json -func ImageBundle(path, image, config string) ([]byte, error) { - log.Debugf("image bundle: %s %s cfg: %s", path, image, config) +func ImageBundle(path string, image string, config []byte) ([]byte, error) { + log.Debugf("image bundle: %s %s cfg: %s", path, image, string(config)) out := new(bytes.Buffer) tw := tar.NewWriter(out) err := tarPrefix(path+"/rootfs/", tw) @@ -160,7 +160,7 @@ func ImageBundle(path, image, config string) ([]byte, error) { if err != nil { return []byte{}, err } - buf := bytes.NewBufferString(config) + buf := bytes.NewBuffer(config) _, err = io.Copy(tw, buf) if err != nil { return []byte{}, err