docker-event-handler/main.go

253 lines
5.3 KiB
Go

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)
}
}
}