diff --git a/go.mod b/go.mod index 962def7b8..fe8b270b8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/threefoldtech/zos go 1.13 require ( + github.com/BurntSushi/toml v0.3.1 github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 github.com/alexflint/go-filemutex v0.0.0-20171028004239-d358565f3c3f diff --git a/pkg/container.go b/pkg/container.go index ee3b95273..a76801cfe 100644 --- a/pkg/container.go +++ b/pkg/container.go @@ -34,6 +34,8 @@ type Container struct { RootFS string // Env env variables to container in format {'KEY=VALUE', 'KEY2=VALUE2'} Env []string + // WorkingDir of the entrypoint command + WorkingDir string // Network network info for container Network NetworkInfo // Mounts extra mounts for container diff --git a/pkg/container/container.go b/pkg/container/container.go index 9d6f30b11..33b1b21ac 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -3,6 +3,7 @@ package container import ( "context" + "github.com/BurntSushi/toml" "github.com/pkg/errors" "fmt" @@ -62,12 +63,7 @@ func New(root string, containerd string) pkg.ContainerModule { } // Run creates and starts a container -// THIS IS A WIP Create action and it's not fully implemented atm func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID, err error) { - log.Info(). - Str("namesapce", ns). - Str("data", fmt.Sprintf("%+v", data)). - Msgf("create new container") // create a new client connected to the default socket path for containerd client, err := containerd.New(c.containerd) if err != nil { @@ -85,8 +81,8 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID return id, ErrEmptyRootFS } - if err := setResolvConf(data.RootFS); err != nil { - return id, errors.Wrap(err, "failed to set resolv.conf") + if err := applyStartup(&data, filepath.Join(data.RootFS, ".startup.toml")); err != nil { + errors.Wrap(err, "error updating environment variable from startup file") } if data.Interactive { @@ -113,8 +109,14 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID oci.WithRootFSPath(data.RootFS), oci.WithProcessArgs(args...), oci.WithEnv(data.Env), + oci.WithHostResolvconf, removeRunMount(), } + + if data.WorkingDir != "" { + opts = append(opts, oci.WithProcessCwd(data.WorkingDir)) + } + if data.Interactive { opts = append( opts, @@ -152,6 +154,11 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID ) } + log.Info(). + Str("namespace", ns). + Str("data", fmt.Sprintf("%+v", data)). + Msgf("create new container") + container, err := client.NewContainer( ctx, data.Name, @@ -166,6 +173,7 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID return id, err } log.Info().Msgf("args %+v", spec.Process.Args) + log.Info().Msgf("env %+v", spec.Process.Env) log.Info().Msgf("root %+v", spec.Root) for _, linxNS := range spec.Linux.Namespaces { log.Info().Msgf("namespace %+v", linxNS.Type) @@ -298,3 +306,46 @@ func (c *containerModule) Delete(ns string, id pkg.ContainerID) error { return container.Delete(ctx) } + +func (c *containerModule) ensureNamespace(ctx context.Context, client *containerd.Client, namespace string) error { + service := client.NamespaceService() + namespaces, err := service.List(ctx) + if err != nil { + return err + } + + for _, ns := range namespaces { + if ns == namespace { + return nil + } + } + + return service.Create(ctx, namespace, nil) +} + +func applyStartup(data *pkg.Container, path string) error { + f, err := os.Open(path) + if err == nil { + defer f.Close() + log.Info().Msg("startup file found") + + startup := startup{} + if _, err := toml.DecodeReader(f, &startup); err != nil { + return err + } + + entry, ok := startup.Entries["entry"] + if !ok { + return nil + } + + data.Env = mergeEnvs(entry.Envs(), data.Env) + if data.Entrypoint == "" && entry.Entrypoint() != "" { + data.Entrypoint = entry.Entrypoint() + } + if data.WorkingDir == "" && entry.WorkingDir() != "" { + data.WorkingDir = entry.WorkingDir() + } + } + return nil +} diff --git a/pkg/container/spec_opts.go b/pkg/container/opts.go similarity index 62% rename from pkg/container/spec_opts.go rename to pkg/container/opts.go index f1b0e6da7..f0ceb3ce5 100644 --- a/pkg/container/spec_opts.go +++ b/pkg/container/opts.go @@ -2,14 +2,13 @@ package container import ( "context" - "os" + "path" - "path/filepath" - "github.com/containerd/containerd" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" - "github.com/opencontainers/runtime-spec/specs-go" ) // withNetworkNamespace set the named network namespace to use for the container @@ -57,22 +56,6 @@ func withAddedCapabilities(caps []string) oci.SpecOpts { } } -func (c *containerModule) ensureNamespace(ctx context.Context, client *containerd.Client, namespace string) error { - service := client.NamespaceService() - namespaces, err := service.List(ctx) - if err != nil { - return err - } - - for _, ns := range namespaces { - if ns == namespace { - return nil - } - } - - return service.Create(ctx, namespace, nil) -} - func removeRunMount() oci.SpecOpts { return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { for i, mount := range s.Mounts { @@ -84,29 +67,3 @@ func removeRunMount() oci.SpecOpts { return nil } } - -func setResolvConf(root string) error { - const tmp = "nameserver 1.1.1.1\nnameserver 1.0.0.1\n2606:4700:4700::1111\nnameserver 2606:4700:4700::1001\n" - - path := filepath.Join(root, "etc/resolv.conf") - f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 644) - if err != nil && !os.IsNotExist(err) { - return err - } - - defer f.Close() - - if os.IsNotExist(err) { - _, err = f.WriteString(tmp) - return err - } - - info, err := f.Stat() - if err != nil { - return err - } - if info.Size() == 0 { - _, err = f.WriteString(tmp) - } - return err -} diff --git a/pkg/container/startup.go b/pkg/container/startup.go new file mode 100644 index 000000000..fa85f45bd --- /dev/null +++ b/pkg/container/startup.go @@ -0,0 +1,63 @@ +package container + +import ( + "fmt" + "strings" +) + +type startup struct { + Entries map[string]entry `toml:"startup"` +} + +type entry struct { + Name string + Args args +} + +type args struct { + Name string + Dir string + Env map[string]string +} + +func (e entry) Entrypoint() string { + if e.Name == "core.system" || + e.Name == "core.base" && e.Args.Name != "" { + return e.Args.Name + } + return "" +} + +func (e entry) WorkingDir() string { + return e.Args.Dir +} + +func (e entry) Envs() []string { + envs := make([]string, 0, len(e.Args.Env)) + for k, v := range e.Args.Env { + envs = append(envs, fmt.Sprintf("%s=%s", k, v)) + } + return envs +} + +// mergeEnvs merge a into b +// all the key from a will endup in b +// if a key is present in both, key from a are kept +func mergeEnvs(a, b []string) []string { + m := make(map[string]string, len(a)+len(b)) + + for _, s := range b { + ss := strings.SplitN(s, "=", 2) + m[ss[0]] = ss[1] + } + for _, s := range a { + ss := strings.SplitN(s, "=", 2) + m[ss[0]] = ss[1] + } + + result := make([]string, 0, len(m)) + for k, v := range m { + result = append(result, fmt.Sprintf("%s=%s", k, v)) + } + return result +} diff --git a/pkg/container/startup_test.go b/pkg/container/startup_test.go new file mode 100644 index 000000000..3313c358a --- /dev/null +++ b/pkg/container/startup_test.go @@ -0,0 +1,96 @@ +package container + +import ( + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/BurntSushi/toml" + "github.com/stretchr/testify/require" +) + +var _startup = `[startup] + +[startup.entry] +name = "core.system" + +[startup.entry.args] +name = "/start" +dir = "/data" + +[startup.entry.args.env] +DIFFICULTY = "easy" +LEVEL = "world" +SERVER_PORT = "25565" +` + +func TestParseStartup(t *testing.T) { + r := strings.NewReader(_startup) + e := startup{} + _, err := toml.DecodeReader(r, &e) + require.NoError(t, err) + + entry, ok := e.Entries["entry"] + require.True(t, ok) + assert.Equal(t, "core.system", entry.Name) + assert.Equal(t, "/start", entry.Args.Name) + assert.Equal(t, "/data", entry.Args.Dir) + assert.Equal(t, map[string]string{ + "DIFFICULTY": "easy", + "LEVEL": "world", + "SERVER_PORT": "25565", + }, entry.Args.Env) +} + +func TestStartupEntrypoint(t *testing.T) { + r := strings.NewReader(_startup) + e := startup{} + _, err := toml.DecodeReader(r, &e) + require.NoError(t, err) + + entry, ok := e.Entries["entry"] + require.True(t, ok) + assert.Equal(t, entry.Entrypoint(), "/start") +} + +func TestStartupEnvs(t *testing.T) { + r := strings.NewReader(_startup) + e := startup{} + _, err := toml.DecodeReader(r, &e) + require.NoError(t, err) + + entry, ok := e.Entries["entry"] + require.True(t, ok) + actual := entry.Envs() + sort.Strings(actual) + expected := []string{ + "DIFFICULTY=easy", + "LEVEL=world", + "SERVER_PORT=25565", + } + assert.Equal(t, expected, actual) +} + +func TestStartupWorkingDir(t *testing.T) { + r := strings.NewReader(_startup) + e := startup{} + _, err := toml.DecodeReader(r, &e) + require.NoError(t, err) + + entry, ok := e.Entries["entry"] + require.True(t, ok) + assert.Equal(t, entry.WorkingDir(), "/data") +} + +func TestMergeEnvs(t *testing.T) { + actual := mergeEnvs( + []string{"FOO=BAR", "HELLO=WORLD"}, + []string{"HELLO=HELLO"}, + ) + + expected := []string{"FOO=BAR", "HELLO=WORLD"} + sort.Strings(actual) + assert.Equal(t, expected, actual) +}