First draft

This commit is contained in:
Peter Bourgon 2015-09-25 12:30:24 +02:00
parent 33c958bc9d
commit 4e6968fe8f
8 changed files with 247 additions and 0 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
reap
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM alpine:latest
RUN echo "http://dl-4.alpinelinux.org/alpine/edge/testing" >>/etc/apk/repositories && apk add --update runit && rm -rf /var/cache/apk/*
ADD reap /
ADD foo /
RUN mkdir -p /etc/service/foo
ADD run-foo /etc/service/foo/run
ADD bar /
RUN mkdir -p /etc/service/bar
ADD run-bar /etc/service/bar/run
ENTRYPOINT ["/reap"]

58
README.md Normal file
View File

@ -0,0 +1,58 @@
# reap
If you have a Docker container that's a collection of runit-supervised daemons,
this process is suitable for use as the ENTRYPOINT.
```Docker
FROM alpine:latest
RUN echo "http://dl-4.alpinelinux.org/alpine/edge/testing" >>/etc/apk/repositories && apk add --update runit && rm -rf /var/cache/apk/*
ADD foo /
RUN mkdir -p /etc/service/foo
ADD run-foo /etc/service/foo/run
ADD bar /
RUN mkdir -p /etc/service/bar
ADD run-bar /etc/service/bar/run
ADD reap /
ENTRYPOINT ["/reap"]
```
**Why not just exec runsvdir?**
`docker stop` issues SIGTERM (or, in a future version of Docker, perhaps another custom signal)
but if runsvdir receives a signal,
it doesn't wait for its supervised processes to exit before returning.
If you don't care about graceful shutdown of your daemons, no problem, you don't need this tool.
**Why not wrap runsvdir in a simple shell script?**
This works great:
```sh
#!/bin/sh
sv_stop() {
for s in $(ls -d /etc/service/*)
do
/sbin/sv stop $s
done
}
trap "sv_stop; exit" SIGTERM
/sbin/runsvdir /etc/service &
wait
```
...except it doesn't [reap orphaned child processes](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/)
and is therefore unsuitable for being PID 1.
**Why not use my_init from phusion/baseimage-docker?**
That works great — if you're willing to add python3 to your Docker images :)
**So this is just a stripped-down my_init in Go?**
Basically, yes.

15
bar Executable file
View File

@ -0,0 +1,15 @@
#!/bin/sh
handle() {
echo "got signal"
exit
}
trap handle SIGINT
while true
do
echo bar: `date`
sleep 1
done

17
foo Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
handle() {
echo "got signal"
exit
}
trap "handle" SIGINT
for i in `seq 1 5`
do
echo foo: $i
sleep 1
done
echo foo: terminating!

135
main.go Normal file
View File

@ -0,0 +1,135 @@
package main
import (
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
)
const etcService = "/etc/service"
func main() {
log.SetFlags(0)
runsvdir, err := exec.LookPath("runsvdir")
if err != nil {
log.Fatal(err)
}
sv, err := exec.LookPath("sv")
if err != nil {
log.Fatal(err)
}
if fi, err := os.Stat(etcService); err != nil {
log.Fatal(err)
} else if !fi.IsDir() {
log.Fatalf("%s is not a directory", etcService)
}
if pid := os.Getpid(); pid != 1 {
log.Printf("warning: I'm not PID 1, I'm PID %d", pid)
}
go reapAll()
supervisor := cmd(runsvdir, etcService)
if err := supervisor.Start(); err != nil {
log.Fatal(err)
}
log.Printf("%s started", runsvdir)
go gracefulShutdown(sv, supervisor.Process)
if err := supervisor.Wait(); err != nil {
log.Printf("%s exited with error: %v", runsvdir, err)
} else {
log.Printf("%s exited cleanly", runsvdir)
}
}
func reapAll() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGCHLD)
for range c {
go reapOne()
}
}
// From https://github.com/ramr/go-reaper/blob/master/reaper.go
func reapOne() {
var (
ws syscall.WaitStatus
pid int
err error
)
for {
pid, err = syscall.Wait4(-1, &ws, 0, nil)
if err != syscall.EINTR {
break
}
}
if err == syscall.ECHILD {
return
}
log.Printf("reaped child process %d (%+v)", pid, ws)
}
type signaler interface {
Signal(os.Signal) error
}
func gracefulShutdown(sv string, s signaler) {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
log.Printf("received %s", <-c)
matches, err := filepath.Glob(filepath.Join(etcService, "*"))
if err != nil {
log.Printf("when shutting down services: %v", err)
return
}
var stopped []string
for _, match := range matches {
fi, err := os.Stat(match)
if err != nil {
log.Printf("%s: %v", match, err)
continue
}
if !fi.IsDir() {
log.Printf("%s: not a directory", match)
continue
}
service := filepath.Base(match)
stop := cmd(sv, "stop", service)
if err := stop.Run(); err != nil {
log.Printf("%s: %v", strings.Join(stop.Args, " "), err)
continue
}
stopped = append(stopped, service)
}
log.Printf("stopped %d: %s", len(stopped), strings.Join(stopped, ", "))
log.Printf("stopping supervisor...")
if err := s.Signal(syscall.SIGTERM); err != nil {
log.Print(err)
}
log.Printf("graceful SIGTERM handler exiting")
}
func cmd(path string, args ...string) *exec.Cmd {
return &exec.Cmd{
Path: path,
Args: append([]string{path}, args...),
Env: os.Environ(),
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
}

3
run-bar Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
exec /bar

3
run-foo Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
exec /foo