grim/convey

closing merged branch
hostnames
2017-10-13, Gary Kramlich
33eae19fcbbe
closing merged branch
/*
* 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 docker
import (
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"time"
"github.com/aphistic/gomol"
"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/yaml"
)
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"`
HealthCheck HealthCheck `yaml:"healthcheck"`
}
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
}
const runTemplate = `run --rm
{{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 .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}}`
// buildScript will create a shell script for the given commands
func (r *Run) buildScript(st *state.State, fullEnv []string) (string, string, string, error) {
entryPoint := r.Shell
if entryPoint == "" {
entryPoint = "/bin/sh"
}
// 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)
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()
// set the run command argument to the script file
commandArg := scriptFile
scripts, err := st.MapSlice(r.Script, fullEnv)
if err != nil {
return "", "", "", err
}
// 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)
// return it all
return scriptFile, entryPoint, commandArg, nil
}
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.Environment)
user, err := user.Current()
if err != nil {
return err
}
// now expand the environment
for idx, v := range fullEnv {
v, err := environment.Mapper(v, fullEnv)
if err != nil {
return err
}
fullEnv[idx] = v
}
// assign a default workspace location
workSpace := r.WorkSpace
if workSpace == "" {
workSpace = "/workspace"
}
// initialize some variables
scriptFile := ""
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.buildScript(st, fullEnv)
if err != nil {
return err
}
// remove the file when the function exits
defer os.Remove(scriptFile)
}
taskLabel := normalize.Normalize(
fmt.Sprintf("convey-%d-task=%s", os.Getpid(), name),
)
dockerWorkspace := st.Workspace.(*Workspace)
//
// Map variables
image, err := environment.Mapper(r.Image, fullEnv)
if err != nil {
return err
}
hostname, err := environment.Mapper(r.Hostname, fullEnv)
if err != nil {
return err
}
labels, err := st.MapSlice(r.Labels, fullEnv)
if err != nil {
return err
}
workdir, err := environment.Mapper(r.WorkDir, fullEnv)
if err != nil {
return err
}
workspacePath, err := environment.Mapper(dockerWorkspace.mountPoint, fullEnv)
if err != nil {
return err
}
workspaceMount, err := environment.Mapper(workSpace, fullEnv)
if err != nil {
return err
}
// build the dict for the template
params := map[string]interface{}{
"Command": commandArg,
"CPUShares": st.CPUShares,
"Detach": r.Detach,
"Hostname": hostname,
"Environment": fullEnv,
"EntryPoint": entryPoint,
"GID": user.Gid,
"HealthCheck": r.HealthCheck,
"Image": image,
"Labels": labels,
"Memory": st.Memory,
"Network": st.Network.Name(),
"ScriptFile": scriptFile,
"SSHAgent": st.EnableSSHAgent,
"SSHAuthSock": os.Getenv("SSH_AUTH_SOCK"),
"UID": user.Uid,
"WorkDir": workdir,
"WorkspacePath": workspacePath,
"WorkspaceMount": workspaceMount,
"TaskLabel": taskLabel,
}
if r.Detach {
stdout, stderr, err := DockerOutput(name, runTemplate, params, st)
if err != nil {
logger.Errorf("%s", stderr)
return err
}
cid := strings.TrimSpace(stdout)
st.MarkDetached(cid)
logger.Infof("started detached container %s", cid)
logger.Infof("checking for healthcheck")
// 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)
logger.Infof("waiting for container to go healthy")
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
}
logger.Infof("container still not healthy, waiting %v", duration)
time.Sleep(duration)
}
}()
err := <-healthChan
if err != nil {
return err
}
logger.Infof("container is ready")
} else {
logger.Infof("no healthcheck found")
}
return nil
}
// run the command
return Docker(name, runTemplate, params, st)
}
func (r *Run) New() tasks.Task {
return &Run{}
}
func (r *Run) Valid() error {
if r.Image == "" {
return errNoImage
}
return nil
}