diff --git a/blueprints/docker-for-mac.yml b/blueprints/docker-for-mac.yml index c29a19e5a..f9c9fc767 100644 --- a/blueprints/docker-for-mac.yml +++ b/blueprints/docker-for-mac.yml @@ -79,6 +79,9 @@ services: "--swarm-default-advertise-addr=eth0", "--userland-proxy-path", "/usr/bin/vpnkit-expose-port", "--storage-driver", "overlay2" ] + # Monitor for image deletes and invoke a TRIM on the container filesystem + - name: trim-after-delete + image: "linuxkit/trim-after-delete:6cc6131300c287fcd40041a28119fee2fc874539" files: - path: /var/config/docker/daemon.json diff --git a/pkg/trim-after-delete/Dockerfile b/pkg/trim-after-delete/Dockerfile new file mode 100644 index 000000000..3e4e8ab93 --- /dev/null +++ b/pkg/trim-after-delete/Dockerfile @@ -0,0 +1,26 @@ +# We need the `fstrim` binary: +FROM linuxkit/alpine:630ee558e4869672fae230c78364e367b8ea67a9 AS mirror +RUN mkdir -p /out/etc/apk && cp -r /etc/apk/* /out/etc/apk/ +RUN apk add --no-cache --initdb -p /out \ + alpine-baselayout \ + busybox \ + util-linux + +# Remove apk residuals +RUN rm -rf /out/etc/apk /out/lib/apk /out/var/cache + +# We also need the Go binary which calls it: +RUN apk add --no-cache go musl-dev +ENV GOPATH=/go PATH=$PATH:/go/bin + +COPY . /go/src/trim-after-delete +RUN go-compile.sh /go/src/trim-after-delete + +FROM scratch +ENTRYPOINT [] +CMD [] +WORKDIR / +COPY --from=mirror /out/ / +COPY --from=mirror /go/bin/trim-after-delete /usr/bin/trim-after-delete +CMD ["/usr/bin/trim-after-delete", "--", "/sbin/fstrim", "/var/lib/docker"] +LABEL org.mobyproject.config='{"binds": ["/var/run:/var/run", "/var/lib/docker:/var/lib/docker"], "capabilities": ["CAP_SYS_ADMIN"]}' diff --git a/pkg/trim-after-delete/Makefile b/pkg/trim-after-delete/Makefile new file mode 100644 index 000000000..0ff768636 --- /dev/null +++ b/pkg/trim-after-delete/Makefile @@ -0,0 +1,4 @@ +include ../package.mk + +IMAGE=trim-after-delete +DEPS=$(wildcard *.go) diff --git a/pkg/trim-after-delete/README.md b/pkg/trim-after-delete/README.md new file mode 100644 index 000000000..813a93d9f --- /dev/null +++ b/pkg/trim-after-delete/README.md @@ -0,0 +1,3 @@ +### trim-after-delete + +This package runs `fstrim /var/lib/docker` after observing a Docker image delete event. diff --git a/pkg/trim-after-delete/main.go b/pkg/trim-after-delete/main.go new file mode 100644 index 000000000..d4cf49af5 --- /dev/null +++ b/pkg/trim-after-delete/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "log" + "net" + "net/http" + "os" + "os/exec" + "strings" + "time" +) + +// Listen for Docker image 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() { + // after-image-deletes --delay 10s -- /sbin/fstrim /var + + delay := flag.Duration("delay", time.Second*10, "maximum time to wait after an image delete before triggering") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "%s: run a command after images are deleted by Docker.\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Example usage:\n") + fmt.Fprintf(os.Stderr, "%s --delay 10s -- /sbin/fstrim /var\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " -- run the command /sbin/fstrim /var at most 10s after an image is deleted.\n") + fmt.Fprintf(os.Stderr, " This would allow large batches of image deletions to happen and amortise the\n") + fmt.Fprintf(os.Stderr, " cost of the TRIM operation.\n\n") + fmt.Fprintf(os.Stderr, "Arguments:\n") + 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", "/var/run/docker.sock") + }, + }, + } + +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: will retry in 1s") + 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("The delayed action will happen at least once more") + action.AtLeastOnceMore() + } + } + } + +}