grim/convey
Clone
Summary
Browse
Changes
Graph
Update all of the images README.md's for the new task type format
2018-02-18, Gary Kramlich
ad96c0ecad0e
Update all of the images README.md's for the new task type format
// 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
{
os
.
Remove
(
scriptFile
)
return
""
,
""
,
""
,
err
}
// set the run command argument to the script file
commandArg
:=
scriptFile
// write the script to the file
ioutil
.
WriteFile
(
scriptFile
,
[]
byte
(
strings
.
Join
(
scripts
,
"\n"
)),
0700
,
)
// 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
{
logger
.
Errorf
(
"%s"
,
stderr
)
return
err
}
cid
:=
strings
.
TrimSpace
(
stdout
)
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
)
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
}
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
,
v
:=
range
fullEnv
{
v
,
err
:=
environment
.
Mapper
(
v
,
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
)
fmt
.
Printf
(
"mountPoint: '%s'\n"
,
dockerWorkspace
.
mountPoint
)
fmt
.
Printf
(
"workspacePath: '%s'\n"
,
workspacePath
)
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
,
err
:=
ImageID
(
image
,
logger
,
st
)
if
err
!=
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
,
err
:=
DockerOutput
(
"checkWorkDir"
,
cmdv
,
st
)
if
err
!=
nil
{
logger
.
Errorf
(
"%s"
,
stderr
)
return
err
}
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
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
.
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
)
}
logger
.
Infof
(
"running container with id %s"
,
runID
)
//
// 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
}