Skip to content
This repository was archived by the owner on Apr 28, 2020. It is now read-only.

Add on_start label for running a command every time the container starts #219

Merged
merged 3 commits into from
Jul 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions hat-examples/on_start/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM codercom/ubuntu-dev

# The command in the on_start label will be run immediately after the
# project starts. You could use this to reinstall dependencies or
# perform any other bootstrapping tasks.
LABEL on_start="touch did_on_start"
10 changes: 10 additions & 0 deletions internal/dockutil/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ func Exec(cntName, cmd string, args ...string) *exec.Cmd {
return exec.Command("docker", args...)
}

func ExecDir(cntName, dir, cmd string, args ...string) *exec.Cmd {
args = append([]string{"exec", "-w", dir, "-i", cntName, cmd}, args...)
return exec.Command("docker", args...)
}

func ExecTTY(cntName, dir, cmd string, args ...string) *exec.Cmd {
args = append([]string{"exec", "-w", dir, "-it", cntName, cmd}, args...)
return exec.Command("docker", args...)
Expand All @@ -25,6 +30,11 @@ func DetachedExec(cntName, cmd string, args ...string) *exec.Cmd {
return exec.Command("docker", args...)
}

func DetachedExecDir(cntName, dir, cmd string, args ...string) *exec.Cmd {
args = append([]string{"exec", "-dw", dir, cntName, cmd}, args...)
return exec.Command("docker", args...)
}

func ExecEnv(cntName string, envs []string, cmd string, args ...string) *exec.Cmd {
args = append([]string{"exec", "-e", strings.Join(envs, ","), "-i", cntName, cmd}, args...)
return exec.Command("docker", args...)
Expand Down
44 changes: 43 additions & 1 deletion runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"golang.org/x/xerrors"

"go.coder.com/flog"
"go.coder.com/sail/internal/dockutil"
)

// containerLogPath is the location of the code-server log.
Expand All @@ -44,6 +45,12 @@ const (
proxyURLLabel = sailLabel + ".proxy_url"
)

// Docker labels for user configuration.
const (
onStartLabel = "on_start"
projectRootLabel = "project_root"
)

// runner holds all the information needed to assemble a new sail container.
// The runner stores itself as state on the container.
// It enables quick iteration on a container with small modifications to it's config.
Expand All @@ -68,6 +75,8 @@ type runner struct {
// the container's root process.
// We want code-server to be the root process as it gives us the nice guarantee that
// the container is only online when code-server is working.
// Additionally, runContainer also runs the image's `on_start` label as a bash
// command inside of the project directory.
func (r *runner) runContainer(image string) error {
cli := dockerClient()
defer cli.Close()
Expand Down Expand Up @@ -131,6 +140,11 @@ func (r *runner) runContainer(image string) error {
return xerrors.Errorf("failed to start container: %w", err)
}

err = r.runOnStart(image)
if err != nil {
return xerrors.Errorf("failed to run on_start label in container: %w", err)
}

return nil
}

Expand Down Expand Up @@ -457,7 +471,7 @@ func (r *runner) projectDir(image string) (string, error) {
return "", xerrors.Errorf("failed to inspect image: %w", err)
}

proot, ok := img.Config.Labels["project_root"]
proot, ok := img.Config.Labels[projectRootLabel]
if ok {
return filepath.Join(proot, r.projectName), nil
}
Expand Down Expand Up @@ -491,6 +505,34 @@ func runnerFromContainer(name string) (*runner, error) {
}, nil
}

// runOnStart runs the image's `on_start` label in the container in the project directory.
func (r *runner) runOnStart(image string) error {
cli := dockerClient()
defer cli.Close()

// Get project directory.
projectDir, err := r.projectDir(image)
if err != nil {
return err
}
projectDir = resolvePath(containerHome, projectDir)

// Get on_start label from image.
img, _, err := cli.ImageInspectWithRaw(context.Background(), image)
if err != nil {
return xerrors.Errorf("failed to inspect image: %w", err)
}
onStartCmd, ok := img.Config.Labels[onStartLabel]
if !ok {
// No on_start label, so we quit early.
return nil
}

// Execute the command detached in the container.
cmd := dockutil.DetachedExecDir(r.cntName, projectDir, "/bin/bash", "-c", onStartCmd)
return cmd.Run()
}

func (r *runner) forkProxy() error {
var err error
r.proxyURL, err = forkProxy(r.cntName)
Expand Down
31 changes: 30 additions & 1 deletion runner_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package main

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.coder.com/sail/internal/dockutil"
)

func Test_runner(t *testing.T) {
// Ensure that the testing environment won't conflict with any running sail projects.
requireProjectsNotRunning(t, "cdr/nbin", "cdr/flog", "cdr/bigdur", "cdr/sshcode")
requireProjectsNotRunning(t, "cdr/nbin", "cdr/flog", "cdr/bigdur", "cdr/sshcode", "cdr/cli")
requireUbuntuDevImage(t)

// labelChecker asserts that all of the correct labels
Expand Down Expand Up @@ -73,6 +76,23 @@ func Test_runner(t *testing.T) {
})
}

// containsFile ensures that a container contains a file.
// This is used for testing the on_start label.
containsFile := func(name, path string) func(*testing.T, *params) {
return func(t *testing.T, p *params) {
t.Run(name, func(t *testing.T) {
cntDir, err := p.proj.containerDir()
require.NoError(t, err)
cntDir = resolvePath(containerHome, cntDir)

// Run the file existence check using /bin/sh.
cmdStr := fmt.Sprintf(`[ -f "%s" ]`, path)
err = dockutil.ExecDir(p.proj.cntName(), cntDir, "/bin/sh", "-c", cmdStr).Run()
require.NoError(t, err)
})
}
}

run(t, "BaseImageNoHat", "https://github.com/cdr/nbin", "",
labelChecker,
codeServerStarts,
Expand All @@ -96,4 +116,13 @@ func Test_runner(t *testing.T) {
codeServerStarts,
loadFromContainer,
)

run(t, "ProjImageOnStartHat", "https://github.com/cdr/cli", "./hat-examples/on_start",
labelChecker,
codeServerStarts,
loadFromContainer,

// ./hat-examples/on_start should create `did_on_start` in the project directory.
containsFile("ContainsOnStartFile", "did_on_start"),
)
}
24 changes: 24 additions & 0 deletions site/content/docs/concepts/labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ LABEL project_root "~/go/src/"

Will bind mount the host directory `$project_root/<org>/<repo>` to `~/go/src/<repo>` in the container.

### On Start Labels

You can run a command in your sail container after it starts by specifying
the `on_start` label. If you'd like to run multiple commands on launch, we
recommend using a `.sh` file as your `on_start` label, as you cannot
provide multiple `on_start` labels in your image.

The `on_start` label is run detached inside of `/bin/bash` as soon as the
container is started, with the work directory set to your `project_root`
(see the section above).

For example:
```Dockerfile
LABEL on_start "npm install"
```
```Dockerfile
LABEL on_start "go get"
```
```Dockerfile
LABEL on_start "./.sail/on_start.sh"
```

Make sure any scripts you make are executable, otherwise sail will fail to
launch.

### Share Labels

Expand Down
2 changes: 1 addition & 1 deletion versionmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

var version string

type versioncmd struct {}
type versioncmd struct{}

func (v *versioncmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
Expand Down