diff --git a/events/container.go b/events/container.go new file mode 100644 index 0000000..8d283e9 --- /dev/null +++ b/events/container.go @@ -0,0 +1,36 @@ +package events + +import ( + "context" + "github.com/docker/docker/api/types" + dockerEvents "github.com/docker/docker/api/types/events" + "github.com/docker/docker/client" + "log" +) + +type Container struct { + Action string + ID string + Container *types.ContainerJSON +} + +//goland:noinspection ALL +func (c Container) __interface_event() { + panic("interface event guard") +} + +func makeContainer(message dockerEvents.Message, client *client.Client) Event { + var e Container + e.ID = message.Actor.ID + e.Action = message.Action + + if message.Action != "destroy" { + container, err := client.ContainerInspect(context.TODO(), message.Actor.ID) + if err != nil { + log.Printf("error inspecting container %v: %v", message.Actor.ID, err) + } else { + e.Container = &container + } + } + return e +} diff --git a/events/event.go b/events/event.go new file mode 100644 index 0000000..c826d42 --- /dev/null +++ b/events/event.go @@ -0,0 +1,24 @@ +package events + +import ( + dockerEvents "github.com/docker/docker/api/types/events" + "github.com/docker/docker/client" + "log" +) + +//goland:noinspection GoSnakeCaseUsage +type Event interface { + __interface_event() +} + +func Make(message dockerEvents.Message, client *client.Client) Event { + switch message.Type { + case "container": + return makeContainer(message, client) + case "network": + return makeNetwork(message, client) + default: + log.Printf("unknown event type %v", message.Type) + return nil + } +} diff --git a/events/network.go b/events/network.go new file mode 100644 index 0000000..475e326 --- /dev/null +++ b/events/network.go @@ -0,0 +1,48 @@ +package events + +import ( + "context" + "github.com/docker/docker/api/types" + dockerEvents "github.com/docker/docker/api/types/events" + "github.com/docker/docker/client" + "log" +) + +type Network struct { + Action string + ID string + Network *types.NetworkResource + Container *types.ContainerJSON +} + +//goland:noinspection ALL +func (c Network) __interface_event() { + panic("interface event guard") +} + +func makeNetwork(message dockerEvents.Message, client *client.Client) Event { + var e Network + e.ID = message.Actor.ID + e.Action = message.Action + + if message.Action != "destroy" { + network, err := client.NetworkInspect(context.TODO(), message.Actor.ID, types.NetworkInspectOptions{}) + if err != nil { + log.Printf("error inspecting network %v: %v", message.Actor.ID, err) + } else { + e.Network = &network + } + } + + if message.Action == "connect" || message.Action == "disconnect" { + if containerId, ok := message.Actor.Attributes["container"]; ok { + container, err := client.ContainerInspect(context.TODO(), containerId) + if err != nil { + log.Printf("error inspecting container %v: %v", containerId, err) + } else { + e.Container = &container + } + } + } + return e +} diff --git a/example.conf b/example.conf index 454bd8e..0ceb68f 100644 --- a/example.conf +++ b/example.conf @@ -1,7 +1,12 @@ -[Handler] -Action=start -Run=notify-send "docker container event: start, name={{ .Name }}" -[Handler] -Action=die -Run=notify-send "docker container event: die, name={{ .Name }}" \ No newline at end of file +[Container] +Action=start +Run=notify-send "{{ .Container.Name }}" + +[Network] +Action=connect +Run=notify-send "connected container {{ .Container.Name }} to network {{ .Network.Name }}" + +[Network] +Action=disconnect +Run=notify-send "disconnected container {{ .Container.Name }} from network {{ .Network.Name }}" diff --git a/handlers/container.go b/handlers/container.go new file mode 100644 index 0000000..d66bb7a --- /dev/null +++ b/handlers/container.go @@ -0,0 +1,112 @@ +package handlers + +import ( + "docker-event-handler/events" + "errors" + "fmt" + "gopkg.in/ini.v1" + "strings" + "text/template" +) + +type Container struct { + Action []string + Name []string + Image []string + ID []string + Hostname []string + Label map[string]*string + Run *template.Template +} + +func (h Container) Matches(event events.Event) bool { + e, ok := event.(events.Container) + if !ok { + return false + } + + if !stringListMatches(h.Action, e.Action) { + return false + } + + if !stringListMatches(h.ID, e.ID) { + return false + } + + if e.Container == nil { + if len(h.Name) > 0 || len(h.Image) > 0 || len(h.Hostname) > 0 || len(h.Label) > 0 { + return false + } + } else { + if !stringListMatches(h.Name, e.Container.Name) { + return false + } + if !stringListMatches(h.Image, e.Container.Image) { + return false + } + if !stringListMatches(h.Hostname, e.Container.Config.Hostname) { + return false + } + if !labelsMatch(h.Label, e.Container.Config.Labels) { + return false + } + } + + return true +} + +func (h Container) Invoke(event events.Event) error { + e := event.(events.Container) // enforce that the event is a container events + return runTemplatedScript(h.Run, e) +} + +func readContainerFromConfig(section *ini.Section) (Container, error) { + var handler Container + var err error + 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.Run != nil { + return handler, errors.New(fmt.Sprintf("duplicate key %v in section %v", key.Name(), section.Name())) + } + handler.Run, err = template.New("Run").Parse(val) + if err != nil { + return handler, err + } + break + default: + return handler, errors.New(fmt.Sprintf("unknown key %v in section %v", key.Name(), section.Name())) + } + } + return handler, nil +} diff --git a/handlers/handler.go b/handlers/handler.go new file mode 100644 index 0000000..28691c2 --- /dev/null +++ b/handlers/handler.go @@ -0,0 +1,76 @@ +package handlers + +import ( + "bytes" + "docker-event-handler/events" + "errors" + "fmt" + "github.com/google/shlex" + "github.com/ryanuber/go-glob" + "gopkg.in/ini.v1" + "os/exec" + "text/template" +) + +func ReadFromConfig(section *ini.Section) (Handler, error) { + switch section.Name() { + case "Container": + return readContainerFromConfig(section) + case "Network": + return readNetworkFromConfig(section) + default: + return nil, errors.New(fmt.Sprintf("unknown section %v", section.Name())) + } +} + +type Handler interface { + Matches(event events.Event) bool + Invoke(event events.Event) error +} + +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 labelsMatch(want map[string]*string, has map[string]string) bool { + for key, val := range want { + actualVal, exists := has[key] + if !exists { + return false + } + if val != nil { + if actualVal != *val { + return false + } + } + } + return true +} + +func runTemplatedScript(template *template.Template, data interface{}) error { + var cmdBuf bytes.Buffer + err := template.Execute(&cmdBuf, data) + 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() +} diff --git a/handlers/network.go b/handlers/network.go new file mode 100644 index 0000000..359121d --- /dev/null +++ b/handlers/network.go @@ -0,0 +1,159 @@ +package handlers + +import ( + "docker-event-handler/events" + "errors" + "fmt" + "gopkg.in/ini.v1" + "strings" + "text/template" +) + +type Network struct { + Action []string + Name []string + Driver []string + ID []string + Label map[string]*string + ContainerName []string + ContainerImage []string + ContainerID []string + ContainerHostname []string + ContainerLabel map[string]*string + Run *template.Template +} + +func (h Network) Matches(event events.Event) bool { + e, ok := event.(events.Network) + if !ok { + return false + } + + if !stringListMatches(h.Action, e.Action) { + return false + } + + if !stringListMatches(h.ID, e.ID) { + return false + } + + if e.Network == nil { + if len(h.Name) > 0 || len(h.Driver) > 0 || len(h.Label) > 0 { + return false + } + } else { + if !stringListMatches(h.Name, e.Network.Name) { + return false + } + if !stringListMatches(h.Driver, e.Network.Driver) { + return false + } + if !labelsMatch(h.Label, e.Network.Labels) { + return false + } + } + + if e.Container == nil { + if len(h.ContainerID) > 0 || len(h.ContainerName) > 0 || len(h.ContainerImage) > 0 || len(h.ContainerHostname) > 0 || len(h.ContainerLabel) > 0 { + return false + } + } else { + if !stringListMatches(h.ContainerID, e.Container.ID) { + return false + } + if !stringListMatches(h.ContainerName, e.Container.Name) { + return false + } + if !stringListMatches(h.ContainerImage, e.Container.Image) { + return false + } + if !stringListMatches(h.ContainerHostname, e.Container.Config.Hostname) { + return false + } + if !labelsMatch(h.ContainerLabel, e.Container.Config.Labels) { + return false + } + } + + return true +} + +func (h Network) Invoke(event events.Event) error { + e := event.(events.Network) // enforce that the event is a network event + return runTemplatedScript(h.Run, e) +} + +func readNetworkFromConfig(section *ini.Section) (Network, error) { + var handler Network + var err error + handler.Label = make(map[string]*string) + handler.ContainerLabel = 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 "ID": + handler.ID = append(handler.ID, val) + break + case "Driver": + handler.Driver = append(handler.Driver, 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 "ContainerName": + handler.ContainerName = append(handler.ContainerName, val) + break + case "ContainerID": + handler.ContainerID = append(handler.ContainerID, val) + break + case "ContainerImage": + handler.ContainerImage = append(handler.ContainerImage, val) + break + case "ContainerHostname": + handler.ContainerHostname = append(handler.ContainerHostname, val) + break + case "ContainerLabel": + 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.ContainerLabel[k] = v + break + case "Run": + if handler.Run != nil { + return handler, errors.New(fmt.Sprintf("duplicate key %v in section %v", key.Name(), section.Name())) + } + handler.Run, err = template.New("Run").Parse(val) + if err != nil { + return handler, err + } + break + default: + return handler, errors.New(fmt.Sprintf("unknown key %v in section %v", key.Name(), section.Name())) + } + } + return handler, nil +} diff --git a/main.go b/main.go index fb20092..579820d 100644 --- a/main.go +++ b/main.go @@ -1,147 +1,37 @@ package main import ( - "bytes" "context" - "errors" - "fmt" + "docker-event-handler/events" + "docker-event-handler/handlers" "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" + dockerEvents "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 + handlers []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 -} +func handleEvent(dockerClient *client.Client, event dockerEvents.Message, handlers []handlers.Handler) { + e := events.Make(event, dockerClient) -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 + if e == nil { 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, - } + log.Printf("%T%+v", e, e) for _, handler := range handlers { - if handler.matches(ev) { - err := handler.invoke(ev) + if handler.Matches(e) { + err := handler.Invoke(e) if err != nil { log.Printf("error invoking handler: %v", err) } @@ -160,55 +50,14 @@ func readConfiguration(path string) (Configuration, error) { } 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) + if section.Name() == "DEFAULT" { + continue } + handler, err := handlers.ReadFromConfig(section) + if err != nil { + return config, err + } + config.handlers = append(config.handlers, handler) } return config, nil } @@ -236,7 +85,8 @@ func main() { evs, errs := dockerClient.Events(context.TODO(), types.EventsOptions{ Since: strconv.FormatInt(time.Now().Unix(), 10), Filters: filters.NewArgs( - filters.Arg("type", events.ContainerEventType), + filters.Arg("type", dockerEvents.ContainerEventType), + filters.Arg("type", dockerEvents.NetworkEventType), ), })