docker event handler implementation
This commit is contained in:
parent
18821efa29
commit
c5bea32e01
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -21,3 +21,6 @@
|
|||
# Go workspace file
|
||||
go.work
|
||||
|
||||
|
||||
# idea
|
||||
/.idea
|
|
@ -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
7
example.conf
Normal 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
33
go.mod
Normal 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
|
||||
)
|
252
main.go
Normal file
252
main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue