├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── persist.go └── supervise.go /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | lastSet.dat 3 | *~ 4 | *.tar.xz 5 | containers/ 6 | docker-supervise 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM progrium/busybox 2 | MAINTAINER Robert Xu 3 | 4 | ADD ./build/docker-supervise /bin/docker-supervise 5 | 6 | ENV DOCKER_HOST unix:///tmp/docker.sock 7 | 8 | VOLUME ["/mnt/data"] 9 | 10 | ENV PERSIST /mnt/data 11 | 12 | EXPOSE 8080 13 | 14 | ENTRYPOINT ["/bin/docker-supervise"] 15 | CMD [] 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean dist 2 | 3 | NAME=docker-supervise 4 | VERSION=0.00 5 | 6 | UNAME_S := $(shell uname -s) 7 | 8 | all: bin 9 | 10 | bin: *.go 11 | ifeq ($(UNAME_S), Linux) 12 | go build -o build/docker-supervise 13 | endif 14 | ifeq ($(UNAME_S), Darwin) 15 | GOOS=linux GOARCH=amd64 go build -o build/docker-supervise 16 | go build -o build/docker-supervise-darwin 17 | endif 18 | 19 | clean: 20 | rm -rf build 21 | rm -rf *~ 22 | 23 | dist: clean 24 | git archive --format=tar --prefix=$(NAME)-$(VERSION)/ HEAD | xz -9v > $(NAME)-$(VERSION).tar.xz 25 | 26 | container: bin Dockerfile 27 | docker build --no-cache -t docker-supervise . 28 | touch build/container -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | docker-supervise 2 | ================ 3 | 4 | Monitors containers via name and automatically restarts those that die. 5 | 6 | Building: 7 | 8 | It is probably a good idea to use `go get -u` to retrieve this package, as it also downloads all the necessary dependencies to build it. 9 | 10 | Usage: 11 | 12 | `DOCKER_HOST`: path to docker socket/port/etc 13 | 14 | HTTP API: 15 | 16 | * `GET /` list of containers being monitored (JSON array of container names) 17 | * `POST /` takes 'id':'[id or name]', and begins to monitor it 18 | * `GET /{id or name}` get configuration of container (404 if not monitored) 19 | * `DELETE /{id or name}` do not monitor this container anymore 20 | 21 | OS X Users: 22 | 23 | I'll leave [this here](http://dave.cheney.net/2012/09/08/an-introduction-to-cross-compilation-with-go). 24 | -------------------------------------------------------------------------------- /persist.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/fsouza/go-dockerclient" 12 | ) 13 | 14 | const ( 15 | ModeTypicalPerm os.FileMode = 0755 16 | ) 17 | 18 | type Persister interface { 19 | Add(string, *docker.Config) error 20 | Get(string) (*docker.Config, error) 21 | GetAll() (map[string]*docker.Config, error) 22 | Remove(string) error 23 | } 24 | 25 | type ConfigStore struct { 26 | config map[string]*docker.Config 27 | saver Persister 28 | mutex *sync.RWMutex 29 | } 30 | 31 | func NewConfigStore(p Persister) *ConfigStore { 32 | configStore := &ConfigStore{ 33 | config: make(map[string]*docker.Config), 34 | saver: p, 35 | mutex: &sync.RWMutex{}, 36 | } 37 | 38 | configStore.Load() 39 | return configStore 40 | } 41 | 42 | // Load items from the persister. 43 | func (c *ConfigStore) Load() error { 44 | if c.saver == nil { 45 | return nil 46 | } 47 | 48 | m, err := c.saver.GetAll() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | for k, v := range m { 54 | c.config[k] = v 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (c *ConfigStore) Add(name string, config *docker.Config) { 61 | c.mutex.Lock() 62 | defer c.mutex.Unlock() 63 | 64 | c.config[name] = config 65 | 66 | if c.saver != nil { 67 | if err := c.saver.Add(name, config); err != nil { 68 | log.Printf("persist: add error: %s", err) 69 | } 70 | } 71 | } 72 | 73 | func (c *ConfigStore) Copy() map[string]*docker.Config { 74 | c.mutex.RLock() 75 | defer c.mutex.RUnlock() 76 | 77 | m := make(map[string]*docker.Config) 78 | 79 | for k, v := range c.config { 80 | m[k] = v 81 | } 82 | 83 | return m 84 | } 85 | 86 | func (c *ConfigStore) Get(name string) (*docker.Config, bool) { 87 | c.mutex.RLock() 88 | defer c.mutex.RUnlock() 89 | 90 | config, ok := c.config[name] 91 | return config, ok 92 | } 93 | 94 | func (c *ConfigStore) Remove(name string) { 95 | c.mutex.Lock() 96 | defer c.mutex.Unlock() 97 | 98 | delete(c.config, name) 99 | if c.saver != nil { 100 | if err := c.saver.Remove(name); err != nil { 101 | log.Printf("persist: remove error: %s", err) 102 | } 103 | } 104 | } 105 | 106 | type DirectoryPersister string 107 | 108 | func (d DirectoryPersister) Filename(name string) string { 109 | return string(d) + "/" + name + ".json" 110 | } 111 | 112 | func (d DirectoryPersister) Add(name string, config *docker.Config) error { 113 | marshal, err := json.MarshalIndent(config, "", " ") 114 | if err != nil { 115 | return err 116 | } 117 | 118 | return ioutil.WriteFile(d.Filename(name), marshal, ModeTypicalPerm) 119 | } 120 | 121 | func (d DirectoryPersister) Get(name string) (*docker.Config, error) { 122 | bte, err := ioutil.ReadFile(d.Filename(name)) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | var cfg docker.Config 128 | err = json.Unmarshal(bte, &cfg) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | return &cfg, nil 134 | } 135 | 136 | func (d DirectoryPersister) GetAll() (map[string]*docker.Config, error) { 137 | files, err := ioutil.ReadDir(string(d)) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | m := make(map[string]*docker.Config) 143 | 144 | for _, v := range files { 145 | name := v.Name() 146 | name = name[:strings.LastIndex(name, ".json")] 147 | conf, err := d.Get(name) 148 | if err == nil { 149 | m[name] = conf 150 | } else { 151 | log.Printf("[warn] couldn't load %s: %v", name, err) 152 | } 153 | } 154 | 155 | return m, nil 156 | } 157 | 158 | func (d DirectoryPersister) Remove(name string) error { 159 | return os.Remove(d.Filename(name)) 160 | } 161 | -------------------------------------------------------------------------------- /supervise.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strings" 9 | 10 | "github.com/fsouza/go-dockerclient" 11 | ) 12 | 13 | func envopt(name, def string) string { 14 | if env := os.Getenv(name); env != "" { 15 | return env 16 | } 17 | return def 18 | } 19 | 20 | func marshal(obj interface{}) []byte { 21 | bytes, err := json.MarshalIndent(obj, "", " ") 22 | if err != nil { 23 | log.Println("marshal:", err) 24 | } 25 | return bytes 26 | } 27 | 28 | func supervise(client *docker.Client, config *ConfigStore) { 29 | events := make(chan *docker.APIEvents) 30 | if err := client.AddEventListener(events); err != nil { 31 | log.Fatal("failed to subscribe to docker events:", err) 32 | } 33 | for event := range events { 34 | if event.Status == "die" { 35 | container, err := client.InspectContainer(event.ID) 36 | if err != nil { 37 | log.Println("supervisor: container destroyed too quickly, skipping", event.ID) 38 | continue 39 | } 40 | 41 | name := container.Name[1:] 42 | 43 | conf, ok := config.Get(name) 44 | if !ok { 45 | continue 46 | } 47 | 48 | hostConfig := container.HostConfig 49 | 50 | if err := client.RemoveContainer(docker.RemoveContainerOptions{ID: container.ID}); err != nil { 51 | log.Println("supervisor: unable to remove container:", err) 52 | } 53 | 54 | newContainer, err := client.CreateContainer(docker.CreateContainerOptions{ 55 | Name: name, 56 | Config: conf, 57 | }) 58 | if err != nil { 59 | log.Println("supervisor: unable to create container:", err) 60 | continue 61 | } 62 | 63 | if err := client.StartContainer(newContainer.ID, hostConfig); err != nil { 64 | log.Println("supervisor: unable to start container:", err) 65 | } 66 | } 67 | } 68 | log.Fatal("supervisor loop closed unexpectedly") 69 | } 70 | 71 | func main() { 72 | persistDir := envopt("PERSIST", "./containers") 73 | endpoint := envopt("DOCKER_HOST", "unix:///var/run/docker.sock") 74 | port := envopt("PORT", "8080") 75 | 76 | client, err := docker.NewClient(endpoint) 77 | if err != nil { 78 | log.Fatal("unable to connect docker:", err) 79 | } 80 | 81 | var persister Persister 82 | if _, err := os.Stat(persistDir); os.IsNotExist(err) { 83 | log.Printf("[warn] persist dir doesn't exist, not going to persist.") 84 | } else { 85 | persister = DirectoryPersister(persistDir) 86 | } 87 | 88 | config := NewConfigStore(persister) 89 | if err := config.Load(); err != nil { 90 | log.Printf("[warn] failed to load from persist dir: %v", err) 91 | } 92 | 93 | go supervise(client, config) 94 | 95 | http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { 96 | path := strings.Trim(r.URL.Path, "/") 97 | 98 | if path == "" { 99 | switch r.Method { 100 | case "GET": 101 | list := make([]string, 0) 102 | for k, _ := range config.Copy() { 103 | list = append(list, k) 104 | } 105 | rw.Write(marshal(list)) 106 | case "POST": 107 | if err := r.ParseForm(); err != nil { 108 | http.Error(rw, err.Error(), http.StatusBadRequest) 109 | return 110 | } 111 | 112 | name := strings.Trim(r.Form.Get("id"), "/") 113 | if name == "" { 114 | http.Error(rw, "Bad request", http.StatusBadRequest) 115 | return 116 | } 117 | 118 | if _, ok := config.Get(name); ok { 119 | rw.Header().Set("Location", "/"+name) 120 | rw.WriteHeader(http.StatusSeeOther) 121 | return 122 | } 123 | 124 | container, err := client.InspectContainer(name) 125 | if err != nil { 126 | http.Error(rw, err.Error(), http.StatusBadRequest) 127 | return 128 | } 129 | 130 | config.Add(container.Name[1:], container.Config) 131 | 132 | rw.Header().Set("Location", "/"+name) 133 | rw.WriteHeader(http.StatusCreated) 134 | default: 135 | http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) 136 | } 137 | } else { 138 | conf, ok := config.Get(path) 139 | if !ok { 140 | http.Error(rw, "Not found", http.StatusNotFound) 141 | return 142 | } 143 | 144 | switch r.Method { 145 | case "GET": 146 | rw.Write(marshal(conf)) 147 | case "DELETE": 148 | config.Remove(path) 149 | default: 150 | http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) 151 | } 152 | } 153 | }) 154 | log.Fatal(http.ListenAndServe(":"+port, nil)) 155 | } 156 | --------------------------------------------------------------------------------