grim/convey

674d936a6785
ChangeLog the fixes from pr #25. Fixes #125, #126
/*
* Convey
* Copyright 2016-2017 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 state
import (
"fmt"
"os"
"strings"
"sync"
"time"
"bitbucket.org/rw_grim/convey/environment"
"bitbucket.org/rw_grim/convey/network"
"bitbucket.org/rw_grim/convey/workspace"
)
type State struct {
Network network.Network
Workspace workspace.Workspace
KeepWorkspace bool
ForceSequential bool
EnableSSHAgent bool
TaskTimeout time.Duration
Environment []string
DockerConfig string
CPUShares string
Memory 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.
parent *State
expandables []string
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
// mutex.
detachedContainers map[string]struct{}
mutex *sync.RWMutex
}
func New() *State {
return &State{
detachedContainers: map[string]struct{}{},
mutex: &sync.RWMutex{},
}
}
func (st *State) Valid() error {
if st.parent == nil && (st.detachedContainers == nil || st.mutex == nil) {
return fmt.Errorf("state must be constructed via New")
}
if st.EnableSSHAgent {
if val := os.Getenv("SSH_AUTH_SOCK"); val == "" {
return fmt.Errorf("ssh-agent forwarding requested, but agent not running")
}
}
return nil
}
// MapSlice calls SliceMapper on the given environment, but also checks to
// see if the variable could be expanded into a list.
func (st *State) MapSlice(env, fullEnv []string) ([]string, error) {
if len(env) == 1 {
// If we only have one thing and it looks like $VAR or ${VAR}, then
// see if we have it in the list of things we can expand on the stack.
if delimiter, ok := st.getDelimiter(getName(env[0])); ok {
mapped, err := environment.Mapper(env[0], fullEnv)
if err != nil {
return nil, err
}
// Split it!
return strings.SplitN(mapped, delimiter, -1), nil
}
}
return environment.SliceMapper(env, fullEnv)
}
// 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) {
if st.parent == nil {
return "", false
}
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)
return &State{
Network: st.Network,
Workspace: st.Workspace,
KeepWorkspace: st.KeepWorkspace,
ForceSequential: st.ForceSequential,
EnableSSHAgent: st.EnableSSHAgent,
TaskTimeout: st.TaskTimeout,
Environment: env,
DockerConfig: st.DockerConfig,
CPUShares: st.CPUShares,
Memory: st.Memory,
parent: st,
expandables: expandable,
expandableDelimiter: delimiter,
}
}
// 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) {
if st.parent != nil {
st.parent.MarkDetached(name)
return
}
st.mutex.Lock()
defer st.mutex.Unlock()
st.detachedContainers[name] = struct{}{}
}
// GetDetached returns a list of all detached containers.
func (st *State) GetDetached() []string {
if st.parent != nil {
return st.parent.GetDetached()
}
st.mutex.RLock()
defer st.mutex.RUnlock()
names := []string{}
for key := range st.detachedContainers {
names = append(names, key)
}
return names
}
// getName returns the name of the variable `var` if the string is of the
// form $var or ${var}.
func getName(env string) string {
env = strings.TrimSpace(env)
if strings.HasPrefix(env, "$") {
return strings.Trim(env[1:], "{}")
}
return ""
}