diff --git a/generate.go b/generate.go index 5bb74b53f..52a25a63c 100644 --- a/generate.go +++ b/generate.go @@ -13,6 +13,7 @@ import ( "github.com/codegangsta/cli" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/syndtr/gocapability/capability" + "github.com/opencontainers/ocitools/translate" ) var generateFlags = []cli.Flag{ @@ -55,7 +56,9 @@ var generateFlags = []cli.Flag{ cli.StringSliceFlag{Name: "seccomp-arch", Usage: "specifies Additional architectures permitted to be used for system calls"}, cli.StringSliceFlag{Name: "seccomp-syscalls", Usage: "specifies Additional architectures permitted to be used for system calls, e.g Name:Action:Arg1_index/Arg1_value/Arg1_valuetwo/Arg1_op, Arg2_index/Arg2_value/Arg2_valuetwo/Arg2_op "}, cli.StringSliceFlag{Name: "seccomp-allow", Usage: "specifies syscalls to be added to allowed"}, + cli.StringFlag{Name: "runtime", Usage: "select the runtime command (used for some translations)"}, cli.StringFlag{Name: "template", Usage: "base template to use for creating the configuration"}, + cli.StringSliceFlag{Name: "translate", Usage: "translate higher level constructs"}, cli.StringSliceFlag{Name: "label", Usage: "add annotations to the configuration e.g. key=value"}, } @@ -93,12 +96,35 @@ var generateCommand = cli.Command{ } } - err := modify(spec, context) + translations := context.StringSlice("translate") + for _, translation := range translations { + translator, ok := translate.Translators[translation] + if !ok { + logrus.Fatalf("unrecognized translation: %s", translation) + } + var err error + spec, err = translator(spec, context) + if err != nil { + logrus.Fatal(err) + } + } + + buf, err := json.Marshal(spec) + if err != nil { + logrus.Fatal(err) + } + var strictSpec rspec.Spec + err = json.Unmarshal(buf, &strictSpec) + if err != nil { + logrus.Fatal(err) + } + + err = modify(&strictSpec, context) if err != nil { logrus.Fatal(err) } cName := "config.json" - data, err := json.MarshalIndent(&spec, "", "\t") + data, err := json.MarshalIndent(&strictSpec, "", "\t") if err != nil { logrus.Fatal(err) } @@ -108,7 +134,7 @@ var generateCommand = cli.Command{ }, } -func loadTemplate(path string) (spec *rspec.Spec, err error) { +func loadTemplate(path string) (spec interface{}, err error) { cf, err := os.Open(path) if err != nil { if os.IsNotExist(err) { @@ -709,7 +735,7 @@ func removeNamespace(namespaces *[]rspec.Namespace, namespaceType rspec.Namespac func sPtr(s string) *string { return &s } -func getDefaultTemplate() *rspec.Spec { +func getDefaultTemplate() interface{} { spec := rspec.Spec{ Version: rspec.Version, Platform: rspec.Platform{ @@ -824,5 +850,5 @@ func getDefaultTemplate() *rspec.Spec { }, } - return &spec + return spec } diff --git a/man/ocitools-generate.1.md b/man/ocitools-generate.1.md index fc6edb700..e4d02cca7 100644 --- a/man/ocitools-generate.1.md +++ b/man/ocitools-generate.1.md @@ -147,6 +147,9 @@ inside of the container. **--rootfs**=ROOTFSPATH Path to the rootfs +**--runtime**=COMMAND + Set the runtime command, which is used for state JSON queries in translations like **--translate=fromContainer**. + **--seccomp-arch**=ARCH Specifies Additional architectures permitted to be used for system calls. By default if you turn on seccomp, only the host architecture will be allowed. @@ -187,6 +190,10 @@ inside of the container. This command mounts a `tmpfs` at `/tmp` within the container. The supported mount options are the same as the Linux default `mount` flags. If you do not specify any options, the systems uses the following options: `rw,noexec,nosuid,nodev,size=65536k`. +**--translate**=TRANSLATION + Apply various higher-level spec translations. Available + translations are listed in the *Translations* section. + **--uid**=UID Sets the UID used within the container. @@ -203,6 +210,18 @@ inside of the container. is unset, create a new namespace. The special *PATH* `host` removes any existing UTS namespace from the configuration. +## Translations + +**fromContainer** +The base OCI spec requires a namespace path in + `linux.namespaces[].path` to join a namespace. However, looking up + that path in `/proc` can be tedious. With a target container ID in + `linux.namespaces[].fromContainer`, the **fromContainer** + translation will query the runtime (set with **--runtime**) for the + state JSON, extract the container PID from that JSON, find the + appropriate namespace path for that PID in `/proc`, and insert that + path in the translated configuration as `linux.namespaces[].path`. + # EXAMPLES ## Generating container in read-only mode diff --git a/translate/from_container.go b/translate/from_container.go new file mode 100644 index 000000000..d57843e53 --- /dev/null +++ b/translate/from_container.go @@ -0,0 +1,95 @@ +package translate + +import ( + "bytes" + "encoding/json" + "fmt" + "os/exec" + "path/filepath" + + "github.com/codegangsta/cli" + rspec "github.com/opencontainers/runtime-spec/specs-go" +) + +func FromContainer(data interface{}, context *cli.Context) (translated interface{}, err error) { + dataMap, ok := data.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("data is not a map[string]interface{}: %s", data) + } + + linuxInterface, ok := dataMap["linux"] + if !ok { + return data, nil + } + + linux, ok := linuxInterface.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("data.linux is not a map[string]interface{}: %s", linuxInterface) + } + + namespacesInterface, ok := linux["namespaces"] + if !ok { + return data, nil + } + + namespaces, ok := namespacesInterface.([]interface{}) + if !ok { + return nil, fmt.Errorf("data.linux.namespaces is not an array: %s", namespacesInterface) + } + + for index, namespaceInterface := range namespaces { + namespace, ok := namespaceInterface.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("data.linux.namespaces[%d] is not a map[string]interface{}: %s", index, namespaceInterface) + } + err := namespaceFromContainer(&namespace, index, context) + if err != nil { + return nil, err + } + } + + return data, nil +} + +func namespaceFromContainer(namespace *map[string]interface{}, index int, context *cli.Context) (err error) { + fromContainerInterface, ok := (*namespace)["fromContainer"] + if ok { + fromContainer, ok := fromContainerInterface.(string) + if !ok { + return fmt.Errorf("data.linux.namespaces[%d].fromContainer is not a string: %s", index, fromContainerInterface) + } + delete(*namespace, "fromContainer") + runtime := context.String("runtime") + if (len(runtime) == 0) { + return fmt.Errorf("translating fromContainer requires a non-empty --runtime") + } + command := exec.Command(runtime, "state", fromContainer) + var out bytes.Buffer + command.Stdout = &out + err := command.Run() + if err != nil { + return err + } + var state rspec.State + err = json.Unmarshal(out.Bytes(), &state) + if err != nil { + return err + } + namespaceTypeInterface, ok := (*namespace)["type"] + if !ok { + return fmt.Errorf("data.linux.namespaces[%d].type is missing: %s", index, fromContainerInterface) + } + namespaceType, ok := namespaceTypeInterface.(string) + if !ok { + return fmt.Errorf("data.linux.namespaces[%d].type is not a string: %s", index, namespaceTypeInterface) + } + switch namespaceType { + case "network": namespaceType = "net" + case "mount": namespaceType = "mnt" + } + proc := "/proc" // FIXME: lookup in /proc/self/mounts, check right namespace + path := filepath.Join(proc, fmt.Sprint(state.Pid), "ns", namespaceType) + (*namespace)["path"] = path + } + return nil +} diff --git a/translate/translate.go b/translate/translate.go new file mode 100644 index 000000000..bbf826beb --- /dev/null +++ b/translate/translate.go @@ -0,0 +1,20 @@ +/* +Package translate handles translation between configuration +specifications. + +For example, it allows you to generate OCI-compliant config.json from +a higher-level configuration language. +*/ +package translate + +import ( + "github.com/codegangsta/cli" +) + +// Translate maps JSON from one specification to another. +type Translate func(data interface{}, context *cli.Context) (translated interface{}, err error) + +// Translators is a map from translator names to Translate functions. +var Translators = map[string]Translate{ + "fromContainer": FromContainer, +}