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 00aab6375..e1050bdd7 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -3,10 +3,10 @@ package container import ( "context" + "github.com/BurntSushi/toml" "github.com/pkg/errors" "fmt" - "io" "os" "path" "path/filepath" @@ -14,8 +14,6 @@ import ( "syscall" "time" - "github.com/BurntSushi/toml" - "github.com/rs/zerolog/log" "github.com/opencontainers/runtime-spec/specs-go" @@ -65,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 { @@ -92,6 +85,10 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID 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 { if err := os.MkdirAll(filepath.Join(data.RootFS, "sandbox"), 0770); err != nil { return id, err @@ -118,6 +115,11 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID oci.WithEnv(data.Env), removeRunMount(), } + + if data.WorkingDir != "" { + opts = append(opts, oci.WithProcessCwd(data.WorkingDir)) + } + if data.Interactive { opts = append( opts, @@ -155,6 +157,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, @@ -303,54 +310,71 @@ func (c *containerModule) Delete(ns string, id pkg.ContainerID) error { return container.Delete(ctx) } -// readEnvs reads the environment variable from the statup.toml file -func readEnvs(r io.Reader) ([]string, error) { - env := struct { - Startup struct { - Entry struct { - Name string `json:"name"` - Args struct { - Name string `json:"name"` - Dir string `json:"dir"` - Env map[string]string `json:"env"` - } `json:"args"` - } `json:"entry"` - } `json:"startup"` - }{} - if _, err := toml.DecodeReader(r, &env); err != nil { - return nil, err - } - - result := make([]string, 0, len(env.Startup.Entry.Args.Env)) - for k, v := range env.Startup.Entry.Args.Env { - result = append(result, fmt.Sprintf("%s=%s", k, v)) - } - return result, nil +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) } -// 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 { - ma := make(map[string]string, len(a)) - mb := make(map[string]string, len(b)) +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" - for _, s := range a { - ss := strings.SplitN(s, "=", 2) - ma[ss[0]] = ss[1] + 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 } - for _, s := range b { - ss := strings.SplitN(s, "=", 2) - mb[ss[0]] = ss[1] + + defer f.Close() + + if os.IsNotExist(err) { + _, err = f.WriteString(tmp) + return err } - for ka, va := range ma { - mb[ka] = va + info, err := f.Stat() + if err != nil { + return err } + if info.Size() == 0 { + _, err = f.WriteString(tmp) + } + return err +} - result := make([]string, 0, len(mb)) - for k, v := range mb { - result = append(result, fmt.Sprintf("%s=%s", k, v)) +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 result + return nil } diff --git a/pkg/container/container_test.go b/pkg/container/container_test.go deleted file mode 100644 index 70a2f8d65..000000000 --- a/pkg/container/container_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package container - -import ( - "sort" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "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 TestReadEnvs(t *testing.T) { - r := strings.NewReader(_startup) - - envs, err := readEnvs(r) - require.NoError(t, err) - assert.Equal(t, []string{ - "DIFFICULTY=easy", - "LEVEL=world", - "SERVER_PORT=25565", - }, envs) -} - -func TestMergeEnvs(t *testing.T) { - env := mergeEnvs( - []string{"FOO=BAR", "HELLO=WORLD"}, - []string{"HELLO=HELLO"}, - ) - - assert.Equal(t, - sort.StringSlice([]string{"HELLO=WORLD", "FOO=BAR"}), - sort.StringSlice(env)) -} 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..3e00c4e87 --- /dev/null +++ b/pkg/container/startup.go @@ -0,0 +1,68 @@ +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 { + ma := make(map[string]string, len(a)) + mb := make(map[string]string, len(b)) + + for _, s := range a { + ss := strings.SplitN(s, "=", 2) + ma[ss[0]] = ss[1] + } + for _, s := range b { + ss := strings.SplitN(s, "=", 2) + mb[ss[0]] = ss[1] + } + + for ka, va := range ma { + mb[ka] = va + } + + result := make([]string, 0, len(mb)) + for k, v := range mb { + 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) +}