grim/convey

Move stuff to the new command generator

2018-02-11, Gary Kramlich
eda2e7b48ff2
Parents 14e4e891eda9
Children 35cb993e3262
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 @@
import (
"bufio"
- "bytes"
"os/exec"
- "strings"
"sync"
- "text/template"
"time"
- "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 {
var (
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
// logging it.
-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) {
var (
wg = &sync.WaitGroup{}
outCollector = newStringCollector(wg)
@@ -58,43 +53,15 @@
)
wg.Add(2)
- err := run(name, cmdTemplate, params, timeout, outCollector.handler, errCollector.handler)
+ err := run(name, cmdv, timeout, outCollector.handler, errCollector.handler)
wg.Wait()
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
- lock.Lock()
- tmpl, err := template.New(name).Parse(cleanTemplate)
- lock.Unlock()
- if err != nil {
- return nil, err
- }
-
- // now execute the template
- cmd := new(bytes.Buffer)
- err = tmpl.Execute(cmd, params)
- if err != nil {
- return nil, err
- }
-
- 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)
- if err != nil {
- return err
- }
-
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 @@
+// Convey
+// 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.
+package command
+
+import (
+ "math"
+ "sync"
+)
+
+const (
+ minAlloc = 10
+)
+
+type Generator struct {
+ args []string
+ next int
+ lock sync.Mutex
+}
+
+// NewGenerator creates a new generator initialized with the gives arguments.
+func NewGenerator(args ...string) *Generator {
+ g := &Generator{
+ args: make([]string, minAlloc),
+ next: 0,
+ }
+
+ g.Appendv(args)
+
+ return g
+}
+
+// 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) {
+ newSize := len(g.args)
+
+ if reqSize > minAlloc {
+ newSize += reqSize
+ } else {
+ newSize += minAlloc
+ }
+
+ newArgs := make([]string, newSize)
+ for idx, item := range g.args {
+ newArgs[idx] = item
+ }
+ g.args = newArgs
+ }
+}
+
+// Append appends the given arguments to the end of the arguments.
+func (g *Generator) Append(args ...string) {
+ g.Appendv(args)
+}
+
+// Appendv appends the give string slice to the command
+func (g *Generator) Appendv(args []string) {
+ g.lock.Lock()
+ defer g.lock.Unlock()
+
+ if len(args) <= 0 {
+ return
+ }
+
+ for _, item := range args {
+ g.resize(1)
+ g.args[g.next] = item
+ g.next++
+ }
+}
+
+// Prepend prepends the given arguments to the beginning of the arguments.
+func (g *Generator) Prepend(args ...string) {
+ g.Prependv(args)
+}
+
+// Prependv prepends the given string slice to the command.
+func (g *Generator) Prependv(args []string) {
+ g.lock.Lock()
+ defer g.lock.Unlock()
+
+ if len(args) <= 0 {
+ return
+ }
+
+ // 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
+ idx := 0
+
+ // add the prepended items
+ for ; idx < len(args); idx++ {
+ newArgs[idx] = args[idx]
+ }
+
+ // add the original items
+ offset := len(args)
+ for ; idx < g.next+offset; idx++ {
+ newArgs[idx] = g.args[idx-offset]
+ }
+
+ // now assign newArgs
+ g.args = newArgs
+ g.next += len(args)
+}
+
+// Command returns the currently built command.
+func (g *Generator) Command() []string {
+ g.lock.Lock()
+ defer g.lock.Unlock()
+
+ ret := make([]string, g.next)
+ for i := 0; i < g.next; i++ {
+ ret[i] = g.args[i]
+ }
+
+ return ret
+}
--- /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 @@
+// Convey
+// 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
+
+import (
+ "testing"
+
+ "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) {
+ g := NewGenerator()
+
+ Expect(g).ToNot(BeNil())
+}
+
+func (s *generatorSuite) TestWithArgs(t sweet.T) {
+ g := NewGenerator("a")
+ 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) {
+ g := NewGenerator("a")
+ g.Append("b", "c")
+ Expect(g.Command()).To(BeEquivalentTo([]string{"a", "b", "c"}))
+
+ g = NewGenerator("a", "b")
+ g.Append("c")
+ Expect(g.Command()).To(BeEquivalentTo([]string{"a", "b", "c"}))
+
+ g = NewGenerator("a")
+ 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")
+
+ g.Prepend("a")
+ Expect(g.Command()).To(BeEquivalentTo([]string{"a", "b", "c"}))
+
+ g = NewGenerator("k")
+ 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
- -f {{.dockerfile}}
-{{range .Tags}} -t {{.}}{{end}}
-{{range .Labels}} --label '{{.}}'{{end}}
-{{range .Arguments}} --build-arg {{.}}{{end}}
-{{if .Target}} --target {{.Target}}{{end}}
- {{.buildContext}}`
-
// 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{}{
- "source": dockerfile,
- "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 {
return err
}
@@ -107,17 +94,36 @@
return err
}
- // now run the build
- params = map[string]interface{}{
- "dockerfile": filepath.Join(buildDir, filepath.Base(dockerfile)),
- "buildContext": buildDir,
- "Tags": tags,
- "Target": b.Target,
- "Labels": labels,
- "Arguments": arguments,
+ // create the basic command
+ cmd := command.NewGenerator(
+ "build",
+ "-f", filepath.Join(buildDir, filepath.Base(dockerfile)),
+ )
+
+ // add any and all tags
+ for _, tag := range tags {
+ cmd.Append("-t", tag)
}
- 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
+ if b.Target != "" {
+ cmd.Append("--target", b.Target)
+ }
+
+ // finally add the build context
+ cmd.Append(buildDir)
+
+ 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 @@
"tag": &Tag{},
"environment": &Environment{},
}
+)
- dockerTemplate = `docker {{if .DockerConfig}}--config {{.DockerConfig}} {{end}}`
-)
+func dockerCommand(cmdv []string, st *state.State) []string {
+ cmd := command.NewGenerator()
+ cmd.Appendv(cmdv)
+
+ // if we have a docker config prepend it first
+ if st.DockerConfig != "" {
+ cmd.Prepend("--config", st.DockerConfig)
+ }
+
+ // finally prepend the actual docker command
+ cmd.Prepend("docker")
+
+ return cmd.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{}{
- "source": src,
- "destination": dest,
- "workspace": workSpace,
- }
-
dir := filepath.Dir(dest)
if err := os.MkdirAll(dir, 0777); err != nil {
return err
}
- return Docker(name, exportTemplate, params, st)
+ // build out the source path
+ source := workSpace + ":" + filepath.Join("/workspace", src)
+
+ cmdv := []string{
+ "cp",
+ source,
+ dest,
+ }
+
+ return Docker(name, cmdv, st)
}
func exportGlob(name, workSpace, mountPoint, pattern string, st *state.State) error {
- params := map[string]interface{}{
- "file": pattern,
- "workspace": mountPoint,
+ cmdv := []string{
+ "run",
+ "--rm",
+ "-v",
+ mountPoint + ":/workspace",
+ "convey/workspace-tools:latest",
+ "zglob",
+ pattern,
}
- out, _, err := DockerOutput(fmt.Sprintf("%s-zglob", name), zglobTemplate, params, st)
+ out, _, err := DockerOutput(fmt.Sprintf("%s-zglob", name), cmdv, st)
if err != nil {
return err
}
--- 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"`
}
-const (
- 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,
- "Cid": cid,
+ cmdv := []string{
+ "inspect",
+ "-f",
+ "\"{{if .Config.Healthcheck}}true{{else}}false{{end}}\"",
+ cid,
}
- stdout, stderr, err := DockerOutput(
- name+"/healthcheck",
- containerHasHealthCheckTemplate,
- params,
- st,
- )
-
+ stdout, stderr, err := DockerOutput(name+"/healthcheck", cmdv, st)
if err != nil {
logger.Error(stderr)
@@ -69,18 +57,14 @@
}
func containerIsHealthy(name, cid string, st *state.State, logger *gomol.LogAdapter) (bool, error) {
- params := map[string]interface{}{
- "Format": containerIsHealthyFormat,
- "Cid": cid,
+ cmdv := []string{
+ "inspect",
+ "-f",
+ "\"{{.State.Health.Status}}\"",
+ cid,
}
- stdout, stderr, err := DockerOutput(
- name+"/healthcheck",
- containerIsHealthyTemplate,
- params,
- st,
- )
-
+ stdout, stderr, err := DockerOutput(name+"/healthcheck", cmdv, st)
if err != nil {
logger.Error(stderr)
--- 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 @@
return err
}
- params := map[string]interface{}{
- "source": realSrc,
- "destination": dest,
- "workspaceID": st.Workspace.Name(),
+ cmdv := []string{
+ "cp",
+ realSrc,
+ st.Workspace.Name() + ":/workspace/" + dest,
}
- if err := Docker(name, importTemplate, params, st); err != nil {
+ if err := Docker(name, cmdv, st); err != nil {
return err
}
}
--- 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 @@
import (
"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 @@
return err
}
- params := map[string]interface{}{
- "username": username,
- "password": password,
- "server": server,
+ cmd := command.NewGenerator(
+ "login",
+ "-u", username,
+ "-p", password,
+ )
+
+ if server != "" {
+ cmd.Append(server)
}
- 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 @@
import (
"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 @@
return err
}
- params := map[string]interface{}{
- "server": server,
+ cmd := command.NewGenerator("logout")
+
+ if server != "" {
+ cmd.Append(server)
}
- 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 @@
state *state.State
}
-const (
- 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) {
network := &Network{
@@ -48,11 +42,9 @@
state: st,
}
- params := map[string]interface{}{
- "Name": network.name,
- }
+ cmdv := []string{"network", "create", network.name}
- _, _, err := DockerOutput("create network", networkCreateTemplate, params, st)
+ _, _, err := DockerOutput("create network", cmdv, st)
if err != nil {
return nil, err
}
@@ -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{}{
- "Name": network.name,
- "ContainerName": name,
+ cmdv := []string{
+ "network",
+ "disconnect",
+ "--force",
+ network.name,
+ name,
}
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)
if err != nil {
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{}{
- "Name": network.name,
- }
-
// monkey with the timeout so our cleanup always runs
oldTimeout := network.state.PlanTimeout
defer func() {
@@ -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{}{
- "image": image,
- }
+ cmdv := []string{"pull", image}
- if err := Docker(name, pullTemplate, params, st); err != nil {
+ if err := Docker(name, cmdv, st); err != nil {
return err
}
}
--- 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{}{
- "image": image,
- }
+ cmdv := []string{"push", image}
- if err := Docker(name, pushTemplate, params, st); err != nil {
+ if err := Docker(name, cmdv, st); err != nil {
return err
}
}
--- 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{}{
- "image": image,
- }
+ 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 {
continue
}
--- 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 @@
return nil
}
-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}}
- -l {{.TaskLabel}}
-{{range .Environment}} -e {{.}}{{end}}
-{{if .Username}} --user {{.Username}}{{end}}
-{{if .HealthCheck}}
-{{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}}
-{{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)
if err != nil {
logger.Errorf("%s", stderr)
@@ -206,56 +179,31 @@
return nil
}
-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)
- if err != nil {
- return nil, err
+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)
- if err != nil {
- return nil, err
- }
-
- hostname, err := environment.Mapper(r.Hostname, fullEnv)
- if err != nil {
- return nil, err
- }
-
- labels, err := st.MapSlice(r.Labels, fullEnv)
- if err != nil {
- return nil, err
+ // 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)
- if err != nil {
- return nil, err
- }
-
- workspaceMount, err := environment.Mapper(workSpace, fullEnv)
- if err != nil {
- return nil, err
+ // 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)
- if err != nil {
- return nil, err
+ // 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{}{
- "username": username,
- "image": image,
- "hostname": hostname,
- "labels": labels,
- "workdir": workdir,
- "workspacePath": workspacePath,
- "workspaceMount": workspaceMount,
- }, nil
+ // 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()
- if err != nil {
- return err
- }
-
- // now expand the environment
+ // expand the environment
for idx, v := range fullEnv {
v, err := environment.Mapper(v, fullEnv)
if err != nil {
@@ -278,12 +221,92 @@
fullEnv[idx] = v
}
+ // create an id for this container
+ runID := util.ShortID()
+
+ // build the command
+ cmd := command.NewGenerator(
+ "run", "--rm",
+ "--name", runID,
+ "--network", st.Network.Name(),
+ )
+
+ // add the hostname if specified
+ hostname, err := environment.Mapper(r.Hostname, fullEnv)
+ if err != nil {
+ return err
+ }
+ if hostname != "" {
+ 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)
+ if err != nil {
+ return err
+ }
+
// assign a default workspace location
workSpace := r.WorkSpace
if workSpace == "" {
workSpace = "/workspace"
}
+ workspaceMount, err := environment.Mapper(workSpace, fullEnv)
+ if err != nil {
+ return err
+ }
+
+ cmd.Append(
+ "-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)
+ if err != nil {
+ return err
+ }
+ if workdir != "" {
+ cmd.Append("-w", workdir)
+ }
+
+ // detach if necessary
+ if r.Detach {
+ cmd.Append("-d")
+ }
+
+ // add cpushares if necessary
+ if st.CPUShares != "" {
+ cmd.Append("--cpu-shares", st.CPUShares)
+ }
+
+ // add memory constraints if necessary
+ if st.Memory != "" {
+ cmd.Append("--memory", st.Memory)
+ }
+
+ // grab the current user's UID and GID
+ user, err := user.Current()
+ if err != nil {
+ return err
+ }
+ cmd.Append("-e", "UID="+user.Uid, "-e", "GID="+user.Gid)
+
+ // set user if one was specified
+ username, err := environment.Mapper(r.User, fullEnv)
+ if err != nil {
+ return err
+ }
+ if username != "" {
+ cmd.Append("--user", username)
+ }
+
// initialize some variables
scriptFile := ""
entryPoint := r.EntryPoint
@@ -299,50 +322,70 @@
if err != nil {
return err
}
+
+ if scriptFile != "" {
+ cmd.Append("-v", scriptFile+":"+scriptFile)
+ }
+
}
- taskLabel := normalize.Normalize(
- fmt.Sprintf("convey-%d-task=%s", os.Getpid(), name),
+ if entryPoint != "" {
+ cmd.Append("--entrypoint", entryPoint)
+ }
+
+ // add a label to the container
+ cmd.Append(
+ "--label",
+ normalize.Normalize(fmt.Sprintf("convey-%d-task=%s", os.Getpid(), name)),
)
- //
- // Map variables
- vars, err := r.mapVariables(fullEnv, st, workSpace)
+ // run through any user supplied labels
+ labels, err := st.MapSlice(r.Labels, fullEnv)
if err != nil {
return err
}
+ for _, label := range labels {
+ cmd.Append("--label", label)
+ }
- runID := util.ShortID()
+ // add the ssh agent stuff
+ if st.EnableSSHAgent {
+ authSock := os.Getenv("SSH_AUTH_SOCK")
+ cmd.Append(
+ "-e", "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) {
+ cmd.Append("-e", env)
+ }
+
+ // add the image to the command
+ // TODO make sure image isn't an empty string
+ image, err := environment.Mapper(r.Image, fullEnv)
+ if err != nil {
+ return err
+ }
+ cmd.Append(image)
+
+ // append the command if we have one
+ if commandArg != "" {
+ cmd.Append(commandArg)
+ }
+
logger.Infof("running container with id %s", runID)
- // build the dict for the template
- params := map[string]interface{}{
- "Command": commandArg,
- "CPUShares": st.CPUShares,
- "Detach": r.Detach,
- "Hostname": vars["hostname"],
- "Environment": environment.Prune(fullEnv),
- "EntryPoint": entryPoint,
- "Username": vars["username"],
- "UID": user.Uid,
- "GID": user.Gid,
- "HealthCheck": r.HealthCheck,
- "Image": vars["image"],
- "Labels": vars["labels"],
- "Memory": st.Memory,
- "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,
- "Name": runID,
- }
+ //
+ // Everything after this should be mostly dead code
+ //
if r.Detach {
- 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)
// run the command
- 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{}{
- "source": source,
- "destination": destination,
- }
+ cmdv := []string{"tag", source, destination}
- if err := Docker(name, tagTemplate, params, st); err != nil {
+ if err := Docker(name, cmdv, st); err != nil {
return err
}
}
--- 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{}{
- "CID": cid,
- }
-
- _, stderr, err := DockerOutput("stop container", template, params, st)
+ _, stderr, err := DockerOutput("stop container", cmdv, st)
if err != nil {
return err
}
--- 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 @@
state *state.State
}
-const (
- 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,
+ cmdv := []string{
+ "inspect",
+ "--format",
+ "{{range .Mounts}}{{if eq .Destination \"/workspace\" }}{{.Source}}{{end}}{{end}}",
+ ws.volumeName,
}
- stdout, stderr, err := DockerOutput("findMountPoint", volumeMountTemplate, params, st)
+ stdout, stderr, err := DockerOutput("findMountPoint", cmdv, st)
if err != nil {
ws.logger.Errorf("%s", stderr)
@@ -70,11 +65,13 @@
state: st,
}
- params := map[string]interface{}{
- "Name": ws.name,
+ cmdv := []string{
+ "create",
+ "--name=" + ws.name,
+ "convey/workspace",
}
- out, _, err := DockerOutput("create workspace", wsCreateTemplate, params, st)
+ out, _, err := DockerOutput("create workspace", cmdv, st)
if err != nil {
return nil, err
}
@@ -98,10 +95,6 @@
// Destroy removes the docker workspace.
func (ws *Workspace) Destroy() error {
- params := map[string]interface{}{
- "Name": ws.name,
- }
-
// monkey with the timeout so our cleanup always runs
oldTimeout := ws.state.PlanTimeout
defer func() {
@@ -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)
}