Replace riddler with code that constructs config.json directly

Generated largely from the specified config; small parts taken from `docker image inspect`,
such as the command line.

Renamed some of the yaml keys to match the OCI spec rather than Docker Compose as
we decided they are more readable, no more underscores.

Add some extra functionality
- tmpfs specification
- fully general mount specification
- no new privileges can be specified now

For nostalgic reasons, using engine-api to talk to the docker cli as
we only need an old API version, and it is nice and easy to vendor...

Signed-off-by: Justin Cormack <justin.cormack@docker.com>
This commit is contained in:
Justin Cormack 2017-04-03 18:40:48 +01:00
parent 1477639e09
commit d293eeadf6
4 changed files with 239 additions and 63 deletions

View File

@ -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)

View File

@ -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) {

View File

@ -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
}

View File

@ -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