grim/convey

Parents c124c4c14328
Children 821100c43641
Make the state create and manage a scratch directory as well as handle all cleanup functions
--- a/cleanup/cleanup.go Mon Jan 15 16:19:58 2018 -0600
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-// 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 cleanup handles cleanup tasks for cleaning up after a run.
-package cleanup
-
-import (
- "os"
- "os/signal"
- "sync"
- "syscall"
-
- "bitbucket.org/rw_grim/convey/logging"
- "bitbucket.org/rw_grim/convey/util"
-)
-
-type (
- // List is a list of things that should happen in case the
- // program receives a signal. This should be things like making
- // sure temp files are deleted, volumes are destroyed, etc.
- List struct {
- funcs map[string]Func
- mutex sync.RWMutex
- }
-
- // Func is a function to run on exit (it should be quick).
- Func func()
-)
-
-// NewList will create a new List that will run all registered
-// functions on SIGINT or SIGTERM.
-func NewList() *List {
- var (
- ch = make(chan os.Signal, 1)
- list = &List{funcs: map[string]Func{}}
- adapter = logging.NewAdapter("main")
- )
-
- go func() {
- <-ch
- adapter.Fatal("Received signal, cleaning up")
- list.run()
- os.Exit(1)
- }()
-
- signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
- return list
-}
-
-// Add will register a function to be invoked on system exit
-// unless the returned function is called. The given function
-// is wrapped in the return function, so it can be used in its
-// place. This function MUST be called in order to deregister
-// it on exit if it cannot be called idempotently. The order of
-// functions invoked on exit is not guaranteed to be stable.
-func (c *List) Add(fn Func) func() {
- c.mutex.Lock()
- defer c.mutex.Unlock()
-
- name := util.ShortID()
- c.funcs[name] = fn
- return func() { c.fire(name) }
-}
-
-func (c *List) fire(name string) {
- c.mutex.Lock()
- c.funcs[name]()
- delete(c.funcs, name)
- c.mutex.Unlock()
-}
-
-func (c *List) run() {
- c.mutex.Lock()
- defer c.mutex.Unlock()
-
- for _, fn := range c.funcs {
- fn()
- }
-}
--- a/docker/build.go Mon Jan 15 16:19:58 2018 -0600
+++ b/docker/build.go Mon Jan 15 16:20:18 2018 -0600
@@ -17,7 +17,6 @@
package docker
import (
- "io/ioutil"
"os"
"path/filepath"
@@ -58,14 +57,12 @@
}
// create out build directory
- tmpDir, err := ioutil.TempDir("", "convey-build-")
+ buildDir := filepath.Join(st.Directory, name)
+ err = os.MkdirAll(buildDir, 0700)
if err != nil {
return err
}
- // Remove the dir on defer OR on signal exit (first one to happen)
- defer st.CleanupList.Add(func() { os.RemoveAll(tmpDir) })()
-
dockerfile, err := environment.Mapper(b.Dockerfile, fullEnv)
if err != nil {
return err
@@ -77,7 +74,7 @@
// export the files to it
for _, src := range files {
src, dest := tasks.ParseFilePath(base, src)
- cleanDest := filepath.Clean(filepath.Join(tmpDir, dest))
+ cleanDest := filepath.Clean(filepath.Join(buildDir, dest))
if err = exportFile(name, st.Workspace.Name(), src, cleanDest, st); err != nil {
return err
@@ -87,7 +84,7 @@
// copy the dockerfile to the temp directory
params := map[string]interface{}{
"source": dockerfile,
- "destination": tmpDir,
+ "destination": buildDir,
"workspace": st.Workspace.Name(),
}
@@ -112,8 +109,8 @@
// now run the build
params = map[string]interface{}{
- "dockerfile": filepath.Join(tmpDir, filepath.Base(dockerfile)),
- "buildContext": tmpDir,
+ "dockerfile": filepath.Join(buildDir, filepath.Base(dockerfile)),
+ "buildContext": buildDir,
"Tags": tags,
"Target": b.Target,
"Labels": labels,
--- a/docker/environment.go Mon Jan 15 16:19:58 2018 -0600
+++ b/docker/environment.go Mon Jan 15 16:20:18 2018 -0600
@@ -53,28 +53,18 @@
return err
}
- // Create a temp directory we can export files from the current
- // container into. We can't read directly from the volume, and
- // we don't want to require reading only environment files on
- // the host.
-
- tmpDir, err := ioutil.TempDir("", "convey-environment-")
+ // create a directory in our state for the task
+ envDir := filepath.Join(st.Directory, name)
+ err = os.MkdirAll(envDir, 0700)
if err != nil {
return err
}
- // Remove temp dir on app exit
- cleanupFn := st.CleanupList.Add(func() { os.RemoveAll(tmpDir) })
-
- // Call cleanup function on defer, which may instead be called
- // if the cleanup thread traps a signal.
- defer cleanupFn()
-
for _, file := range files {
// Export the file into the temp directory and maintain the
// structure of the file (for ease of error messages, so we
// get the file a/b/c/env instead of env).
- dest := filepath.Clean(filepath.Join(tmpDir, file))
+ dest := filepath.Clean(filepath.Join(envDir, file))
if err = exportFile(name, st.Workspace.Name(), file, dest, st); err != nil {
return err
--- a/docker/run.go Mon Jan 15 16:19:58 2018 -0600
+++ b/docker/run.go Mon Jan 15 16:20:18 2018 -0600
@@ -95,27 +95,16 @@
{{.Image}}{{if .Command}} {{.Command}}{{end}}`
// writeScript will write a shell script to the given tempfile for the given commands
-func (r *Run) writeScript(st *state.State, fullEnv []string) (string, string, string, error) {
- // figure out the current working directory
- pwd, err := os.Getwd()
- if err != nil {
- return "", "", "", err
- }
-
- // now get the absolute path
- absPwd, err := filepath.Abs(pwd)
+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
+ scriptsDir := filepath.Join(st.Directory, name)
+ err := os.MkdirAll(scriptsDir, 0700)
if err != nil {
return "", "", "", err
}
// create the temp file to write the script to
- script, err := ioutil.TempFile(absPwd, "convey-script-")
- if err != nil {
- return "", "", "", err
- }
-
- // set scriptFile to the name of the temp file
- scriptFile := script.Name()
+ scriptFile := filepath.Join(scriptsDir, "script")
entryPoint := r.Shell
if entryPoint == "" {
@@ -137,11 +126,11 @@
commandArg := scriptFile
// write the script to the file
- script.WriteString(strings.Join(scripts, "\n"))
- script.Close()
-
- // make the script executable to the user
- os.Chmod(scriptFile, 0700)
+ ioutil.WriteFile(
+ scriptFile,
+ []byte(strings.Join(scripts, "\n")),
+ 0700,
+ )
// return it all
return scriptFile, entryPoint, commandArg, nil
@@ -157,7 +146,17 @@
}
cid := strings.TrimSpace(stdout)
- st.MarkDetached(cid)
+
+ st.Cleanup(func(logger *gomol.LogAdapter) {
+ logger.Debugf("stopping container %s", cid)
+
+ err := StopContainer(cid, logger, st)
+ if err != nil {
+ logger.Warnf("failed to stop container %s: %s", cid, err.Error())
+ } else {
+ logger.Infof("stopped container %s", cid)
+ }
+ })
logger.Infof("started detached container %s", cid)
@@ -296,17 +295,10 @@
// if we're using a script defined in the yaml, create it and override
// some variables
if len(r.Script) > 0 {
- scriptFile, entryPoint, commandArg, err = r.writeScript(st, fullEnv)
+ scriptFile, entryPoint, commandArg, err = r.writeScript(name, st, fullEnv)
if err != nil {
return err
}
-
- // Be sure to clean up the script on app exit
- cleanupFn := st.CleanupList.Add(func() { os.Remove(scriptFile) })
-
- // Call cleanup function on defer, which may instead be called
- // if the cleanup thread traps a signal.
- defer cleanupFn()
}
taskLabel := normalize.Normalize(
--- a/main.go Mon Jan 15 16:19:58 2018 -0600
+++ b/main.go Mon Jan 15 16:20:18 2018 -0600
@@ -26,7 +26,6 @@
"github.com/alecthomas/kingpin"
"github.com/aphistic/gomol"
- "bitbucket.org/rw_grim/convey/cleanup"
"bitbucket.org/rw_grim/convey/config"
"bitbucket.org/rw_grim/convey/environment"
"bitbucket.org/rw_grim/convey/loaders/bitbucket"
@@ -205,6 +204,9 @@
return 1
}
+ // create our state
+ st := state.New()
+
// setup logging
if err := logging.Setup(*color, *verbose); err != nil {
fmt.Printf("failed to setup logging: %s\n", err)
@@ -212,6 +214,9 @@
}
defer gomol.ShutdownLoggers()
+ // now defer the state shutdown so we can still log
+ defer st.Destroy()
+
// now load the config
loader := determineLoader()
@@ -244,11 +249,8 @@
return 1
}
- cleanupList := cleanup.NewList()
-
- st := state.New()
+ // set the state's variables and validate it
st.CfgPath = cfgPath
- st.CleanupList = cleanupList
st.KeepWorkspace = *keep
st.DisableDeprecated = *disableDeprecated
st.ForceSequential = *forceSequential
--- a/plans/plans.go Mon Jan 15 16:19:58 2018 -0600
+++ b/plans/plans.go Mon Jan 15 16:20:18 2018 -0600
@@ -56,6 +56,14 @@
}
st.Network = network
logger.Infof("created network %s", st.Network.Name())
+ st.Cleanup(func(logger *gomol.LogAdapter) {
+ logger.Infof("removing network %s", st.Network.Name())
+ if err := st.Network.Destroy(); err != nil {
+ logger.Fatalf("failed to remove network %s", st.Network.Name())
+ } else {
+ logger.Infof("removed network %s", st.Network.Name())
+ }
+ })
// create the workspace
logger.Info("creating workspace...")
@@ -71,47 +79,11 @@
return nil
}
-func (p *Plan) teardown(logger *gomol.LogAdapter, st *state.State) {
- // run through the DetachedContainers and stop them
- for _, cid := range st.GetDetached() {
- logger.Infof("removing detached container %s", cid)
-
- docker.StopContainer(cid, logger, st)
- }
-
- // remove the workspace (if requested)
- if st.KeepWorkspace {
- logger.Infof("not removing workspace %s as requested", st.Workspace.Name())
- } else {
- logger.Infof("removing workspace %s", st.Workspace.Name())
- if err := st.Workspace.Destroy(); err != nil {
- logger.Fatalf("failed to remove workspace %s", st.Workspace.Name())
- } else {
- logger.Infof("removed workspace %s", st.Workspace.Name())
- }
- }
-
- // remove the network
- logger.Infof("removing network %s", st.Network.Name())
- if err := st.Network.Destroy(); err != nil {
- logger.Fatalf("failed to remove network %s", st.Network.Name())
- } else {
- logger.Infof("removed network %s", st.Network.Name())
- }
-}
-
// Execute runs the plan.
func (p *Plan) Execute(path string, tasks map[string]tasks.Task, env []string, st *state.State) error {
logger := logging.NewAdapter(path)
planEnv := environment.Merge(env, p.Environment)
- // Teardown plan on defer on app exit
- cleanupFn := st.CleanupList.Add(func() { p.teardown(logger, st) })
-
- // Call cleanup function on defer, which may instead be called
- // if the cleanup thread traps a signal.
- defer cleanupFn()
-
if err := p.setup(logger, st); err != nil {
return err
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/state/cleanup.go Mon Jan 15 16:20:18 2018 -0600
@@ -0,0 +1,110 @@
+// 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 state
+
+import (
+ "os"
+ "os/signal"
+ "path/filepath"
+ "sync"
+ "syscall"
+
+ "github.com/aphistic/gomol"
+ "github.com/emirpasic/gods/stacks/arraystack"
+
+ "bitbucket.org/rw_grim/convey/logging"
+)
+
+// cleanupList is a simple structure for keeping track of functions to call
+// on cleanup.
+type cleanupList struct {
+ mutex sync.Mutex
+ functions *arraystack.Stack
+}
+
+// newCleanupList create and initalizes a cleanupList
+func newCleanupList() *cleanupList {
+ return &cleanupList{
+ functions: arraystack.New(),
+ }
+}
+
+// Add adds a function to the cleanup list.
+func (cl *cleanupList) Add(fn func(l *gomol.LogAdapter)) {
+ cl.mutex.Lock()
+ defer cl.mutex.Unlock()
+
+ cl.functions.Push(fn)
+}
+
+// run will run all of the cleanup functions in the list
+func (cl *cleanupList) run() {
+ cl.mutex.Lock()
+ defer cl.mutex.Unlock()
+
+ logger := logging.NewAdapter("cleanup")
+
+ it := cl.functions.Iterator()
+ for it.Next() {
+ fn := it.Value().(func(*gomol.LogAdapter))
+
+ fn(logger)
+ }
+}
+
+// cleanupOnSignal will call State.Destroy on SIGINT or SIGTERM
+func (st *State) cleanupOnSignal() {
+ // create a channel to receive the signal
+ ch := make(chan os.Signal, 1)
+
+ go func() {
+ // block until we get a signal, we don't care what it is so we just
+ // throw it away
+ <-ch
+
+ // call the destroy method
+ st.Destroy()
+
+ // exit with an exit code of 1
+ os.Exit(1)
+ }()
+
+ signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
+}
+
+// Cleanup registers a function to be called when the state is being torn down.
+func (st *State) Cleanup(fn func(l *gomol.LogAdapter)) {
+ st.cleanupList.Add(fn)
+}
+
+// destroy will clean up the given state and run any registered cleanup
+// functions
+func (st *State) Destroy() {
+ // run the registered cleanup functions
+ st.cleanupList.run()
+
+ // finally remove the workspace if requested
+ if !st.KeepWorkspace {
+ if err := os.RemoveAll(st.Directory); err != nil {
+ st.logger.Warningf("error removing %s: %s\n", st.Directory, err.Error())
+ }
+
+ // try to remove the .convey directory. This will only succeed when
+ // our state is the only one and that's intentional.
+ os.Remove(filepath.Dir(st.Directory))
+ }
+}
--- a/state/state.go Mon Jan 15 16:19:58 2018 -0600
+++ b/state/state.go Mon Jan 15 16:20:18 2018 -0600
@@ -19,13 +19,17 @@
import (
"fmt"
+ "io/ioutil"
"os"
+ "path/filepath"
"strings"
"sync"
"time"
- "bitbucket.org/rw_grim/convey/cleanup"
+ "github.com/aphistic/gomol"
+
"bitbucket.org/rw_grim/convey/environment"
+ "bitbucket.org/rw_grim/convey/logging"
"bitbucket.org/rw_grim/convey/network"
"bitbucket.org/rw_grim/convey/workspace"
)
@@ -34,8 +38,11 @@
// State holds all of the runtime data during a run.
type State struct {
+ Directory string
+ logger *gomol.LogAdapter
+ cleanupList *cleanupList
+
CfgPath string
- CleanupList *cleanup.List
Network network.Network
Workspace workspace.Workspace
KeepWorkspace bool
@@ -82,10 +89,43 @@
// New creates a new state.
func New() *State {
- return &State{
+ dir, err := createDirectory()
+ if err != nil {
+ panic(err)
+ }
+
+ st := &State{
+ Directory: dir,
+ logger: logging.NewAdapter("state"),
+ cleanupList: newCleanupList(),
runningContainers: map[string]struct{}{},
detachedContainers: map[string]struct{}{},
}
+
+ st.cleanupOnSignal()
+
+ return st
+}
+
+// createDirectory creates the state directory for this run.
+func createDirectory() (string, error) {
+ pwd, err := os.Getwd()
+ if err != nil {
+ return "", err
+ }
+
+ parent := filepath.Join(pwd, ".convey")
+ err = os.MkdirAll(parent, 0700)
+ if err != nil {
+ return "", err
+ }
+
+ dir, err := ioutil.TempDir(parent, "")
+ if err != nil {
+ return "", err
+ }
+
+ return dir, nil
}
// Valid validates whether the state is correct or not.
@@ -255,7 +295,6 @@
// is used to map a slice within an extended task.
func (st *State) WrapWithExpandableEnv(env, expandable []string, delimiter string) *State {
return &State{
- CleanupList: st.CleanupList,
Network: st.Network,
Workspace: st.Workspace,
KeepWorkspace: st.KeepWorkspace,