Move stuff to the new command generator
--- a/command/command.go Sun Jan 21 21:44:40 2018 -0600
+++ b/command/command.go Sun Feb 11 22:22:46 2018 -0600
@@ -19,15 +19,10 @@
- "github.com/kballard/go-shellquote"
"bitbucket.org/rw_grim/convey/logging"
"github.com/aphistic/gomol"
@@ -38,19 +33,19 @@
// Run runs the command specified in cmdTemplate which is rendered with the
// given params and logs stderr and stdout to a new log adapter.
-func Run(name, cmdTemplate string, params map[string]interface{}, timeout time.Duration) error {
+func Run(name string, cmdv []string, timeout time.Duration) error { logger = logging.NewAdapter(name)
outCollector = newLogCollector(logger, gomol.LevelInfo)
errCollector = newLogCollector(logger, gomol.LevelError)
- return run(name, cmdTemplate, params, timeout, outCollector, errCollector)
+ return run(name, cmdv, timeout, outCollector, errCollector) // RunOutput works just like Run but returns stdout and stderr instead of
-func RunOutput(name, cmdTemplate string, params map[string]interface{}, timeout time.Duration) (string, string, error) {
+func RunOutput(name string, cmdv []string, timeout time.Duration) (string, string, error) { outCollector = newStringCollector(wg)
@@ -58,43 +53,15 @@
- err := run(name, cmdTemplate, params, timeout, outCollector.handler, errCollector.handler)
+ err := run(name, cmdv, timeout, outCollector.handler, errCollector.handler) return outCollector.output, errCollector.output, err
-func execTemplate(name, cmdTemplate string, params map[string]interface{}) ([]string, error) {
- // we use multiline comments to make the code readable, which leaves
- // embedding newlines, so remove those.
- cleanTemplate := strings.Replace(cmdTemplate, "\n", "", -1)
- // go's template stuff is *NOT* thread safe, so we need to lock around it
- tmpl, err := template.New(name).Parse(cleanTemplate)
- // now execute the template
- cmd := new(bytes.Buffer)
- err = tmpl.Execute(cmd, params)
- return shellquote.Split(cmd.String())
-func run(name, cmdTemplate string, params map[string]interface{}, timeout time.Duration, outHandler, errHandler collector) error {
+func run(name string, cmdv []string, timeout time.Duration, outHandler, errHandler collector) error { logger := logging.NewAdapter(name)
- cmdv, err := execTemplate(name, cmdTemplate, params)
logger.Debugf("running command \"%v\"", cmdv)
cmd := exec.Command(cmdv[0], cmdv[1:]...)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/command/generator.go Sun Feb 11 22:22:46 2018 -0600
@@ -0,0 +1,136 @@
+// Copyright 2016-2018 Gary Kramlich <grim@reaperworld.com> +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// Package command provides utilities for running external commands. +// NewGenerator creates a new generator initialized with the gives arguments. +func NewGenerator(args ...string) *Generator { + args: make([]string, minAlloc), +// resize will resize the slice to the required size or minimum allocation +// which ever is larger. +func (g *Generator) resize(reqSize int) { + if g.next >= len(g.args) { + if reqSize > minAlloc { + newArgs := make([]string, newSize) + for idx, item := range g.args { +// Append appends the given arguments to the end of the arguments. +func (g *Generator) Append(args ...string) { +// Appendv appends the give string slice to the command +func (g *Generator) Appendv(args []string) { + for _, item := range args { +// Prepend prepends the given arguments to the beginning of the arguments. +func (g *Generator) Prepend(args ...string) { +// Prependv prepends the given string slice to the command. +func (g *Generator) Prependv(args []string) { + // allocate a new slice that's the size of the + newSize := int(math.Max(float64(len(args)), float64(minAlloc))) + len(g.args) + newArgs := make([]string, newSize) + // set our current index to 0 + // add the prepended items + for ; idx < len(args); idx++ { + newArgs[idx] = args[idx] + // add the original items + for ; idx < g.next+offset; idx++ { + newArgs[idx] = g.args[idx-offset] +// Command returns the currently built command. +func (g *Generator) Command() []string { + ret := make([]string, g.next) + for i := 0; i < g.next; i++ { --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/command/generator_test.go Sun Feb 11 22:22:46 2018 -0600
@@ -0,0 +1,113 @@
+// Copyright 2016-2018 Gary Kramlich <grim@reaperworld.com> +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + "github.com/aphistic/sweet" + junit "github.com/aphistic/sweet-junit" + . "github.com/onsi/gomega" +type generatorSuite struct{} +func TestMain(m *testing.M) { + RegisterFailHandler(sweet.GomegaFail) + sweet.Run(m, func(s *sweet.S) { + s.RegisterPlugin(junit.NewPlugin()) + s.AddSuite(&generatorSuite{}) +func (s *generatorSuite) TestWithoutArgs(t sweet.T) { + Expect(g).ToNot(BeNil()) +func (s *generatorSuite) TestWithArgs(t sweet.T) { + Expect(g.Command()).To(BeEquivalentTo([]string{"a"})) + g = NewGenerator("a", "b", "c") + Expect(g.Command()).To(BeEquivalentTo([]string{"a", "b", "c"})) +func (s *generatorSuite) TestAppend(t sweet.T) { + Expect(g.Command()).To(BeEquivalentTo([]string{"a", "b", "c"})) + g = NewGenerator("a", "b") + Expect(g.Command()).To(BeEquivalentTo([]string{"a", "b", "c"})) + g.Append("b", "c", "d", "e", "f", "g", "h", "i", "j", "k") + Expect(g.Command()).To(BeEquivalentTo([]string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"})) +func (s *generatorSuite) TestPrepend(t sweet.T) { + g := NewGenerator("b", "c") + Expect(g.Command()).To(BeEquivalentTo([]string{"a", "b", "c"})) + g.Prepend("a", "b", "c", "d", "e", "f", "g", "h", "i", "j") + Expect(g.Command()).To(BeEquivalentTo([]string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"})) +func (s *generatorSuite) TestMultiplePrepends(t sweet.T) { + g := NewGenerator("u", "v", "w", "x", "y", "z") + g.Prepend("k", "l", "m", "n", "o", "p", "q", "r", "s", "t") + g.Prepend("a", "b", "c", "d", "e", "f", "g", "h", "i", "j") + Expect(g.Command()).To(BeEquivalentTo([]string{ + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", + "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", + "u", "v", "w", "x", "y", "z", +func (s *generatorSuite) TestAppendv(t sweet.T) { + Expect(NewGenerator().Command()).To(BeEquivalentTo([]string{})) + cmd := NewGenerator("a", "b", "c") + cmd.Appendv([]string{"d", "e", "f"}) + Expect(cmd.Command()).To(BeEquivalentTo([]string{"a", "b", "c", "d", "e", "f"})) +func (s *generatorSuite) TestMixedAppend(t sweet.T) { + cmd := NewGenerator("a") + cmd.Appendv([]string{"b", "c"}) + Expect(cmd.Command()).To(BeEquivalentTo([]string{"a", "b", "c"})) +func (s *generatorSuite) TestPrependv(t sweet.T) { + cmd := NewGenerator("c") + cmd.Prependv([]string{"a", "b"}) + Expect(cmd.Command()).To(BeEquivalentTo([]string{"a", "b", "c"})) + cmd = NewGenerator("a") + cmd.Prependv([]string{}) + Expect(cmd.Command()).To(BeEquivalentTo([]string{"a"})) --- a/docker/build.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/build.go Sun Feb 11 22:22:46 2018 -0600
@@ -22,6 +22,7 @@
"github.com/aphistic/gomol"
+ "bitbucket.org/rw_grim/convey/command" "bitbucket.org/rw_grim/convey/environment"
"bitbucket.org/rw_grim/convey/state"
"bitbucket.org/rw_grim/convey/tasks"
@@ -39,14 +40,6 @@
Arguments yaml.StringOrSlice `yaml:"arguments"`
-const buildTemplate = `build
-{{range .Tags}} -t {{.}}{{end}}
-{{range .Labels}} --label '{{.}}'{{end}}
-{{range .Arguments}} --build-arg {{.}}{{end}}
-{{if .Target}} --target {{.Target}}{{end}}
// Execute runs the docker build command
func (b *Build) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error {
fullEnv := environment.Merge(env, st.GetEnv())
@@ -82,13 +75,7 @@
// copy the dockerfile to the temp directory
- params := map[string]interface{}{
- "destination": buildDir,
- "workspace": st.Workspace.Name(),
- if err := Docker(name, exportTemplate, params, st); err != nil {
+ if err := exportFile(name, st.Workspace.Name(), dockerfile, buildDir, st); err != nil { @@ -107,17 +94,36 @@
- params = map[string]interface{}{
- "dockerfile": filepath.Join(buildDir, filepath.Base(dockerfile)),
- "buildContext": buildDir,
- "Arguments": arguments,
+ // create the basic command + cmd := command.NewGenerator( + "-f", filepath.Join(buildDir, filepath.Base(dockerfile)), + // add any and all tags + for _, tag := range tags { - return Docker(name, buildTemplate, params, st)
+ // add any and all labels + for _, label := range labels { + cmd.Append("--label", label) + // add any and all build arguments + for _, arg := range arguments { + cmd.Append("--build-arg", arg) + // add the target if we have one + cmd.Append("--target", b.Target) + // finally add the build context + return Docker(name, cmd.Command(), st) // New creates a new docker build task.
--- a/docker/docker.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/docker.go Sun Feb 11 22:22:46 2018 -0600
@@ -38,23 +38,30 @@
"environment": &Environment{},
- dockerTemplate = `docker {{if .DockerConfig}}--config {{.DockerConfig}} {{end}}`
+func dockerCommand(cmdv []string, st *state.State) []string { + cmd := command.NewGenerator() + // if we have a docker config prepend it first + if st.DockerConfig != "" { + cmd.Prepend("--config", st.DockerConfig) + // finally prepend the actual docker command // Docker runs a docker command.
-func Docker(name, template string, params map[string]interface{}, st *state.State) error {
- fullTemplate := dockerTemplate + template
- params["DockerConfig"] = st.DockerConfig
- return command.Run(name, fullTemplate, params, st.PlanTimeout)
+func Docker(name string, cmdv []string, st *state.State) error { + return command.Run(name, dockerCommand(cmdv, st), st.PlanTimeout) // DockerOutput runs a docker command but returns the output rather than
// outputting to the logger.
-func DockerOutput(name, template string, params map[string]interface{}, st *state.State) (string, string, error) {
- fullTemplate := dockerTemplate + template
- params["DockerConfig"] = st.DockerConfig
- return command.RunOutput(name, fullTemplate, params, st.PlanTimeout)
+func DockerOutput(name string, cmdv []string, st *state.State) (string, string, error) { + return command.RunOutput(name, dockerCommand(cmdv, st), st.PlanTimeout) --- a/docker/export.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/export.go Sun Feb 11 22:22:46 2018 -0600
@@ -37,9 +37,6 @@
Files yaml.StringOrSlice `yaml:"files"`
-const exportTemplate = `cp {{.workspace}}:/workspace/{{.source}} {{.destination}}`
-const zglobTemplate = `run --rm -v {{.workspace}}:/workspace convey/workspace-tools:latest zglob {{.file}}`
func checkFilePattern(file string) error {
if strings.ContainsRune(file, '*') && strings.ContainsRune(file, ':') {
return errWildcardWithDestination
@@ -49,27 +46,35 @@
func exportFile(name, workSpace, src, dest string, st *state.State) error {
- params := map[string]interface{}{
- "workspace": workSpace,
dir := filepath.Dir(dest)
if err := os.MkdirAll(dir, 0777); err != nil {
- return Docker(name, exportTemplate, params, st)
+ // build out the source path + source := workSpace + ":" + filepath.Join("/workspace", src) + return Docker(name, cmdv, st) func exportGlob(name, workSpace, mountPoint, pattern string, st *state.State) error {
- params := map[string]interface{}{
- "workspace": mountPoint,
+ mountPoint + ":/workspace", + "convey/workspace-tools:latest", - out, _, err := DockerOutput(fmt.Sprintf("%s-zglob", name), zglobTemplate, params, st)
+ out, _, err := DockerOutput(fmt.Sprintf("%s-zglob", name), cmdv, st) --- a/docker/healthcheck.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/healthcheck.go Sun Feb 11 22:22:46 2018 -0600
@@ -34,27 +34,15 @@
Timeout time.Duration `yaml:"timeout"`
- containerHasHealthCheckTemplate = `inspect -f "{{.Format}}" {{.Cid}}`
- containerHasHealthCheckFormat = `{{if .Config.Healthcheck}}true{{else}}false{{end}}`
- containerIsHealthyTemplate = `inspect -f "{{.Format}}" {{.Cid}}`
- containerIsHealthyFormat = `{{.State.Health.Status}}`
func containerHasHealthCheck(name, cid string, st *state.State, logger *gomol.LogAdapter) (bool, error) {
- params := map[string]interface{}{
- "Format": containerHasHealthCheckFormat,
+ "\"{{if .Config.Healthcheck}}true{{else}}false{{end}}\"", - stdout, stderr, err := DockerOutput(
- containerHasHealthCheckTemplate,
+ stdout, stderr, err := DockerOutput(name+"/healthcheck", cmdv, st) @@ -69,18 +57,14 @@
func containerIsHealthy(name, cid string, st *state.State, logger *gomol.LogAdapter) (bool, error) {
- params := map[string]interface{}{
- "Format": containerIsHealthyFormat,
+ "\"{{.State.Health.Status}}\"", - stdout, stderr, err := DockerOutput(
- containerIsHealthyTemplate,
+ stdout, stderr, err := DockerOutput(name+"/healthcheck", cmdv, st) --- a/docker/import.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/import.go Sun Feb 11 22:22:46 2018 -0600
@@ -32,8 +32,6 @@
Files yaml.StringOrSlice `yaml:"files"`
-const importTemplate = `cp {{.source}} {{.workspaceID}}:/workspace/{{.destination}}`
// Execute runs the import task.
func (i *Import) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error {
fullEnv := environment.Merge(env, st.GetEnv())
@@ -57,13 +55,13 @@
- params := map[string]interface{}{
- "workspaceID": st.Workspace.Name(),
+ st.Workspace.Name() + ":/workspace/" + dest, - if err := Docker(name, importTemplate, params, st); err != nil {
+ if err := Docker(name, cmdv, st); err != nil { --- a/docker/login.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/login.go Sun Feb 11 22:22:46 2018 -0600
@@ -19,6 +19,7 @@
"github.com/aphistic/gomol"
+ "bitbucket.org/rw_grim/convey/command" "bitbucket.org/rw_grim/convey/environment"
"bitbucket.org/rw_grim/convey/state"
"bitbucket.org/rw_grim/convey/tasks"
@@ -31,8 +32,6 @@
Server string `yaml:"server"`
-const loginTemplate = `login -u {{.username}} -p {{.password}}{{if .server}} {{.server}}{{end}}`
// Execute runs the login task.
func (l *Login) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error {
fullEnv := environment.Merge(env, st.GetEnv())
@@ -52,13 +51,17 @@
- params := map[string]interface{}{
+ cmd := command.NewGenerator( - return Docker(name, loginTemplate, params, st)
+ return Docker(name, cmd.Command(), st) // New creates a new login task.
--- a/docker/logout.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/logout.go Sun Feb 11 22:22:46 2018 -0600
@@ -19,6 +19,7 @@
"github.com/aphistic/gomol"
+ "bitbucket.org/rw_grim/convey/command" "bitbucket.org/rw_grim/convey/environment"
"bitbucket.org/rw_grim/convey/state"
"bitbucket.org/rw_grim/convey/tasks"
@@ -29,8 +30,6 @@
Server string `yaml:"server"`
-const logoutTemplate = `logout{{if .server}} {{.server}}{{end}}`
// Execute runs the logout task.
func (l *Logout) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error {
fullEnv := environment.Merge(env, st.GetEnv())
@@ -40,11 +39,13 @@
- params := map[string]interface{}{
+ cmd := command.NewGenerator("logout") - return Docker(name, logoutTemplate, params, st)
+ return Docker(name, cmd.Command(), st) // New creates a new logout task.
--- a/docker/network.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/network.go Sun Feb 11 22:22:46 2018 -0600
@@ -34,12 +34,6 @@
- networkCreateTemplate = `network create {{.Name}}`
- networkDisconnectTemplate = `network disconnect --force {{.Name}} {{.ContainerName}}`
- networkDestroyTemplate = `network rm {{.Name}}`
// NewNetwork creates a new docker network.
func NewNetwork(st *state.State) (*Network, error) {
@@ -48,11 +42,9 @@
- params := map[string]interface{}{
+ cmdv := []string{"network", "create", network.name} - _, _, err := DockerOutput("create network", networkCreateTemplate, params, st)
+ _, _, err := DockerOutput("create network", cmdv, st) @@ -70,13 +62,16 @@
// Destroy tears down the docker network.
func (network *Network) Destroy() error {
for _, name := range network.state.GetRunning() {
- params := map[string]interface{}{
network.logger.Infof("disconnecting container %s from network %s", name, network.Name())
- err := Docker("disconnect container", networkDisconnectTemplate, params, network.state)
+ err := Docker("disconnect container", cmdv, network.state) network.logger.Warningf("failed to disconnect container %s from network %s", name, network.Name())
@@ -86,10 +81,6 @@
network.logger.Infof("disconnected container %s from network %s", name, network.Name())
- params := map[string]interface{}{
// monkey with the timeout so our cleanup always runs
oldTimeout := network.state.PlanTimeout
@@ -97,5 +88,7 @@
network.state.PlanTimeout = 15 * time.Minute
- return Docker("remove network", networkDestroyTemplate, params, network.state)
+ cmdv := []string{"network", "rm", network.name} + return Docker("remove network", cmdv, network.state) --- a/docker/pull.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/pull.go Sun Feb 11 22:22:46 2018 -0600
@@ -32,8 +32,6 @@
Images yaml.StringOrSlice `yaml:"images"`
-const pullTemplate = `pull {{.image}}`
// Execute runs the pull task.
func (p *Pull) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error {
fullEnv := environment.Merge(env, st.GetEnv())
@@ -44,11 +42,9 @@
for _, image := range images {
- params := map[string]interface{}{
+ cmdv := []string{"pull", image} - if err := Docker(name, pullTemplate, params, st); err != nil {
+ if err := Docker(name, cmdv, st); err != nil { --- a/docker/push.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/push.go Sun Feb 11 22:22:46 2018 -0600
@@ -31,8 +31,6 @@
Images yaml.StringOrSlice `yaml:"images"`
-const pushTemplate = `push {{.image}}`
// Execute runs the push task.
func (p *Push) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error {
fullEnv := environment.Merge(env, st.GetEnv())
@@ -43,11 +41,9 @@
for _, image := range images {
- params := map[string]interface{}{
+ cmdv := []string{"push", image} - if err := Docker(name, pushTemplate, params, st); err != nil {
+ if err := Docker(name, cmdv, st); err != nil { --- a/docker/remove.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/remove.go Sun Feb 11 22:22:46 2018 -0600
@@ -34,8 +34,6 @@
Quiet bool `yaml:"quiet"`
-const removeTemplate = `rmi {{.image}}`
// Execute runs the remove task.
func (r *Remove) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error {
fullEnv := environment.Merge(env, st.GetEnv())
@@ -46,11 +44,9 @@
for _, image := range images {
- params := map[string]interface{}{
+ cmdv := []string{"rmi", image} - if _, stderr, err := DockerOutput(name, removeTemplate, params, st); err != nil {
+ if _, stderr, err := DockerOutput(name, cmdv, st); err != nil { if strings.Contains(stderr, "No such image") && r.Quiet {
--- a/docker/run.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/run.go Sun Feb 11 22:22:46 2018 -0600
@@ -27,6 +27,7 @@
"github.com/aphistic/gomol"
+ "bitbucket.org/rw_grim/convey/command" "bitbucket.org/rw_grim/convey/environment"
"bitbucket.org/rw_grim/convey/normalize"
"bitbucket.org/rw_grim/convey/state"
@@ -66,34 +67,6 @@
-const runTemplate = `run --rm
-{{if .Name}} --name {{.Name}}{{end}}
-{{if .UID}} -e UID={{.UID}}{{end}}
-{{if .GID}} -e GID={{.GID}}{{end}}
-{{if .Detach}} -d{{end}}
-{{if .Hostname}} --network-alias {{.Hostname}}{{end}}
- -v {{.WorkspacePath}}:{{.WorkspaceMount}}
- -e CONVEY_WORKSPACE={{.WorkspaceMount}}
-{{if .CPUShares }} --cpu-shares {{.CPUShares}}{{end}}
-{{if .Memory }} --memory {{.Memory}}{{end}}
-{{if .ScriptFile }} -v {{.ScriptFile}}:{{.ScriptFile}}{{end}}
-{{if .SSHAgent }} -e SSH_AUTH_SOCK -v {{.SSHAuthSock}}:{{.SSHAuthSock}}{{end}}
-{{if .EntryPoint}} --entrypoint {{.EntryPoint}}{{end}}
-{{if .WorkDir}} -w {{.WorkDir}}{{end}}
-{{if .Network}} --network {{.Network}}{{end}}
-{{range .Labels}} -l '{{.}}'{{end}}
-{{range .Environment}} -e {{.}}{{end}}
-{{if .Username}} --user {{.Username}}{{end}}
-{{if .HealthCheck.Command}} --health-cmd "{{.HealthCheck.Command}}"{{end}}
-{{if .HealthCheck.Interval}} --health-interval {{.HealthCheck.Interval}}{{end}}
-{{if .HealthCheck.Retries}} --health-retries {{.HealthCheck.Retries}}{{end}}
-{{if .HealthCheck.StartPeriod}} --health-start-period {{.HealthCheck.StartPeriod}}{{end}}
-{{if .HealthCheck.Timeout}} --health-timeout {{.HealthCheck.Timeout}}{{end}}
- {{.Image}}{{if .Command}} {{.Command}}{{end}}`
// writeScript will write a shell script to the given tempfile for the given commands
func (r *Run) writeScript(name string, st *state.State, fullEnv []string) (string, string, string, error) {
// make sure the scripts directory exist in our state directory
@@ -137,8 +110,8 @@
// detach will run the container detached
-func (r *Run) detach(name, runTemplate string, params map[string]interface{}, st *state.State, logger *gomol.LogAdapter) error {
- stdout, stderr, err := DockerOutput(name, runTemplate, params, st)
+func (r *Run) detach(name string, cmdv []string, st *state.State, logger *gomol.LogAdapter) error { + stdout, stderr, err := DockerOutput(name, cmdv, st) logger.Errorf("%s", stderr)
@@ -206,56 +179,31 @@
-func (r *Run) mapVariables(fullEnv []string, st *state.State, workSpace string) (map[string]interface{}, error) {
- dockerWorkspace := st.Workspace.(*Workspace)
- username, err := environment.Mapper(r.User, fullEnv)
+func (r *Run) buildCommandHealthCheck(cmd *command.Generator) { + // add the healthcheck command if we got one + if r.HealthCheck.Command != "" { + cmd.Append("--health-cmd", r.HealthCheck.Command) - image, err := environment.Mapper(r.Image, fullEnv)
- hostname, err := environment.Mapper(r.Hostname, fullEnv)
- labels, err := st.MapSlice(r.Labels, fullEnv)
+ // add the provided interval + if r.HealthCheck.Interval != time.Duration(0) { + cmd.Append("--health-interval", r.HealthCheck.Interval.String()) - workspacePath, err := environment.Mapper(dockerWorkspace.mountPoint, fullEnv)
- workspaceMount, err := environment.Mapper(workSpace, fullEnv)
+ // add the provided retries + if r.HealthCheck.Retries != 0 { + cmd.Append("--health-retries", fmt.Sprintf("%d", r.HealthCheck.Retries)) - // we do workDir after workspaceMount so we can put workspaceMount into an
- // environment that workDir will be mapped against.
- wsEnv := environment.Merge(fullEnv, []string{"CONVEY_WORKSPACE=" + workspaceMount})
- workdir, err := environment.Mapper(r.WorkDir, wsEnv)
+ // add the provided start period + if r.HealthCheck.StartPeriod != time.Duration(0) { + cmd.Append("--health-start-period", r.HealthCheck.StartPeriod.String()) - return map[string]interface{}{
- "workspacePath": workspacePath,
- "workspaceMount": workspaceMount,
+ // add the provided timeout + if r.HealthCheck.Timeout != time.Duration(0) { + cmd.Append("--health-timeout", r.HealthCheck.Timeout.String()) // Execute runs the run task.
@@ -263,12 +211,7 @@
fullEnv := environment.Merge(env, r.Environment)
fullEnv = environment.Merge(fullEnv, st.GetEnv())
- user, err := user.Current()
- // now expand the environment
+ // expand the environment for idx, v := range fullEnv {
v, err := environment.Mapper(v, fullEnv)
@@ -278,12 +221,92 @@
+ // create an id for this container + runID := util.ShortID() + cmd := command.NewGenerator( + "--network", st.Network.Name(), + // add the hostname if specified + hostname, err := environment.Mapper(r.Hostname, fullEnv) + cmd.Append("--network-alias", hostname) + // figure out the workspace variables + dockerWorkspace := st.Workspace.(*Workspace) + workspacePath, err := environment.Mapper(dockerWorkspace.mountPoint, fullEnv) + fmt.Printf("mountPoint: '%s'\n", dockerWorkspace.mountPoint) + fmt.Printf("workspacePath: '%s'\n", workspacePath) // assign a default workspace location
+ workspaceMount, err := environment.Mapper(workSpace, fullEnv) + "-v", workspacePath+":"+workspaceMount, + "-e", "CONVEY_WORKSPACE="+workspaceMount, + // we do workDir after workspaceMount so we can put workspaceMount into an + // environment that workDir will be mapped against. + wsEnv := environment.Merge(fullEnv, []string{"CONVEY_WORKSPACE=" + workspaceMount}) + workdir, err := environment.Mapper(r.WorkDir, wsEnv) + cmd.Append("-w", workdir) + // add cpushares if necessary + if st.CPUShares != "" { + cmd.Append("--cpu-shares", st.CPUShares) + // add memory constraints if necessary + cmd.Append("--memory", st.Memory) + // grab the current user's UID and GID + user, err := user.Current() + cmd.Append("-e", "UID="+user.Uid, "-e", "GID="+user.Gid) + // set user if one was specified + username, err := environment.Mapper(r.User, fullEnv) + cmd.Append("--user", username) // initialize some variables
entryPoint := r.EntryPoint
@@ -299,50 +322,70 @@
+ cmd.Append("-v", scriptFile+":"+scriptFile) - taskLabel := normalize.Normalize(
- fmt.Sprintf("convey-%d-task=%s", os.Getpid(), name),
+ cmd.Append("--entrypoint", entryPoint) + // add a label to the container + normalize.Normalize(fmt.Sprintf("convey-%d-task=%s", os.Getpid(), name)),
- vars, err := r.mapVariables(fullEnv, st, workSpace)
+ // run through any user supplied labels + labels, err := st.MapSlice(r.Labels, fullEnv) + for _, label := range labels { + cmd.Append("--label", label) - runID := util.ShortID()
+ // add the ssh agent stuff + authSock := os.Getenv("SSH_AUTH_SOCK") + "-v", authSock+":"+authSock, + // add the health check stuff + r.buildCommandHealthCheck(cmd) + // now add all non-empty environment variables + for _, env := range environment.Prune(fullEnv) { + // add the image to the command + // TODO make sure image isn't an empty string + image, err := environment.Mapper(r.Image, fullEnv) + // append the command if we have one logger.Infof("running container with id %s", runID)
- // build the dict for the template
- params := map[string]interface{}{
- "CPUShares": st.CPUShares,
- "Hostname": vars["hostname"],
- "Environment": environment.Prune(fullEnv),
- "EntryPoint": entryPoint,
- "Username": vars["username"],
- "HealthCheck": r.HealthCheck,
- "Image": vars["image"],
- "Labels": vars["labels"],
- "Network": st.Network.Name(),
- "ScriptFile": scriptFile,
- "SSHAgent": st.EnableSSHAgent,
- "SSHAuthSock": os.Getenv("SSH_AUTH_SOCK"),
- "WorkDir": vars["workdir"],
- "WorkspacePath": vars["workspacePath"],
- "WorkspaceMount": vars["workspaceMount"],
- "TaskLabel": taskLabel,
+ // Everything after this should be mostly dead code - return r.detach(name, runTemplate, params, st, logger)
+ return r.detach(name, cmd.Command(), st, logger) // Mark running so we can detach this container form the network
@@ -354,7 +397,7 @@
defer st.UnmarkRunning(runID)
- return Docker(name, runTemplate, params, st)
+ return Docker(name, cmd.Command(), st) // New creates a new run task.
--- a/docker/tag.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/tag.go Sun Feb 11 22:22:46 2018 -0600
@@ -32,8 +32,6 @@
Destinations yaml.StringOrSlice `yaml:"destinations"`
-const tagTemplate = `tag {{.source}} {{.destination}}`
// Execute runs the execute task.
func (t *Tag) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error {
fullEnv := environment.Merge(env, st.GetEnv())
@@ -49,12 +47,9 @@
for _, destination := range destinations {
- params := map[string]interface{}{
- "destination": destination,
+ cmdv := []string{"tag", source, destination} - if err := Docker(name, tagTemplate, params, st); err != nil {
+ if err := Docker(name, cmdv, st); err != nil { --- a/docker/util.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/util.go Sun Feb 11 22:22:46 2018 -0600
@@ -51,13 +51,9 @@
// StopContainer will call 'docker stop' on the given container id
func StopContainer(cid string, logger *gomol.LogAdapter, st *state.State) error {
- template := `stop {{.CID}}`
+ cmdv := []string{"stop", cid} - params := map[string]interface{}{
- _, stderr, err := DockerOutput("stop container", template, params, st)
+ _, stderr, err := DockerOutput("stop container", cmdv, st) --- a/docker/workspace.go Sun Jan 21 21:44:40 2018 -0600
+++ b/docker/workspace.go Sun Feb 11 22:22:46 2018 -0600
@@ -37,20 +37,15 @@
- wsCreateTemplate = `create --name={{.Name}} convey/workspace`
- wsDestroyTemplate = `rm -v {{.Name}}`
- volumeMountTemplate = `inspect --format "{{.Format}}" {{.Volume}}`
- volumeMountFormat = `{{range .Mounts}}{{if eq .Destination \"/workspace\" }}{{.Source}}{{end}}{{end}}`
func (ws *Workspace) findMountPoint(st *state.State) error {
- params := map[string]interface{}{
- "Volume": ws.volumeName,
- "Format": volumeMountFormat,
+ "{{range .Mounts}}{{if eq .Destination \"/workspace\" }}{{.Source}}{{end}}{{end}}", - stdout, stderr, err := DockerOutput("findMountPoint", volumeMountTemplate, params, st)
+ stdout, stderr, err := DockerOutput("findMountPoint", cmdv, st) ws.logger.Errorf("%s", stderr)
@@ -70,11 +65,13 @@
- params := map[string]interface{}{
- out, _, err := DockerOutput("create workspace", wsCreateTemplate, params, st)
+ out, _, err := DockerOutput("create workspace", cmdv, st) @@ -98,10 +95,6 @@
// Destroy removes the docker workspace.
func (ws *Workspace) Destroy() error {
- params := map[string]interface{}{
// monkey with the timeout so our cleanup always runs
oldTimeout := ws.state.PlanTimeout
@@ -109,5 +102,7 @@
ws.state.PlanTimeout = 15 * time.Minute
- return Docker("remove workspace", wsDestroyTemplate, params, ws.state)
+ cmdv := []string{"rm", "-v", ws.name} + return Docker("remove workspace", cmdv, ws.state)