grim/convey

Bump the version for release
v0.14.0-alpha3
2018-02-20, Gary Kramlich
166a6d1979fa
Bump the version for release
// 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 docker
import (
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"time"
"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"
"bitbucket.org/rw_grim/convey/tasks"
"bitbucket.org/rw_grim/convey/util"
"bitbucket.org/rw_grim/convey/yaml"
)
// Run represents a docker run task which will run a container.
type Run struct {
Command string `yaml:"command"`
Detach bool `yaml:"detach"`
Hostname string `yaml:"hostname"`
EntryPoint string `yaml:"entrypoint"`
Environment yaml.StringOrSlice `yaml:"environment"`
Image string `yaml:"image"`
WorkDir string `yaml:"workdir"`
WorkSpace string `yaml:"workspace"`
Script yaml.StringOrSlice `yaml:"script"`
Shell string `yaml:"shell"`
Labels yaml.StringOrSlice `yaml:"labels"`
User string `yaml:"user"`
HealthCheck HealthCheck `yaml:"healthcheck"`
}
// UnmarshalYAML is a custom yaml unmarshaller for run tasks.
func (r *Run) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRun Run
raw := rawRun{Shell: "/bin/sh", HealthCheck: HealthCheck{}}
if err := unmarshal(&raw); err != nil {
return err
}
*r = Run(raw)
return nil
}
// 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
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
scriptFile := filepath.Join(scriptsDir, "script")
entryPoint := r.Shell
if entryPoint == "" {
entryPoint = "/bin/sh"
}
// Scripts must retain order, so don't use st.MapSlice to
// expand things (which results in a non-deterministically
// ordered slice of the expanded input). It also doesn't
// make sense to expand things here anyway - use a loop in
// bash if you need that kind of control.
scripts, err := environment.SliceMapper(r.Script, fullEnv)
if err != nil {
if nErr := os.Remove(scriptFile); nErr != nil {
fmt.Printf("error removing file: %s\n", nErr)
}
return "", "", "", err
}
// set the run command argument to the script file
commandArg := scriptFile
// write the script to the file
err = ioutil.WriteFile(scriptFile, []byte(strings.Join(scripts, "\n")), 0700)
if err != nil {
return "", "", "", err
}
// return it all
return scriptFile, entryPoint, commandArg, nil
}
// detach will run the container detached
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 {
if nErr := logger.Errorf("%s", stderr); nErr != nil {
fmt.Printf("error reporting error: %s\n", nErr)
}
return err
}
cid := strings.TrimSpace(stdout)
st.Cleanup(func(logger *gomol.LogAdapter) {
if nErr := logger.Debugf("stopping container %s", cid); nErr != nil {
fmt.Printf("error reporting debug: %s\n", nErr)
}
err = StopContainer(cid, logger, st)
if err != nil {
if nErr := logger.Warnf("failed to stop container %s: %s", cid, err.Error()); nErr != nil {
fmt.Printf("error reporting warning: %s\n", nErr)
}
} else {
if nErr := logger.Infof("stopped container %s", cid); nErr != nil {
fmt.Printf("error reporting info: %s\n", nErr)
}
}
})
if nErr := logger.Infof("started detached container %s", cid); nErr != nil {
fmt.Printf("error reporting info: %s\n", nErr)
}
if nErr := logger.Infof("checking for healthcheck"); nErr != nil {
fmt.Printf("error reporting info: %s\n", nErr)
}
// check if the container has a health check
hasHealth, err := containerHasHealthCheck(name, cid, st, logger)
if err != nil {
return err
}
if hasHealth {
healthChan := make(chan error)
if nErr := logger.Infof("waiting for container to go healthy"); nErr != nil {
fmt.Printf("error reporting info: %s\n", nErr)
}
go func() {
duration := 5 * time.Second
for {
// check the health
healthy, err := containerIsHealthy(name, cid, st, logger)
if err != nil {
healthChan <- err
}
if healthy {
healthChan <- nil
}
if nErr := logger.Infof("container still not healthy, waiting %v", duration); nErr != nil {
fmt.Printf("error reporting info: %s\n", nErr)
}
time.Sleep(duration)
}
}()
err := <-healthChan
if err != nil {
return err
}
if lErr := logger.Infof("container is ready"); lErr != nil {
fmt.Printf("error reporting info: %s\n", lErr)
}
} else {
if lErr := logger.Infof("no healthcheck found"); lErr != nil {
fmt.Printf("error reporting info: %s\n", lErr)
}
}
return nil
}
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)
}
// add the provided interval
if r.HealthCheck.Interval != time.Duration(0) {
cmd.Append("--health-interval", r.HealthCheck.Interval.String())
}
// add the provided retries
if r.HealthCheck.Retries != 0 {
cmd.Append("--health-retries", fmt.Sprintf("%d", r.HealthCheck.Retries))
}
// add the provided start period
if r.HealthCheck.StartPeriod != time.Duration(0) {
cmd.Append("--health-start-period", r.HealthCheck.StartPeriod.String())
}
// add the provided timeout
if r.HealthCheck.Timeout != time.Duration(0) {
cmd.Append("--health-timeout", r.HealthCheck.Timeout.String())
}
}
// Execute runs the run task.
func (r *Run) Execute(name string, logger *gomol.LogAdapter, env []string, st *state.State) error {
fullEnv := environment.Merge(env, r.Environment)
fullEnv = environment.Merge(fullEnv, st.GetEnv())
// expand the environment
for idx, item := range fullEnv {
v, err := environment.Mapper(item, fullEnv)
if err != nil {
return err
}
fullEnv[idx] = v
}
// create an id for this container
runID := util.ShortID()
// Map the image name so we can use it elsewhere
image, err := environment.Mapper(r.Image, fullEnv)
if err != nil {
return err
} else if image == "" {
return errNoImage
}
// 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)
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)
} else {
id, nErr := ImageID(image, logger, st)
if nErr != nil {
return err
}
if id != "" {
// Check if the image does _not_ have a workdir set. If it doesn't,
// set workdir to the convey workspace
cmdv := []string{
"inspect",
"--format",
"{{.Config.WorkingDir}}",
image,
}
stdout, stderr, nErr := DockerOutput("checkWorkDir", cmdv, st)
if nErr != nil {
if lErr := logger.Errorf("%s", stderr); lErr != nil {
fmt.Printf("error reporting error: %s\n", lErr)
}
return nErr
}
if strings.TrimSpace(stdout) == "" {
cmd.Append("-w", workspaceMount)
}
}
}
// 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
var scriptFile string
entryPoint := r.EntryPoint
commandArg, err := environment.Mapper(r.Command, fullEnv)
if err != nil {
return err
}
// 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(name, st, fullEnv)
if err != nil {
return err
}
if scriptFile != "" {
cmd.Append("-v", scriptFile+":"+scriptFile)
}
}
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)),
)
// 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)
}
// 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
cmd.Append(image)
// append the command if we have one
if commandArg != "" {
cmd.Append(commandArg)
}
if lErr := logger.Infof("running container with id %s", runID); lErr != nil {
fmt.Printf("error reporting info: %s\n", lErr)
}
//
// Everything after this should be mostly dead code
//
if r.Detach {
return r.detach(name, cmd.Command(), st, logger)
}
// Mark running so we can detach this container form the network
// when we've got a signal to shutdown. An active network cannot
// be removed, so we need to track things to kill along with it.
// This is no longer a problem once the subprocess returns.
st.MarkRunning(runID)
defer st.UnmarkRunning(runID)
// run the command
return Docker(name, cmd.Command(), st)
}
// New creates a new run task.
func (r *Run) New() tasks.Task {
return &Run{}
}
// Valid validates the run task.
func (r *Run) Valid() error {
if r.Image == "" {
return errNoImage
}
return nil
}