Skip to content

Commit

Permalink
WIP: translate: Stub in a translation framework
Browse files Browse the repository at this point in the history
Add tooling to translate higher-level configs into the basic OCI
config.  On IRC, Julz floated a linux.namespaces[].fromContainer as a
higher-level version of linux.namespaces[].path for emulating exec
[1].  That makes sense to me, and Mrunal is open to something like
[2]:

  $ ocitools generate --template=high-level-config.json --translate=fromContainer --runtime=runc

The interface{} -> rspec.Spec conversion happens through a JSON
serialization trick suggested by 梁辰晔 (Liang Chenye) [3] after the
translations, because before the translations the template may contain
JSON that's not represented in the OCI Spec structure
(e.g. fromContainer in namespace entries).

This commit still needs (marked by FIXMEs):

* Lookup /proc in /proc/self/mounts (recursive?).
* Compare /proc with state JSON namespace [4].
* Adjustments to setupNamespaces to avoid clobbering the template.
* Better handling for user-namespace injection.
* A way to handle nested definition lists in man pages.  I expect we
  should drop go-md2man in favor of a more established man-page format
  (e.g. AsciiDoc).

[1]: http://ircbot.wl.linuxfoundation.org/eavesdrop/%23opencontainers/%23opencontainers.2016-04-27.log.html#t2016-04-27T20:32:09
[2]: http://ircbot.wl.linuxfoundation.org/eavesdrop/%23opencontainers/%23opencontainers.2016-04-28.log.html#t2016-04-28T16:12:48
[3]: opencontainers#54 (comment)
[4]: https://groups.google.com/a/opencontainers.org/forum/#!topic/dev/ujtABQoCmgk
     Subject: Linux: Adding the PID namespace inode to state JSON
     Date: Wed, 30 Dec 2015 21:34:57 -0800
     Message-ID: <[email protected]>

Signed-off-by: W. Trevor King <[email protected]>
  • Loading branch information
wking committed May 3, 2016
1 parent 8ab86b6 commit dae2098
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 6 deletions.
39 changes: 33 additions & 6 deletions generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -54,7 +55,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"},
}

Expand Down Expand Up @@ -92,12 +95,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)
}
Expand All @@ -107,7 +133,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) {
Expand Down Expand Up @@ -464,6 +490,7 @@ func addIDMappings(spec *rspec.Spec, context *cli.Context) error {
}

if len(context.StringSlice("uidmappings")) > 0 || len(context.StringSlice("gidmappings")) > 0 {
/* FIXME: move to setupNamespaces so we can avoid creating a duplicate 'user' entry */
spec.Linux.Namespaces = append(spec.Linux.Namespaces, rspec.Namespace{Type: "user"})
}

Expand Down Expand Up @@ -670,12 +697,12 @@ func setupNamespaces(spec *rspec.Spec, context *cli.Context) {
ns := mapStrToNamespace(nsName, nsPath)
linuxNs = append(linuxNs, ns)
}
spec.Linux.Namespaces = linuxNs
spec.Linux.Namespaces = linuxNs // FIXME: don't clobber the template namespaces
}

func sPtr(s string) *string { return &s }

func getDefaultTemplate() *rspec.Spec {
func getDefaultTemplate() interface{} {
spec := rspec.Spec{
Version: rspec.Version,
Platform: rspec.Platform{
Expand Down Expand Up @@ -790,5 +817,5 @@ func getDefaultTemplate() *rspec.Spec {
},
}

return &spec
return spec
}
13 changes: 13 additions & 0 deletions man/ocitools-generate.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,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.
Expand Down Expand Up @@ -179,6 +182,16 @@ 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:

**fromContainer**
FIXME: This needs to be indented as a sub-definition-list.
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`**.

**--uid**=UID
Sets the UID used within the container.

Expand Down
95 changes: 95 additions & 0 deletions translate/from_container.go
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 20 additions & 0 deletions translate/translate.go
Original file line number Diff line number Diff line change
@@ -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,
}

0 comments on commit dae2098

Please sign in to comment.