package main import ( "bytes" "context" "errors" "fmt" "github.com/docker/docker/api/types/filters" "gopkg.in/ini.v1" "log" "os" "os/exec" "strconv" "strings" "text/template" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/events" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/google/shlex" "github.com/ryanuber/go-glob" ) type Configuration struct { handlers []Handler } type ContainerEvent struct { Action string Name string ID string Image string Labels map[string]string Mounts []types.MountPoint Hostname string IPAddress string Ports nat.PortMap } type Handler struct { Action []string Name []string Image []string ID []string Hostname []string Label map[string]*string Handler *template.Template } func (h Handler) matches(e ContainerEvent) bool { if !stringListMatches(h.Action, e.Action) { return false } if !stringListMatches(h.Name, e.Name) { return false } if !stringListMatches(h.Image, e.Image) { return false } if !stringListMatches(h.ID, e.ID) { return false } if !stringListMatches(h.Hostname, e.Hostname) { return false } for key, val := range h.Label { actualVal, exists := e.Labels[key] if !exists { return false } if val != nil { if actualVal != *val { return false } } } return true } func (h Handler) invoke(e ContainerEvent) error { var cmdBuf bytes.Buffer err := h.Handler.Execute(&cmdBuf, e) if err != nil { return err } parts, err := shlex.Split(cmdBuf.String()) if err != nil { return err } if len(parts) < 1 { return errors.New("no handler given") } name := parts[0] args := parts[1:] return exec.Command(name, args...).Run() } func stringListMatches(list []string, subject string) bool { if len(list) == 0 { return true } for _, pattern := range list { if glob.Glob(pattern, subject) { return true } } return false } func handleEvent(dockerClient *client.Client, event events.Message, handlers []Handler) { if event.Type != "container" { // only container events are currently supported return } if event.Action == "destroy" { // destroy event is not supported because the container can't be inspected return } container, err := dockerClient.ContainerInspect(context.TODO(), event.Actor.ID) if err != nil { log.Printf("error inspecting container %v: %v", event.Actor.ID, err) return } ev := ContainerEvent{ Action: event.Action, Name: container.Name, ID: event.Actor.ID, Image: container.Config.Image, Labels: container.Config.Labels, Mounts: container.Mounts, Hostname: container.Config.Hostname, IPAddress: container.NetworkSettings.IPAddress, Ports: container.NetworkSettings.Ports, } for _, handler := range handlers { if handler.matches(ev) { err := handler.invoke(ev) if err != nil { log.Printf("error invoking handler: %v", err) } } } } func readConfiguration(path string) (Configuration, error) { var config Configuration cfg, err := ini.LoadSources(ini.LoadOptions{ AllowNonUniqueSections: true, AllowShadows: true, }, path) if err != nil { return config, err } for _, section := range cfg.Sections() { if section.Name() == "Handler" { var handler Handler handler.Label = make(map[string]*string) for _, key := range section.Keys() { val := key.String() switch key.Name() { case "Action": handler.Action = append(handler.Action, val) break case "Name": handler.Name = append(handler.Name, val) break case "Image": handler.Image = append(handler.Image, val) break case "ID": handler.ID = append(handler.ID, val) break case "Hostname": handler.Hostname = append(handler.Hostname, val) break case "Label": parts := strings.SplitN(val, "=", 2) var k string var v *string if len(parts) == 2 { k = parts[0] v = &parts[1] } else { k = parts[0] v = nil } handler.Label[k] = v break case "Run": if handler.Handler != nil { return config, errors.New(fmt.Sprintf("duplicate key %v in section %v", key.Name(), section.Name())) } handler.Handler, err = template.New("Handler").Parse(val) if err != nil { return config, err } break default: return config, errors.New(fmt.Sprintf("unknown key %v in section %v", key.Name(), section.Name())) } } config.handlers = append(config.handlers, handler) } } return config, nil } func main() { configPath := "/etc/docker-event-handler.conf" if len(os.Args) > 1 { if len(os.Args) > 2 { log.Fatal("too many arguments") } configPath = os.Args[1] } config, err := readConfiguration(configPath) if err != nil { log.Fatal("configuration parsing failed", err) } dockerClient, err := client.NewClientWithOpts(client.WithAPIVersionNegotiation()) if err != nil { log.Fatal("docker connection failed", err) } evs, errs := dockerClient.Events(context.TODO(), types.EventsOptions{ Since: strconv.FormatInt(time.Now().Unix(), 10), Filters: filters.NewArgs( filters.Arg("type", events.ContainerEventType), ), }) for { select { case err := <-errs: log.Fatal("event stream failed", err) case event := <-evs: handleEvent(dockerClient, event, config.handlers) } } }