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 workspace file
|
||||||
go.work
|
go.work
|
||||||
|
|
||||||
|
|
||||||
|
# idea
|
||||||
|
/.idea
|
|
@ -1,3 +1,4 @@
|
||||||
# docker-event-handler
|
# 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