Skip to content

Commit

Permalink
Add --user option that works on OSX but for some reason NOT on LINUX!!!
Browse files Browse the repository at this point in the history
Rework how we remove --start and --run commands from the os.Args
Add --help for verbose help, and shorten usage() drastically
Turn off superfluous commentary unless we're in --verbose mode
Stop using goxc to build cross-platform binaries
Fix hang that was happening on --run commands with no primary
  • Loading branch information
markriggins committed Apr 15, 2016
1 parent 34d609b commit 18d9c3f
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 101 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
*.gz
dist/*
.DS_Store
build/*
.DS_Store
.dockerfy*
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dockerfy:
echo "Building dockerfy"
go install -ldflags "$(LDFLAGS)"


dist-clean:
rm -rf dist
rm -f dockerfy-linux-*.tar.gz
Expand Down
51 changes: 34 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ missing OS functionality (such as an init process, and reaping zombies etc.)
4. Secrets injected into configuration files (without leaking them to the environment)
5. Waiting for dependencies (any server and port) to become available before the primary command starts
6. Tailing log files to the container's stdout and/or stderr
8. Running commands before the primary command begins
7. Starting Services -- and shutting down the container if they fail
7. Running commands before the primary command begins
8. Starting Services -- and shutting down the container if they fail
9. Propagating signals to child processes
9. Reaping Zombie (defunct) processes
10. Reaping Zombie (defunct) processes
11. Running services and commands and various user accounts


## Dockerfile Example

Expand All @@ -28,6 +30,7 @@ missing OS functionality (such as an init process, and reaping zombies etc.)
"--run", "/app/bin/migrate_lock --server='${MYSQLSERVER}:${MYSQLPORT}'", "--", \
"--start", "/app/bin/cache-cleaner-daemon", "-p", "{{ .Secret.DB_PASSWORD }}", "--",\
"--reap", \
"--user", "nobody", \
"nginx", "-g", "daemon off;" ]

## equivalent docker-compose.yml Example
Expand All @@ -50,7 +53,8 @@ missing OS functionality (such as an init process, and reaping zombies etc.)
"--wait", "tcp://${MYSQLSERVER}:${MYSQLPORT", "--timeout", "60s",
"--run", "/app/bin/migrate_lock --server='${MYSQLSERVER}:${MYSQLPORT}'", "--",
"--start", "/app/bin/cache-cleaner-daemon", "-p", "{{ .Secret.DB_PASSWORD }}", "--",
"--reap",
"--reap",
"--user", "nobody",
'--', 'nginx', '-g', 'daemon off;' ]

Expand All @@ -61,20 +65,20 @@ The above example will run the nginx program inside a docker container, but **be
2. **Load secret settings** from a file a /secrets/secrets.env, that become available for use in templates as {{ .Secret.**VARNAME** }}
3. **Execute the nginx.conf.tmpl template**. This template uses the powerful go language templating features to substitute environment variables and secret settings directly into the nginx.conf file. (Which is handy since nginx doesn't read the environment itself.) Every occurance of {{ .Env.**VARNAME** }} will be replaced with the value of $VARNAME, and every {{ .Secret.**VARNAME** }} will be replaced with the secret value of VARNAME.
4. **Wait** for the http://${MYSQLSERVER} server to start accepting requests on port ${MYSQLPORT} for up to 60 seconds
4. **Run migrate_lock** a program to perform a Django/MySql database migration to update the database schema, and wait for it to finish.
5. **Start the cache-cleaner-daemon**, which will run in the background presumably cleaning up stale cache files while nginx runs
6. **Start Reaping Zombie processes** under a separate goroutine in case the cache-cleaner-deamon loses track of its child processes.
7. **Run nginx** with its customized nginx.conf file and html
8. **Propagate Signals** to all processes, so the container can exit cleanly on SIGHUP or SIGINT
9. **Monitor Processes** and exit if nginx or the cache-cleaner-daemon dies
5. **Run migrate_lock** a program to perform a Django/MySql database migration to update the database schema, and wait for it to finish.
6. **Start the cache-cleaner-daemon**, which will run in the background presumably cleaning up stale cache files while nginx runs
7. **Start Reaping Zombie processes** under a separate goroutine in case the cache-cleaner-deamon loses track of its child processes.
8. **Run nginx** with its customized nginx.conf file and html as user `nobody`
9. **Propagate Signals** to all processes, so the container can exit cleanly on SIGHUP or SIGINT
10. **Monitor Processes** and exit if nginx or the cache-cleaner-daemon dies


This all assumes that the /secrets volume was mounted and the environment variables $MYSQLSERVER, $MYSQLPORT
and $DEPLOYMENT_ENV were set when the container started. Note that **dockerfy** expands the environment variables in its arguments, since the ENTRYPOINT [] form in Dockerfiles does not, replacing all $VARNAME, {{ .Env.VARNAME }} and {{ .Secret.VARNAME }} occurances with their values from the environment or secrets file.

Note that the `ps -ef` command would list the unexpanded argument '{{ .Secret.DB_PASSWORD }}', not the actual password

The "--" argument is used to signify the end of arguments for a -start or -run command.
The "--" argument is used to signify the end of arguments for a --start or --run command.


# Typical Use-Case
Expand Down Expand Up @@ -114,7 +118,7 @@ The entire ./overlays files must be COPY'd into the Docker image (usually along

COPY / /app

Then the desired alternative for the files can be chosen at runtime use the -overlay *src:dest* option
Then the desired alternative for the files can be chosen at runtime use the --overlay *src:dest* option

$ dockefy --overlay /app/overlays/_commmon/html:/usr/share/nginx/ \
--overlay /app/overlays/$DEPLOYMENT_ENV/html:/usr/share/nginx/ \
Expand Down Expand Up @@ -233,8 +237,7 @@ NOTE: MySql server is not an HTTP server, so use the tcp protocol instead of htt

$ dockerfy --wait tcp://$MYSQLSERVER:$MYSQLPORT --timeout 120s ...

You can specify multiple dependancies by repeating the -wait flag. If the dependancies fail to become available before the timeout (which defaults to 10 seconds), then dockery will exit, and your primary command will not be run.

You can specify multiple dependancies by repeating the --wait flag. If the dependancies fail to become available before the timeout (which defaults to 10 seconds), then dockery will exit, and your primary command will not be run.

### Running Commands
The `--run` option gives you the opportunity to run commands **after** the overlays, secrets and templates have been processed, but **before** the primary program begins. You can run anything you like, even bash scripts like this:
Expand All @@ -257,6 +260,20 @@ The `--start` option gives you the opportunity to start a commands as a service
All options up to but not including the '--' will be passed to the command. You can start as many services as you like, they will all be started in the same order as how they were provided on the command line, and all commands must continue **successfully** or **dockerfy** will
stop your primary command and exit, and the container will stop.

### Switching User Accounts
The `--user` option gives you the ability specify which user accounts with which to run commands or start services. The `--user` flag takes either a username or UID as its argument, and affects all subsequent commands.

$ dockerfy \
--user mark --run id -F -- \
--user bob --run id -F -- \
--user 0 --run id -F -- \
id -a

The above command will first run the `id -F` command as user "mark", which will print mark's full name "Mark Riggins".
Then it will print bob's full name. Next it will print the full name of the account with user id 0, which happens to be "root". Finally the primary command `id` will run with as the user account of the `last` invokation of the `--user` option, giving us the full id information for the root account.

The **dockerfy** command itself will continue to run as the root user so it will have permission to monitor and signal any services that were started.

### Reaping Zombies
Long-lived containers should with services use the `--reap` option to clean up any zombie processes that might arise if a service fails to wait for its child processes to die. Otherwise, eventually the process table can fill up and your container will become unresponsive. Normally the init daemon would do this important task, but docker containers do not have an init daemon, so **dockerfy** will assume the responsibility.

Expand All @@ -266,9 +283,9 @@ Note that in order for this work fully, **dockerfy** should be the primary proce
**Dockerfy** passes SIGHUP, SIGINT, SIGQUIT, SIGTERM and SIGKILL to all commands and services, giving them a brief chance to respond, and then kills them and exits. This allows your container to exit gracefully, and completely shut down services, and not hang when it us run in interactive mode via `docker run -it ...` when you type ^C

### Tailing Log Files
Some programs (like nginx) insist on writing their logs to log files instead of stdout and stderr. Although nginx can be tricked into doing the desired thing by replacing the default log files with symbolic links to /dev/stdout and /dev/stderr, we really don't know how every program out there does its logging, so **dockerfy** gives you to option of tailing as many log files as you wish to stdout and stderr via the -stdout and -stderr flags.
Some programs (like nginx) insist on writing their logs to log files instead of stdout and stderr. Although nginx can be tricked into doing the desired thing by replacing the default log files with symbolic links to /dev/stdout and /dev/stderr, we really don't know how every program out there does its logging, so **dockerfy** gives you to option of tailing as many log files as you wish to stdout and stderr via the --stdout and --stderr flags.

$ dockerfy --stdout info.log -stdout perf.log
$ dockerfy --stdout info.log --stdout perf.log


If `inotify` does not work in you container, you use `--log-poll` to poll for file changes instead.
Expand All @@ -291,7 +308,7 @@ But of course, use the latest release!


##Inspiration and Open Source Usage
Dockerfy is based on the work of others, relying heavily on jwilder's templates, wait, and log tailer, [ dockerize](https://github.com/jwilder/dockerize) and miekg's zinit ](https://github.com/miekg/dinit) a small init-like "daemon", and other tips on stackoverflow and other places of public commentary about docker.
Dockerfy is based on the work of others, relying heavily on jwilder's wait, and log tailer, [ dockerize](https://github.com/jwilder/dockerize) and miekg's dinit ](https://github.com/miekg/dinit) a small init-like "daemon", and other tips on stackoverflow and other places of public commentary about docker.

The secrets injection, overlays, and commands that run before the primary command starts and the command-line syntax for running commands and starting services are unique to **dockerfy**.

Expand Down
80 changes: 66 additions & 14 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,98 @@ import (
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"
)

type Commands struct {
run []*exec.Cmd // list of commands to run BEFORE the primar
start []*exec.Cmd // list of services to start
credential *syscall.Credential // credentials for primary command
}

//
// Removes start and run commands and their arguments from os.Args
// Returns array of removed commands
// Removes --start and --run commands options and arguments from os.Args
// Removes --user <uid|username> options and applies the credentials to following
// start or run commands and primary command
// Returns array of removed run commands, and an array of removed start commands
//
func removeCmdFromOsArgs(flag string) []*exec.Cmd {
func removeCommandsFromOsArgs() Commands {

var newOsArgs = []string{}
var commands = Commands{}

var cmd *exec.Cmd
var cmds = []*exec.Cmd{}
var cmd_user *user.User

for i := 0; i < len(os.Args); i++ {

switch {
case ("-"+flag == os.Args[i] || "--"+flag == os.Args[i]) && cmd == nil:
cmd = &exec.Cmd{Stdout: os.Stdout, Stderr: os.Stderr}
cmds = append(cmds, cmd)
case ("--start" == os.Args[i] || "-start" == os.Args[i]) && cmd == nil:
cmd = &exec.Cmd{Stdout: os.Stdout,
Stderr: os.Stderr,
SysProcAttr: &syscall.SysProcAttr{Credential: commands.credential}}
commands.start = append(commands.start, cmd)

case ("--run" == os.Args[i] || "-run" == os.Args[i]) && cmd == nil:
cmd = &exec.Cmd{Stdout: os.Stdout,
Stderr: os.Stderr,
SysProcAttr: &syscall.SysProcAttr{Credential: commands.credential}}
commands.run = append(commands.run, cmd)

case ("--user" == os.Args[i] || "-user" == os.Args[i]) && cmd == nil:
if os.Getuid() != 0 {
log.Fatalf("dockerfy must run as root to use the --user flag")
}
cmd_user = &user.User{}

case "--" == os.Args[i] && cmd != nil: // End of args for this cmd
cmd = nil

default:
if cmd != nil {
if cmd_user != nil {
// Expect a username or uid
cmd_user, _ = user.LookupId(os.Args[i])
if cmd_user == nil {
// Not a userid, try as a username
cmd_user, _ = user.Lookup(os.Args[i])
if cmd_user == nil {
log.Fatalf("unknown user: '%s'", os.Args[i])
}
}
uid, _ := strconv.Atoi(cmd_user.Uid)
gid, _ := strconv.Atoi(cmd_user.Gid)

commands.credential = new(syscall.Credential)
commands.credential.Uid = uint32(uid)
commands.credential.Gid = uint32(gid)

cmd_user = nil
} else if cmd != nil {
// Expect a command first, then a series of arguments
if len(cmd.Path) == 0 {
_ = "breakpoint"
cmd.Path = os.Args[i]
if filepath.Base(cmd.Path) == cmd.Path {
cmd.Path, _ = exec.LookPath(cmd.Path)
}
}
cmd.Args = append(cmd.Args, os.Args[i])
} else {
newOsArgs = append(newOsArgs, os.Args[i])
}
}
}
if cmd_user != nil {
log.Fatalln("need a username or uid after the --user flag")
}
if cmd != nil {
if len(cmd.Path) == 0 {
log.Fatalf("need a command after -" + flag)
}
cmds = append(cmds, cmd)
log.Fatalf("need a command after the --start or --run flag")
}
os.Args = newOsArgs
return cmds
return commands
}

func toString(cmd *exec.Cmd) string {
Expand Down
Loading

0 comments on commit 18d9c3f

Please sign in to comment.