docker event handler implementation

This commit is contained in:
Gwendolyn 2022-03-22 19:27:39 +01:00
parent 18821efa29
commit c5bea32e01
6 changed files with 1708 additions and 1 deletions

3
.gitignore vendored
View File

@ -21,3 +21,6 @@
# Go workspace file
go.work
# idea
/.idea

View File

@ -1,3 +1,4 @@
# docker-event-handler
listen to docker events and respond to them
This program allows you to listen to docker events and respond to them.
Currently only container events are supported.

7
example.conf Normal file
View File

@ -0,0 +1,7 @@
[Handler]
Action=start
Run=notify-send "docker container event: start, name={{ .Name }}"
[Handler]
Action=die
Run=notify-send "docker container event: die, name={{ .Name }}"

33
go.mod Normal file
View File

@ -0,0 +1,33 @@
module docker-event-handler
go 1.18
require (
github.com/docker/docker v20.10.13+incompatible
github.com/docker/go-connections v0.4.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/ryanuber/go-glob v1.0.0
gopkg.in/ini.v1 v1.66.0
)
require (
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/containerd/containerd v1.6.1 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.45.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gotest.tools/v3 v3.1.0 // indirect
)

1411
go.sum Normal file

File diff suppressed because it is too large Load Diff

252
main.go Normal file
View File

@ -0,0 +1,252 @@
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)
}
}
}