--- a/ChangeLog Mon Oct 02 12:06:42 2017 -0500
+++ b/ChangeLog Tue Oct 03 21:16:34 2017 -0500
@@ -1,5 +1,6 @@
- * Nothing yet! Be the first!!
+ * Fix Push/Pop Extend Frame breakage under concurrent execution. Fixed #126 (Eric Fritz) + * Fritz fixed a goof. Fixed #125. (Eric Fritz) * Add support for multiple tags to the build task. (Eric Fritz)
--- a/command/command.go Mon Oct 02 12:06:42 2017 -0500
+++ b/command/command.go Tue Oct 03 21:16:34 2017 -0500
@@ -26,7 +26,7 @@
- "github.com/mattn/go-shellwords"
+ "github.com/kballard/go-shellquote" "bitbucket.org/rw_grim/convey/logging"
"github.com/aphistic/gomol"
@@ -80,7 +80,7 @@
- return shellwords.Parse(cmd.String())
+ return shellquote.Split(cmd.String()) func run(name, cmdTemplate string, params map[string]interface{}, timeout time.Duration, outHandler, errHandler collector) error {
--- a/docker/run.go Mon Oct 02 12:06:42 2017 -0500
+++ b/docker/run.go Tue Oct 03 21:16:34 2017 -0500
@@ -245,7 +245,7 @@
cid := strings.TrimSpace(stdout)
- st.DetachedContainers = append(st.DetachedContainers, cid)
logger.Infof("started detached container %s", cid)
--- a/intrinsic/extend.go Mon Oct 02 12:06:42 2017 -0500
+++ b/intrinsic/extend.go Tue Oct 03 21:16:34 2017 -0500
@@ -45,10 +45,12 @@
// While extending, certain environment variables can be expanded into
// lists. We store this meta information as a stack on the state, which
// is passed into the inner task execute method (and so on).
- st.PushExtendFrame(e.Environment, e.Expand, e.ExpandDelimiter)
- defer st.PopExtendFrame()
- return e.InnerTask.Execute(name, logger, env, st)
+ return e.InnerTask.Execute(name, logger, env, st.WrapWithExpandableEnv( func (e *Extend) New() tasks.Task {
--- a/main.go Mon Oct 02 12:06:42 2017 -0500
+++ b/main.go Tue Oct 03 21:16:34 2017 -0500
@@ -131,17 +131,16 @@
- ForceSequential: *forceSequential,
- EnableSSHAgent: enableSSHAgent,
- TaskTimeout: *taskTimeout,
- Environment: environment.Merge(defEnv, *env),
+ st.KeepWorkspace = *keep + st.ForceSequential = *forceSequential + st.EnableSSHAgent = enableSSHAgent + st.TaskTimeout = *taskTimeout + st.Environment = environment.Merge(defEnv, *env) + st.DockerConfig = *dockerConfig + st.CPUShares = *cpuShares - DockerConfig: *dockerConfig,
if err := st.Valid(); err != nil {
--- a/plans/plans.go Mon Oct 02 12:06:42 2017 -0500
+++ b/plans/plans.go Tue Oct 03 21:16:34 2017 -0500
@@ -64,7 +64,7 @@
func (p *Plan) teardown(logger *gomol.LogAdapter, st *state.State) {
// run through the DetachedContainers and stop them
- for _, cid := range st.DetachedContainers {
+ for _, cid := range st.GetDetached() { logger.Infof("removing detached container %s", cid)
docker.StopContainer(cid, logger, st)
--- a/state/state.go Mon Oct 02 12:06:42 2017 -0500
+++ b/state/state.go Tue Oct 03 21:16:34 2017 -0500
@@ -18,10 +18,10 @@
"bitbucket.org/rw_grim/convey/environment"
@@ -40,24 +40,42 @@
TaskTimeout time.Duration
- ExtendFrames []extendFrame
- DetachedContainers []string
+ // States have the ability to "wrap" another one without + // changing the underlying state. This is used by the + // extends intrinsic in order to modify the stat without + // requiring unique access to the state object during the + // execution of the extended task. A past implementation + // had modified the stack directly, but that causes an + // extended task to clobber other tasks in concurrent mode. + expandableDelimiter string + // This list is a stash of container names which are run + // in detached mode. Appending to this may happen from + // multiple goroutines, so this needs to be guarded via + detachedContainers map[string]struct{} -type extendFrame struct {
+ detachedContainers: map[string]struct{}{}, + mutex: &sync.RWMutex{}, -var errEmptyExtendStack = errors.New("empty extend stack")
+func (st *State) Valid() error { + if st.parent == nil && (st.detachedContainers == nil || st.mutex == nil) { + return fmt.Errorf("state must be constructed via New") -func (st *State) Valid() error {
if val := os.Getenv("SSH_AUTH_SOCK"); val == "" {
return fmt.Errorf("ssh-agent forwarding requested, but agent not running")
@@ -162,44 +180,75 @@
// GetDelimiter returns the highest (outermost extend task) delimiter registered
// with a given expandable environment variable. Returns true if found by name.
func (st *State) getDelimiter(name string) (string, bool) {
- for i := len(st.ExtendFrames) - 1; i >= 0; i-- {
- frame := st.ExtendFrames[i]
- for _, expandable := range frame.expandable {
- if expandable == name {
- return frame.delimiter, true
+ for _, expandable := range st.expandables { + if expandable == name { + return st.expandableDelimiter, true
+ return st.parent.getDelimiter(name) +// WrapWithExpandableEnv will create a shallow clone of the state with a reference +// to the current state as "parent" with a modified environment. This creates a local +// stack of states which do not interfere with other goroutines. A pop operation is +// the same as ignoring the wrapped values and using the underlying state. This stack +// is used to map a slice within an extended task. +func (st *State) WrapWithExpandableEnv(env, expandable []string, delimiter string) *State { + // Merge the environment into this map, but do NOT override anything that + // is currently in the state's environment - this has higher precedence. + env = environment.Merge(env, st.Environment) + Workspace: st.Workspace, + KeepWorkspace: st.KeepWorkspace, + ForceSequential: st.ForceSequential, + EnableSSHAgent: st.EnableSSHAgent, + TaskTimeout: st.TaskTimeout, + DockerConfig: st.DockerConfig, + CPUShares: st.CPUShares, + expandables: expandable, + expandableDelimiter: delimiter, -// PushExtendFrame will store the set of expandable environment variables
-// and the current set of environments which are restored on pop. These frames
-// are used to map a slice within an extended task.
-func (st *State) PushExtendFrame(env, expandable []string, delimiter string) {
- st.ExtendFrames = append(st.ExtendFrames, extendFrame{
- expandable: expandable,
- stashedEnv: st.Environment,
+// MarkDetached will add the given container name into the list +// of containers running in detached mode which must be shut down +// at the end of the plan. This falls through directly to the root +// state so that states wrapping the global one do not have to sync +// additional detached container names. +func (st *State) MarkDetached(name string) { + st.parent.MarkDetached(name) - // Merge the environment into this map, but do NOT override anything that
- // is currently in the state's enviornment - this has higher precedence.
- st.Environment = environment.Merge(env, st.Environment)
+ defer st.mutex.Unlock() + st.detachedContainers[name] = struct{}{} -// PopExtendFrame will remove one frame from the top of the stack.
-func (st *State) PopExtendFrame() error {
- idx := len(st.ExtendFrames) - 1
- return errEmptyExtendStack
+// GetDetached returns a list of all detached containers. +func (st *State) GetDetached() []string { + return st.parent.GetDetached() - last := st.ExtendFrames[idx]
- st.ExtendFrames = st.ExtendFrames[:idx]
- st.Environment = last.stashedEnv
+ defer st.mutex.RUnlock()
+ for key := range st.detachedContainers { + names = append(names, key) --- a/state/state_test.go Mon Oct 02 12:06:42 2017 -0500
+++ b/state/state_test.go Tue Oct 03 21:16:34 2017 -0500
@@ -37,131 +37,74 @@
-func (e *stateSuite) TestMapSlice(t sweet.T) {
+func (s *stateSuite) TestMapSlice(t sweet.T) { + st1.Environment = []string{"FOO=BAR"} + Expect(mapEnv(st1, "$X")).To(ConsistOf([]string{"$X"})) + Expect(mapEnv(st1, "$FOO")).To(ConsistOf([]string{"BAR"})) + st2 := st1.WrapWithExpandableEnv([]string{"X=A;B;C"}, []string{"X"}, ";") + Expect(mapEnv(st2, "$X")).To(ConsistOf([]string{"A", "B", "C"})) + Expect(mapEnv(st2, "$FOO")).To(ConsistOf([]string{"BAR"})) + st3 := st2.WrapWithExpandableEnv([]string{"BAR=B;A;R::B;A;Z", "FOO=SAME"}, []string{"BAR"}, "::") + Expect(mapEnv(st3, "$X")).To(ConsistOf([]string{"A", "B", "C"})) + Expect(mapEnv(st3, "$FOO")).To(ConsistOf([]string{"BAR"})) + Expect(mapEnv(st3, "$BAR")).To(ConsistOf([]string{"B;A;R", "B;A;Z"})) +func (s *stateSuite) TestMapSliceComplex(t sweet.T) { st.Environment = []string{"FOO=BAR"}
- mapEnv := func(val string) []string {
- mapped, err := st.MapSlice([]string{val}, st.Environment)
- Expect(err).To(BeNil())
- Expect(mapEnv("$X")).To(ConsistOf([]string{"$X"}))
- Expect(mapEnv("$FOO")).To(ConsistOf([]string{"BAR"}))
- st.PushExtendFrame([]string{"X=A;B;C"}, []string{"X"}, ";")
- Expect(mapEnv("$X")).To(ConsistOf([]string{"A", "B", "C"}))
- Expect(mapEnv("$FOO")).To(ConsistOf([]string{"BAR"}))
- st.PushExtendFrame([]string{"BAR=B;A;R::B;A;Z", "FOO=SAME"}, []string{"BAR"}, "::")
- Expect(mapEnv("$X")).To(ConsistOf([]string{"A", "B", "C"}))
- Expect(mapEnv("$FOO")).To(ConsistOf([]string{"BAR"}))
- Expect(mapEnv("$BAR")).To(ConsistOf([]string{"B;A;R", "B;A;Z"}))
- Expect(st.PopExtendFrame()).To(BeNil())
- Expect(st.PopExtendFrame()).To(BeNil())
- Expect(mapEnv("$X")).To(ConsistOf([]string{"$X"}))
- Expect(mapEnv("$FOO")).To(ConsistOf([]string{"BAR"}))
-func (e *stateSuite) TestMapSliceComplex(t sweet.T) {
- st.Environment = []string{"FOO=BAR"}
- st.PushExtendFrame([]string{"X=A;B;C", "Y=D;E;F"}, []string{"X", "Y"}, ";")
- mapEnv := func(val string) []string {
- mapped, err := st.MapSlice([]string{val}, st.Environment)
- Expect(err).To(BeNil())
+ st = st.WrapWithExpandableEnv([]string{"X=A;B;C", "Y=D;E;F"}, []string{"X", "Y"}, ";") - Expect(mapEnv("BONK")).To(ConsistOf([]string{"BONK"}))
- Expect(mapEnv("$FOO")).To(ConsistOf([]string{"BAR"}))
+ Expect(mapEnv(st, "BONK")).To(ConsistOf([]string{"BONK"})) + Expect(mapEnv(st, "$FOO")).To(ConsistOf([]string{"BAR"})) - Expect(mapEnv("$X")).To(ConsistOf([]string{"A", "B", "C"}))
- Expect(mapEnv("$Y")).To(ConsistOf([]string{"D", "E", "F"}))
+ Expect(mapEnv(st, "$X")).To(ConsistOf([]string{"A", "B", "C"})) + Expect(mapEnv(st, "$Y")).To(ConsistOf([]string{"D", "E", "F"})) - Expect(mapEnv("x${X}x")).To(ConsistOf([]string{"xAx", "xBx", "xCx"}))
- Expect(mapEnv("x${Y}x")).To(ConsistOf([]string{"xDx", "xEx", "xFx"}))
+ Expect(mapEnv(st, "x${X}x")).To(ConsistOf([]string{"xAx", "xBx", "xCx"})) + Expect(mapEnv(st, "x${Y}x")).To(ConsistOf([]string{"xDx", "xEx", "xFx"})) - Expect(mapEnv("$X/$Y")).To(ConsistOf([]string{
+ Expect(mapEnv(st, "$X/$Y")).To(ConsistOf([]string{ -func (e *stateSuite) TestMapSliceIterative(t sweet.T) {
+func (s *stateSuite) TestMapSliceIterative(t sweet.T) { st.Environment = []string{"FOO=BAR"}
- st.PushExtendFrame([]string{"X=A;x$Y", "Y=B;y$Z", "Z=C;D;E"}, []string{"X", "Y", "Z"}, ";")
+ st = st.WrapWithExpandableEnv([]string{"X=A;x$Y", "Y=B;y$Z", "Z=C;D;E"}, []string{"X", "Y", "Z"}, ";") - mapEnv := func(val string) []string {
- mapped, err := st.MapSlice([]string{val}, st.Environment)
- Expect(err).To(BeNil())
- Expect(mapEnv("$X")).To(ConsistOf([]string{"A", "xB", "xyC", "xyD", "xyE"}))
+ Expect(mapEnv(st, "$X")).To(ConsistOf([]string{"A", "xB", "xyC", "xyD", "xyE"})) -func (e *stateSuite) TestMapSliceIndirect(t sweet.T) {
+func (s *stateSuite) TestMapSliceIndirect(t sweet.T) { st.Environment = []string{"FOO=BAR"}
- st.PushExtendFrame([]string{"X=$Y", "Y=A;$Z;C", "Z=B;$W", "W=D"}, []string{"X", "Y", "Z"}, ";")
+ st = st.WrapWithExpandableEnv([]string{"X=$Y", "Y=A;$Z;C", "Z=B;$W", "W=D"}, []string{"X", "Y", "Z"}, ";") - mapEnv := func(val string) []string {
- mapped, err := st.MapSlice([]string{val}, st.Environment)
- Expect(err).To(BeNil())
- Expect(mapEnv("$X")).To(ConsistOf([]string{"A", "B", "C", "D"}))
+ Expect(mapEnv(st, "$X")).To(ConsistOf([]string{"A", "B", "C", "D"})) -func (e *stateSuite) TestMapSliceRecursive(t sweet.T) {
+func (s *stateSuite) TestMapSliceRecursive(t sweet.T) { st.Environment = []string{"FOO=BAR"}
- st.PushExtendFrame([]string{"X=A;x$Y", "Y=B;y$Z", "Z=C;D;$X"}, []string{"X", "Y", "Z"}, ";")
+ st = st.WrapWithExpandableEnv([]string{"X=A;x$Y", "Y=B;y$Z", "Z=C;D;$X"}, []string{"X", "Y", "Z"}, ";") _, err := st.MapSlice([]string{"$X"}, st.Environment)
Expect(err).NotTo(BeNil())
Expect(err.Error()).To(ContainSubstring("hit limit"))
-func (e *stateSuite) TestPopExtendFrameSize(t sweet.T) {
- st.PushExtendFrame(nil, nil, "")
- Expect(st.ExtendFrames).To(HaveLen(1))
- st.PushExtendFrame(nil, nil, "")
- Expect(st.ExtendFrames).To(HaveLen(2))
- Expect(st.PopExtendFrame()).To(BeNil())
- Expect(st.ExtendFrames).To(HaveLen(1))
- Expect(st.PopExtendFrame()).To(BeNil())
- Expect(st.ExtendFrames).To(HaveLen(0))
-func (e *stateSuite) TestPopExtendFrameRestoresEnv(t sweet.T) {
- st.Environment = []string{"FOO=BAR", "BAR=BAZ"}
- st.PushExtendFrame([]string{"FOO=BONK", "BAZ=BONK"}, nil, "")
- Expect(st.Environment).To(HaveLen(3))
- Expect(st.Environment).To(ConsistOf([]string{"FOO=BAR", "BAR=BAZ", "BAZ=BONK"}))
- Expect(st.Environment).To(HaveLen(2))
- Expect(st.Environment).To(ConsistOf([]string{"FOO=BAR", "BAR=BAZ"}))
-func (e *stateSuite) TestPopExtendFrameEmpty(t sweet.T) {
- Expect(st.PopExtendFrame()).To(Equal(errEmptyExtendStack))
func (e *stateSuite) TestGetNames(t sweet.T) {
names := getNames("foo $bar ${baz} ${bar} $bonk_quux honk")
Expect(names).To(HaveLen(4))
@@ -190,3 +133,47 @@
map[string]string{"a": "3", "b": "5", "c": "7"},
+func (s *stateSuite) TestWrapParent(t sweet.T) { + st1.WrapWithExpandableEnv(nil, nil, "") + Expect(st1.parent).To(BeNil()) + st2 := st1.WrapWithExpandableEnv(nil, nil, "") + Expect(st2.parent).To(BeIdenticalTo(st1)) +func (s *stateSuite) TestWrapWithExpandableEnvMap(t sweet.T) { + st1.Environment = []string{"FOO=BAR", "BAR=BAZ"} + Expect(st1.Environment).To(HaveLen(2)) + Expect(st1.Environment).To(ConsistOf([]string{"FOO=BAR", "BAR=BAZ"})) + st2 := st1.WrapWithExpandableEnv([]string{"FOO=BONK", "BAZ=BONK"}, nil, "") + Expect(st2.Environment).To(HaveLen(3)) + Expect(st2.Environment).To(ConsistOf([]string{"FOO=BAR", "BAR=BAZ", "BAZ=BONK"})) +func (s *stateSuite) TestDetached(t sweet.T) { + Expect(st1.GetDetached()).To(BeEmpty()) + st1.MarkDetached("foo") + Expect(st1.GetDetached()).To(HaveLen(1)) + Expect(st1.GetDetached()).To(ConsistOf([]string{"foo"})) + st2 := st1.WrapWithExpandableEnv(nil, nil, "") + st2.MarkDetached("bar") + Expect(st2.GetDetached()).To(HaveLen(2)) + Expect(st2.GetDetached()).To(ConsistOf([]string{"foo", "bar"})) + Expect(st1.GetDetached()).To(HaveLen(2)) + Expect(st1.GetDetached()).To(ConsistOf([]string{"foo", "bar"})) +func mapEnv(st *State, val string) []string { + mapped, err := st.MapSlice([]string{val}, st.Environment) + Expect(err).To(BeNil())