Skip to content

Commit dae2098

Browse files
committed
WIP: translate: Stub in a translation framework
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]: #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]>
1 parent 8ab86b6 commit dae2098

File tree

4 files changed

+161
-6
lines changed

4 files changed

+161
-6
lines changed

generate.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/codegangsta/cli"
1414
rspec "github.com/opencontainers/runtime-spec/specs-go"
1515
"github.com/syndtr/gocapability/capability"
16+
"github.com/opencontainers/ocitools/translate"
1617
)
1718

1819
var generateFlags = []cli.Flag{
@@ -54,7 +55,9 @@ var generateFlags = []cli.Flag{
5455
cli.StringSliceFlag{Name: "seccomp-arch", Usage: "specifies Additional architectures permitted to be used for system calls"},
5556
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 "},
5657
cli.StringSliceFlag{Name: "seccomp-allow", Usage: "specifies syscalls to be added to allowed"},
58+
cli.StringFlag{Name: "runtime", Usage: "select the runtime command (used for some translations)"},
5759
cli.StringFlag{Name: "template", Usage: "base template to use for creating the configuration"},
60+
cli.StringSliceFlag{Name: "translate", Usage: "translate higher level constructs"},
5861
cli.StringSliceFlag{Name: "label", Usage: "add annotations to the configuration e.g. key=value"},
5962
}
6063

@@ -92,12 +95,35 @@ var generateCommand = cli.Command{
9295
}
9396
}
9497

95-
err := modify(spec, context)
98+
translations := context.StringSlice("translate")
99+
for _, translation := range translations {
100+
translator, ok := translate.Translators[translation]
101+
if !ok {
102+
logrus.Fatalf("unrecognized translation: %s", translation)
103+
}
104+
var err error
105+
spec, err = translator(spec, context)
106+
if err != nil {
107+
logrus.Fatal(err)
108+
}
109+
}
110+
111+
buf, err := json.Marshal(spec)
112+
if err != nil {
113+
logrus.Fatal(err)
114+
}
115+
var strictSpec rspec.Spec
116+
err = json.Unmarshal(buf, &strictSpec)
117+
if err != nil {
118+
logrus.Fatal(err)
119+
}
120+
121+
err = modify(&strictSpec, context)
96122
if err != nil {
97123
logrus.Fatal(err)
98124
}
99125
cName := "config.json"
100-
data, err := json.MarshalIndent(&spec, "", "\t")
126+
data, err := json.MarshalIndent(&strictSpec, "", "\t")
101127
if err != nil {
102128
logrus.Fatal(err)
103129
}
@@ -107,7 +133,7 @@ var generateCommand = cli.Command{
107133
},
108134
}
109135

110-
func loadTemplate(path string) (spec *rspec.Spec, err error) {
136+
func loadTemplate(path string) (spec interface{}, err error) {
111137
cf, err := os.Open(path)
112138
if err != nil {
113139
if os.IsNotExist(err) {
@@ -464,6 +490,7 @@ func addIDMappings(spec *rspec.Spec, context *cli.Context) error {
464490
}
465491

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

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

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

678-
func getDefaultTemplate() *rspec.Spec {
705+
func getDefaultTemplate() interface{} {
679706
spec := rspec.Spec{
680707
Version: rspec.Version,
681708
Platform: rspec.Platform{
@@ -790,5 +817,5 @@ func getDefaultTemplate() *rspec.Spec {
790817
},
791818
}
792819

793-
return &spec
820+
return spec
794821
}

man/ocitools-generate.1.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ inside of the container.
139139
**--rootfs**="*ROOTFSPATH*"
140140
Path to the rootfs
141141

142+
**--runtime**=COMMAND
143+
Set the runtime command, which is used for state JSON queries in translations like **--translate=fromContainer**.
144+
142145
**--seccomp-arch**=ARCH
143146
Specifies Additional architectures permitted to be used for system calls.
144147
By default if you turn on seccomp, only the host architecture will be allowed.
@@ -179,6 +182,16 @@ inside of the container.
179182
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:
180183
`rw,noexec,nosuid,nodev,size=65536k`.
181184

185+
**--translate**=TRANSLATION
186+
Apply various higher-level spec translations.
187+
Available translations:
188+
189+
**fromContainer**
190+
FIXME: This needs to be indented as a sub-definition-list.
191+
The base OCI spec requires a namespace path in **`linux.namespaces[].path`** to join a namespace.
192+
However, looking up that path in `/proc` can be tedious.
193+
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`**.
194+
182195
**--uid**=UID
183196
Sets the UID used within the container.
184197

translate/from_container.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package translate
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"os/exec"
8+
"path/filepath"
9+
10+
"github.com/codegangsta/cli"
11+
rspec "github.com/opencontainers/runtime-spec/specs-go"
12+
)
13+
14+
func FromContainer(data interface{}, context *cli.Context) (translated interface{}, err error) {
15+
dataMap, ok := data.(map[string]interface{})
16+
if !ok {
17+
return nil, fmt.Errorf("data is not a map[string]interface{}: %s", data)
18+
}
19+
20+
linuxInterface, ok := dataMap["linux"]
21+
if !ok {
22+
return data, nil
23+
}
24+
25+
linux, ok := linuxInterface.(map[string]interface{})
26+
if !ok {
27+
return nil, fmt.Errorf("data.linux is not a map[string]interface{}: %s", linuxInterface)
28+
}
29+
30+
namespacesInterface, ok := linux["namespaces"]
31+
if !ok {
32+
return data, nil
33+
}
34+
35+
namespaces, ok := namespacesInterface.([]interface{})
36+
if !ok {
37+
return nil, fmt.Errorf("data.linux.namespaces is not an array: %s", namespacesInterface)
38+
}
39+
40+
for index, namespaceInterface := range namespaces {
41+
namespace, ok := namespaceInterface.(map[string]interface{})
42+
if !ok {
43+
return nil, fmt.Errorf("data.linux.namespaces[%d] is not a map[string]interface{}: %s", index, namespaceInterface)
44+
}
45+
err := namespaceFromContainer(&namespace, index, context)
46+
if err != nil {
47+
return nil, err
48+
}
49+
}
50+
51+
return data, nil
52+
}
53+
54+
func namespaceFromContainer(namespace *map[string]interface{}, index int, context *cli.Context) (err error) {
55+
fromContainerInterface, ok := (*namespace)["fromContainer"]
56+
if ok {
57+
fromContainer, ok := fromContainerInterface.(string)
58+
if !ok {
59+
return fmt.Errorf("data.linux.namespaces[%d].fromContainer is not a string: %s", index, fromContainerInterface)
60+
}
61+
delete(*namespace, "fromContainer")
62+
runtime := context.String("runtime")
63+
if (len(runtime) == 0) {
64+
return fmt.Errorf("translating fromContainer requires a non-empty --runtime")
65+
}
66+
command := exec.Command(runtime, "state", fromContainer)
67+
var out bytes.Buffer
68+
command.Stdout = &out
69+
err := command.Run()
70+
if err != nil {
71+
return err
72+
}
73+
var state rspec.State
74+
err = json.Unmarshal(out.Bytes(), &state)
75+
if err != nil {
76+
return err
77+
}
78+
namespaceTypeInterface, ok := (*namespace)["type"]
79+
if !ok {
80+
return fmt.Errorf("data.linux.namespaces[%d].type is missing: %s", index, fromContainerInterface)
81+
}
82+
namespaceType, ok := namespaceTypeInterface.(string)
83+
if !ok {
84+
return fmt.Errorf("data.linux.namespaces[%d].type is not a string: %s", index, namespaceTypeInterface)
85+
}
86+
switch namespaceType {
87+
case "network": namespaceType = "net"
88+
case "mount": namespaceType = "mnt"
89+
}
90+
proc := "/proc" // FIXME: lookup in /proc/self/mounts, check right namespace
91+
path := filepath.Join(proc, fmt.Sprint(state.Pid), "ns", namespaceType)
92+
(*namespace)["path"] = path
93+
}
94+
return nil
95+
}

translate/translate.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
Package translate handles translation between configuration
3+
specifications.
4+
5+
For example, it allows you to generate OCI-compliant config.json from
6+
a higher-level configuration language.
7+
*/
8+
package translate
9+
10+
import (
11+
"github.com/codegangsta/cli"
12+
)
13+
14+
// Translate maps JSON from one specification to another.
15+
type Translate func(data interface{}, context *cli.Context) (translated interface{}, err error)
16+
17+
// Translators is a map from translator names to Translate functions.
18+
var Translators = map[string]Translate{
19+
"fromContainer": FromContainer,
20+
}

0 commit comments

Comments
 (0)