mirror of
https://github.com/linuxkit/linuxkit.git
synced 2025-07-19 01:06:27 +00:00
168 lines
5.0 KiB
Go
168 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Listen for Docker image, container, and volume delete events and run a command after a delay.
|
|
|
|
// Event represents the subset of the Docker event message that we're
|
|
// interested in
|
|
type Event struct {
|
|
Type string
|
|
Action string
|
|
}
|
|
|
|
// String returns an Event in a human-readable form
|
|
func (e Event) String() string {
|
|
return fmt.Sprintf("Type: %s, Action: %s", e.Type, e.Action)
|
|
}
|
|
|
|
// DelayedAction runs a function in the future at least once after every call
|
|
// to AtLeastOnceMore.
|
|
type DelayedAction struct {
|
|
c chan interface{}
|
|
}
|
|
|
|
// NewDelayedAction creates a delayed action which guarantees to call f
|
|
// at most d after a call to AtLeastOnceMore.
|
|
func NewDelayedAction(d time.Duration, f func()) *DelayedAction {
|
|
c := make(chan interface{})
|
|
go func() {
|
|
for {
|
|
<-c
|
|
time.Sleep(d)
|
|
f()
|
|
}
|
|
}()
|
|
return &DelayedAction{
|
|
c: c,
|
|
}
|
|
}
|
|
|
|
// AtLeastOnceMore guarantees to call f at least once more within the originally
|
|
// specified duration.
|
|
func (a *DelayedAction) AtLeastOnceMore() {
|
|
select {
|
|
case a.c <- nil:
|
|
// Started a fresh countdown
|
|
default:
|
|
// There is already a countdown in progress
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
delay := flag.Duration("delay", time.Second*10, "maximum time to wait after an image delete before triggering")
|
|
sockPath := flag.String("docker-socket", "/var/run/docker.sock", "location of the docker socket to connect to")
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, `%[1]s: run a command after images are deleted by Docker.
|
|
|
|
Example usage:
|
|
%[1]s --docker-socket=/var/run/docker.sock.raw --delay 10s -- /sbin/fstrim /var
|
|
-- connect to the docker API through /var/run/docker.sock.raw, and run the
|
|
command /sbin/fstrim /var at most 10s after images are deleted. This
|
|
allows large batches of image deletions to happen and amortise the cost of
|
|
the TRIM operation.
|
|
|
|
Arguments:
|
|
`, filepath.Base(os.Args[0]))
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
flag.Parse()
|
|
toRun := flag.Args()
|
|
if len(toRun) == 0 {
|
|
log.Fatalf("Please supply a program to run. For usage add -h")
|
|
}
|
|
|
|
log.Printf("I will run %s around %.1f seconds after an image is deleted", strings.Join(toRun, " "), delay.Seconds())
|
|
|
|
action := NewDelayedAction(*delay, func() {
|
|
cmdline := strings.Join(toRun, " ")
|
|
log.Printf("Running %s", cmdline)
|
|
cmd := exec.Command(toRun[0], toRun[1:]...)
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if ee, ok := err.(*exec.ExitError); ok {
|
|
log.Printf("%s failed: %s", cmdline, string(ee.Stderr))
|
|
return
|
|
}
|
|
log.Printf("Unexpected failure while running: %s: %#v", cmdline, err)
|
|
}
|
|
})
|
|
|
|
// Connect to Docker over the Unix domain socket
|
|
httpc := http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
|
return net.Dial("unix", *sockPath)
|
|
},
|
|
},
|
|
}
|
|
|
|
RECONNECT:
|
|
// (Re-)connect forever, reading events
|
|
for {
|
|
res, err := httpc.Get("http://unix/v1.24/events")
|
|
if err != nil {
|
|
log.Printf("Failed to connect to the Docker daemon at %s: will retry in 1s", *sockPath)
|
|
time.Sleep(time.Second)
|
|
continue RECONNECT
|
|
}
|
|
// Check the server identifies as Docker. This will provide an early failure
|
|
// if we're pointed at completely the wrong address.
|
|
server := res.Header.Get("Server")
|
|
if !strings.HasPrefix(server, "Docker") {
|
|
log.Printf("Server identified as %s -- is this really Docker?", server)
|
|
panic(errors.New("Remote server is not Docker"))
|
|
}
|
|
log.Printf("(Re-)connected to the Docker daemon")
|
|
d := json.NewDecoder(res.Body)
|
|
var event Event
|
|
for {
|
|
err = d.Decode(&event)
|
|
if err != nil {
|
|
log.Printf("Failed to read event: will retry in 1s")
|
|
res.Body.Close()
|
|
time.Sleep(time.Second)
|
|
continue RECONNECT
|
|
}
|
|
if event.Action == "delete" && event.Type == "image" {
|
|
log.Printf("An image has been removed: will run the action at least once more")
|
|
action.AtLeastOnceMore()
|
|
} else if event.Action == "destroy" && event.Type == "container" {
|
|
log.Printf("A container has been removed: will run the action at least once more")
|
|
action.AtLeastOnceMore()
|
|
} else if event.Action == "destroy" && event.Type == "volume" {
|
|
log.Printf("A volume has been removed: will run the action at least once more")
|
|
action.AtLeastOnceMore()
|
|
} else if event.Action == "prune" && event.Type == "image" {
|
|
log.Printf("Dangling images have been removed: will run the action at least once more")
|
|
action.AtLeastOnceMore()
|
|
} else if event.Action == "prune" && event.Type == "container" {
|
|
log.Printf("Stopped containers have been removed: will run the action at least once more")
|
|
action.AtLeastOnceMore()
|
|
} else if event.Action == "prune" && event.Type == "volume" {
|
|
log.Printf("Unused volumes have been removed: will run the action at least once more")
|
|
action.AtLeastOnceMore()
|
|
} else if event.Action == "prune" && event.Type == "builder" {
|
|
log.Printf("Dangling build cache has been removed: will run the action at least once more")
|
|
action.AtLeastOnceMore()
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|