253 lines
5.3 KiB
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)
|
|
}
|
|
}
|
|
|
|
}
|