Compare commits

...

15 Commits

Author SHA1 Message Date
IamTheFij 0aafe9a389 Skip docker-compose hook
continuous-integration/drone/push Build is passing Details
2021-05-13 14:07:07 -07:00
IamTheFij 98cbd29dc9 Switch to pre-commit image
continuous-integration/drone/push Build is failing Details
2021-05-13 13:58:21 -07:00
IamTheFij 4e8a9cf929 Update pre-commit hooks
continuous-integration/drone/push Build is failing Details
2021-05-13 13:52:41 -07:00
IamTheFij 15625a05fb Add additional linting
continuous-integration/drone/push Build is failing Details
2021-04-30 11:24:14 -07:00
IamTheFij f651d55786 Switch to external slog package and update dependencies 2021-04-29 09:35:36 -07:00
IamTheFij 5698f9bccd Fix tag version propogation
continuous-integration/drone/push Build is passing Details
2020-12-01 19:42:03 -08:00
IamTheFij 2a03ef21ad Further parameterize Makefile to make it easier for me to reuse
continuous-integration/drone/push Build is passing Details
2020-12-01 17:45:16 -08:00
IamTheFij a60ff562c7 Merge branch 'add-exec-schedules' into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-08-19 14:20:21 -07:00
IamTheFij 82dc4b82e7 Add test instructions 2020-08-19 14:20:12 -07:00
IamTheFij a823590368 Update readme with exec instructions 2020-08-19 14:19:26 -07:00
IamTheFij eb7cdb0d1f Update pipeline notify 2020-08-19 14:13:29 -07:00
IamTheFij 2808b07b09 Make sure version is set when building final binaries 2020-08-19 14:13:29 -07:00
IamTheFij a5f9b0866f Add tests to drone 2020-08-19 14:13:29 -07:00
IamTheFij e83d5b6784 Add integration test 2020-08-19 14:13:28 -07:00
IamTheFij 9913442526 Add the ability to create execute jobs on long running containers 2020-08-19 14:13:28 -07:00
13 changed files with 1204 additions and 270 deletions

View File

@ -1,11 +1,25 @@
---
kind: pipeline
name: test
steps:
- name: build
image: golang:1.12
- name: test
image: golang:1.15
commands:
- make build
- make test
- name: check
image: iamthefij/drone-pre-commit:personal
environment:
SKIP: docker-compose-check
# - name: itest
# image: docker/compose:alpine-1.26.2
# environment:
# VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
# commands:
# - apk add make bash
# - make itest
---
kind: pipeline
@ -24,7 +38,9 @@ trigger:
steps:
- name: build
image: golang:1.12
image: golang:1.15
environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
commands:
- make build-linux-static
@ -77,3 +93,28 @@ steps:
from_secret: docker_username
password:
from_secret: docker_password
---
kind: pipeline
name: notify
depends_on:
- test
- publish
trigger:
status:
- failure
steps:
- name: notify
image: drillster/drone-email
settings:
host:
from_secret: SMTP_HOST # pragma: whitelist secret
username:
from_secret: SMTP_USER # pragma: whitelist secret
password:
from_secret: SMTP_PASS # pragma: whitelist secret
from: drone@iamthefij.com

3
.gitignore vendored
View File

@ -30,3 +30,6 @@ dockron
dockron-*
# deps
vendor/
# Test output
itest/*_result.txt

View File

@ -1,21 +1,23 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
rev: v3.4.0
hooks:
- id: check-added-large-files
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- repo: git://github.com/dnephin/pre-commit-golang
rev: v0.3.5
rev: v0.4.0
hooks:
- id: go-fmt
- id: go-imports
# - id: gometalinter
# - id: golangci-lint
- id: golangci-lint
- repo: https://github.com/IamTheFij/docker-pre-commit
rev: v2.0.0
hooks:
- id: docker-compose-check
- repo: https://github.com/hadolint/hadolint
rev: v2.4.0
hooks:
- id: hadolint

View File

@ -1,5 +1,5 @@
ARG REPO=library
FROM golang:1.12-alpine AS builder
FROM golang:1.15-alpine AS builder
# hadolint ignore=DL3018
RUN apk add --no-cache git

View File

@ -1,14 +1,17 @@
.PHONY: test all
DOCKER_TAG ?= dockron-dev-${USER}
OUTPUT ?= dockron
DOCKER_TAG ?= $(OUTPUT)-dev-$(USER)
GIT_TAG_NAME := $(shell git tag -l --contains HEAD)
GIT_SHA := $(shell git rev-parse HEAD)
VERSION := $(if $(GIT_TAG_NAME),$(GIT_TAG_NAME),$(GIT_SHA))
VERSION ?= $(if $(GIT_TAG_NAME),$(GIT_TAG_NAME),$(GIT_SHA))
GOFILES = *.go go.mod go.sum
.PHONY: default
default: build
.PHONY: all
all: check test itest
# Downloads dependencies into vendor directory
vendor: $(GOFILES)
go mod vendor
@ -22,9 +25,13 @@ run:
test:
go test -coverprofile=coverage.out
go tool cover -func=coverage.out
# @go tool cover -func=coverage.out | awk -v target=80.0% \
@go tool cover -func=coverage.out | awk -v target=75.0% \
'/^total:/ { print "Total coverage: " $$3 " Minimum coverage: " target; if ($$3+0.0 >= target+0.0) print "ok"; else { print "fail"; exit 1; } }'
.PHONY: itest
itest:
./itest/itest.sh
# Installs pre-commit hooks
.PHONY: install-hooks
install-hooks:
@ -36,45 +43,45 @@ check:
pre-commit run --all-files
# Output target
dockron: $(GOFILES)
$(OUTPUT): $(GOFILES)
@echo Version: $(VERSION)
go build -ldflags '-X "main.version=${VERSION}"' -o dockron
go build -ldflags '-X "main.version=$(VERSION)"' -o $(OUTPUT)
# Alias for building
.PHONY: build
build: dockron
build: $(OUTPUT)
dockron-darwin-amd64: $(GOFILES)
$(OUTPUT)-darwin-amd64: $(GOFILES)
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 \
go build -ldflags '-X "main.version=${VERSION}"' -a -installsuffix nocgo \
-o dockron-darwin-amd64
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
-o $(OUTPUT)-darwin-amd64
dockron-linux-amd64: $(GOFILES)
$(OUTPUT)-linux-amd64: $(GOFILES)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
go build -ldflags '-X "main.version=${VERSION}"' -a -installsuffix nocgo \
-o dockron-linux-amd64
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
-o $(OUTPUT)-linux-amd64
dockron-linux-arm: $(GOFILES)
$(OUTPUT)-linux-arm: $(GOFILES)
GOOS=linux GOARCH=arm CGO_ENABLED=0 \
go build -ldflags '-X "main.version=${VERSION}"' -a -installsuffix nocgo \
-o dockron-linux-arm
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
-o $(OUTPUT)-linux-arm
dockron-linux-arm64: $(GOFILES)
$(OUTPUT)-linux-arm64: $(GOFILES)
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
go build -ldflags '-X "main.version=${VERSION}"' -a -installsuffix nocgo \
-o dockron-linux-arm64
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
-o $(OUTPUT)-linux-arm64
.PHONY: build-linux-static
build-linux-static: dockron-linux-amd64 dockron-linux-arm dockron-linux-arm64
build-linux-static: $(OUTPUT)-linux-amd64 $(OUTPUT)-linux-arm $(OUTPUT)-linux-arm64
.PHONY: build-all-static
build-all-static: dockron-darwin-amd64 build-linux-static
build-all-static: $(OUTPUT)-darwin-amd64 build-linux-static
# Cleans all build artifacts
.PHONY: clean
clean:
rm -f dockron
rm -f dockron-linux-*
rm -f $(OUTPUT)
rm -f $(OUTPUT)-linux-*
# Cleans vendor directory
.PHONY: clean-vendor
@ -82,17 +89,17 @@ clean-vendor:
rm -fr ./vendor
.PHONY: docker-build
docker-build: dockron-linux-amd64
docker build . -t ${DOCKER_TAG}-linux-amd64
docker-build: $(OUTPUT)-linux-amd64
docker build . -t $(DOCKER_TAG)-linux-amd64
# Cross build for arm architechtures
.PHONY: docker-build-arm
docker-build-arm: dockron-linux-arm
docker build --build-arg REPO=arm32v7 --build-arg ARCH=arm . -t ${DOCKER_TAG}-linux-arm
docker-build-arm: $(OUTPUT)-linux-arm
docker build --build-arg REPO=arm32v7 --build-arg ARCH=arm . -t $(DOCKER_TAG)-linux-arm
.PHONY: docker-build-arm
docker-build-arm64: dockron-linux-arm64
docker build --build-arg REPO=arm64v8 --build-arg ARCH=arm64 . -t ${DOCKER_TAG}-linux-arm64
docker-build-arm64: $(OUTPUT)-linux-arm64
docker build --build-arg REPO=arm64v8 --build-arg ARCH=arm64 . -t $(DOCKER_TAG)-linux-arm64
.PHONY: docker-run
docker-run: docker-build
@ -101,30 +108,30 @@ docker-run: docker-build
# Cross run on host architechture
.PHONY: docker-run-arm
docker-run-arm: docker-build-arm
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --name $(DOCKER_TAG)-run ${DOCKER_TAG}-linux-arm
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --name $(DOCKER_TAG)-run $(DOCKER_TAG)-linux-arm
.PHONY: docker-run-arm64
docker-run-arm64: docker-build-arm64
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --name $(DOCKER_TAG)-run ${DOCKER_TAG}-linux-arm64
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --name $(DOCKER_TAG)-run $(DOCKER_TAG)-linux-arm64
# Multi stage builds
.PHONY: docker-staged-build
docker-staged-build:
docker build --build-arg VERSION=${VERSION} \
-t ${DOCKER_TAG}-linux-amd64 \
docker build --build-arg VERSION=$(VERSION) \
-t $(DOCKER_TAG)-linux-amd64 \
-f Dockerfile.multi-stage .
# Cross build for arm architechtures
.PHONY: docker-staged-build-arm
docker-staged-build-arm:
docker build --build-arg VERSION=${VERSION} \
--build-arg REPO=arm32v7 --build-arg ARCH=arm -t ${DOCKER_TAG}-linux-arm \
docker build --build-arg VERSION=$(VERSION) \
--build-arg REPO=arm32v7 --build-arg ARCH=arm -t $(DOCKER_TAG)-linux-arm \
-f Dockerfile.multi-stage .
.PHONY: docker-staged-build-arm
docker-staged-build-arm64:
docker build --build-arg VERSION=${VERSION} \
--build-arg REPO=arm64v8 --build-arg ARCH=arm64 -t ${DOCKER_TAG}-linux-arm64 \
docker build --build-arg VERSION=$(VERSION) \
--build-arg REPO=arm64v8 --build-arg ARCH=arm64 -t $(DOCKER_TAG)-linux-arm64 \
-f Dockerfile.multi-stage .
.PHONY: docker-example

View File

@ -32,6 +32,16 @@ Create your container and add a label in the form `'dockron.schedule=* * * * *'`
Dockron will now start that container peridically on the schedule.
If you have a long running container that you'd like to schedule an exec command inside of, you can do so with labels as well. Add your job in the form `dockron.<job>.schedule=* * * * *` and `dockeron.<job>.command=echo hello`. Both labels are required to create an exec job.
Eg.
labels:
- "dockron.dates.schedule=* * * * *"
- "dockron.dates.command=date"
_Note: Exec jobs will not log their output anywhere. Not to the host container or to Dockron. It's up to you to deal with this for now. There is also currently no way to health check these._
### Cron Expression Formatting
For more information on the cron expression parsing, see the docs for [robfig/cron](https://godoc.org/github.com/robfig/cron).
@ -55,3 +65,7 @@ Either use a separate tool in conjunction with Dockron, or use a more robust sch
If you have go on your machine, you can simply use `make build` or `make run` to build and test Dockron. If you don't have go but you do have Docker, you can still build docker images using the provide multi-stage Dockerfile! You can kick that off with `make docker-staged-build`
There is also an example `docker-compose.yml` that will use the multi-stage build to ensure an easy sample. This can be run with `make docker-example`.
## Tests
There are now some basic tests as well as linting and integration tests. You can run all of these by executing `make all`.

View File

@ -10,9 +10,17 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
echoer:
start_echoer:
image: busybox:latest
command: ["date"]
labels:
# Execute every minute
- 'dockron.schedule=* * * * *'
exec_echoer:
image: busybox:latest
command: sh -c "date > /out && tail -f /out"
labels:
# Execute every minute
- 'dockron.date.schedule=* * * * *'
- 'dockron.date.command=date >> /out'

31
go.mod
View File

@ -1,26 +1,27 @@
module github.com/iamthefij/dockron
go 1.12
go 1.15
require (
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/containerd/containerd v1.3.7 // indirect
git.iamthefij.com/iamthefij/slog v1.3.0
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/containerd/containerd v1.4.4 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
github.com/docker/docker v20.10.6+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/protobuf v1.4.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.6.0 // indirect
github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50 // indirect
github.com/stretchr/testify v1.5.1 // indirect
golang.org/x/net v0.0.0-20200707034311-ab3426394381
golang.org/x/sys v0.0.0-20200806125547-5acd03effb82 // indirect
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 // indirect
google.golang.org/grpc v1.31.0 // indirect
google.golang.org/protobuf v1.25.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
google.golang.org/genproto v0.0.0-20210427215850-f767ed18ee4d // indirect
google.golang.org/grpc v1.37.0 // indirect
gotest.tools/v3 v3.0.3 // indirect
)

144
go.sum
View File

@ -1,129 +1,168 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.iamthefij.com/iamthefij/slog v1.3.0 h1:4Hu5PQvDrW5e3FrTS3q2iIXW0iPvhNY/9qJsqDR3K3I=
git.iamthefij.com/iamthefij/slog v1.3.0/go.mod h1:1RUj4hcCompZkAxXCRfUX786tb3cM/Zpkn97dGfUfbg=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.9 h1:3RbgqgGVqmcpbOiwrjbVtDHLlJBGF6aE+yHmNtBNsFQ=
github.com/Microsoft/go-winio v0.4.9/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/containerd/containerd v1.3.7 h1:eFSOChY8TTcxvkzp8g+Ov1RL0MYww7XEeK0y+zqGpVc=
github.com/containerd/containerd v1.3.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/containerd/containerd v1.4.4 h1:rtRG4N6Ct7GNssATwgpvMGfnjnwfjnu/Zs9W3Ikzq+M=
github.com/containerd/containerd v1.4.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.6.2+incompatible h1:4FI6af79dfCS/CYb+RRtkSHw3q1L/bnDjG1PcPZtQhM=
github.com/docker/distribution v2.6.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.6+incompatible h1:oXI3Vas8TI8Eu/EjH4srKHJBVqraSzJybhxY7Om9faQ=
github.com/docker/docker v20.10.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50/go.mod h1:1pdIZTAHUz+HDKDVZ++5xg/duPlhKAIzw9qy42CWYp4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200806125547-5acd03effb82 h1:6cBnXxYO+CiRVrChvCosSv7magqTPbyAgz1M8iOv5wM=
golang.org/x/sys v0.0.0-20200806125547-5acd03effb82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 h1:LCO0fg4kb6WwkXQXRQQgUYsFeFb5taTX5WAx5O/Vt28=
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210427215850-f767ed18ee4d h1:XkK62R+tLAM4pyhJdBEvtb5B/hM1uTxsVIOjJm6wras=
google.golang.org/genproto v0.0.0-20210427215850-f767ed18ee4d/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0 h1:uSZWeQJX5j11bIQ4AJoj+McDBo29cY1MCoC1wO3ts+c=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -132,11 +171,16 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

30
itest/docker-compose.yml Normal file
View File

@ -0,0 +1,30 @@
---
version: '3'
services:
dockron:
build:
context: ../
dockerfile: ./Dockerfile.multi-stage
command: ["-watch", "10s", "-debug"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
start_echoer:
image: busybox:latest
command: sh -c "echo ok | tee -a /result.txt"
volumes:
- "./start_result.txt:/result.txt"
labels:
# Execute every minute
- 'dockron.schedule=* * * * *'
exec_echoer:
image: busybox:latest
command: sh -c "tail -f /result.txt"
volumes:
- "./exec_result.txt:/result.txt"
labels:
# Execute every minute
- 'dockron.test.schedule=* * * * *'
- 'dockron.test.command=echo ok >> /result.txt'

36
itest/itest.sh Executable file
View File

@ -0,0 +1,36 @@
#! /bin/bash
set -e
# Change to itest dir
cd "$(dirname "$0")"
function check_results() {
local f=$1
local min=$2
awk "/ok/ { count=count+1 } END { print \"$f: Run count\", count; if (count < $min) { print \"Expected > $min\"; exit 1 } }" "$f"
}
function main() {
# Clear and create result files
echo "start" > ./start_result.txt
echo "start" > ./exec_result.txt
# Clean old containers
docker-compose down || true
# Start containers
echo "Starting containers"
docker-compose up -d --build
echo "Containers started. Sleeping for 70s to let schedules run"
# Schedules run on the shortest interval of a minute. This should allow time
# for the containers to start and execute once
sleep 70
echo "Stopping containers"
docker-compose stop
# Validate result shows minimum amount of executions
check_results ./start_result.txt 2
check_results ./exec_result.txt 1
}
main

267
main.go
View File

@ -3,11 +3,12 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"regexp"
"strings"
"time"
"git.iamthefij.com/iamthefij/slog"
dockerTypes "github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/robfig/cron/v3"
@ -20,118 +21,282 @@ var (
// schedLabel is the string label to search for cron expressions
schedLabel = "dockron.schedule"
// execLabelRegex is will capture labels for an exec job
execLabelRegexp = regexp.MustCompile(`dockron\.([a-zA-Z0-9_-]+)\.(schedule|command)`)
// version of dockron being run
version = "dev"
// Debug can be used to show or supress certain log messages
Debug = true
)
// ContainerClient provides an interface for interracting with Docker
type ContainerClient interface {
ContainerStart(context context.Context, containerID string, options dockerTypes.ContainerStartOptions) error
ContainerExecCreate(ctx context.Context, container string, config dockerTypes.ExecConfig) (dockerTypes.IDResponse, error)
ContainerExecInspect(ctx context.Context, execID string) (dockerTypes.ContainerExecInspect, error)
ContainerExecStart(ctx context.Context, execID string, config dockerTypes.ExecStartCheck) error
ContainerInspect(ctx context.Context, containerID string) (dockerTypes.ContainerJSON, error)
ContainerList(context context.Context, options dockerTypes.ContainerListOptions) ([]dockerTypes.Container, error)
ContainerStart(context context.Context, containerID string, options dockerTypes.ContainerStartOptions) error
}
// ContainerCronJob is an interface of a job to run on containers
type ContainerCronJob interface {
Run()
Name() string
UniqueName() string
Schedule() string
}
// ContainerStartJob represents a scheduled container task
// It contains a reference to a client, the schedule to run on, and the
// ID of that container that should be started
type ContainerStartJob struct {
Client ContainerClient
ContainerID string
Context context.Context
Name string
Schedule string
client ContainerClient
context context.Context
name string
containerID string
schedule string
}
// Run is executed based on the ContainerStartJob Schedule and starts the
// container
func (job ContainerStartJob) Run() {
log.Println("Starting:", job.Name)
err := job.Client.ContainerStart(
job.Context,
job.ContainerID,
slog.Infof("Starting: %s", job.name)
// Check if container is already running
containerJSON, err := job.client.ContainerInspect(
job.context,
job.containerID,
)
slog.OnErrPanicf(err, "Could not get container details for job %s", job.name)
if containerJSON.State.Running {
slog.Warningf("Container is already running. Skipping %s", job.name)
return
}
// Start job
err = job.client.ContainerStart(
job.context,
job.containerID,
dockerTypes.ContainerStartOptions{},
)
if err != nil {
panic(err)
slog.OnErrPanicf(err, "Could not start container for job %s", job.name)
// Check results of job
for check := true; check; check = containerJSON.State.Running {
slog.Debugf("Still running %s", job.name)
containerJSON, err = job.client.ContainerInspect(
job.context,
job.containerID,
)
slog.OnErrPanicf(err, "Could not get container details for job %s", job.name)
time.Sleep(1 * time.Second)
}
slog.Debugf("Done execing %s. %+v", job.name, containerJSON.State)
// Log exit code if failed
if containerJSON.State.ExitCode != 0 {
slog.Errorf(
"Exec job %s existed with code %d",
job.name,
containerJSON.State.ExitCode,
)
}
}
// Name returns the name of the job
func (job ContainerStartJob) Name() string {
return job.name
}
// Schedule returns the schedule of the job
func (job ContainerStartJob) Schedule() string {
return job.schedule
}
// UniqueName returns a unique identifier for a container start job
func (job ContainerStartJob) UniqueName() string {
// ContainerID should be unique as a change in label will result in
// a new container as they are immutable
return job.ContainerID
return job.name + "/" + job.containerID
}
// ContainerExecJob is a scheduled job to be executed in a running container
type ContainerExecJob struct {
ContainerStartJob
shellCommand string
}
// Run is executed based on the ContainerStartJob Schedule and starts the
// container
func (job ContainerExecJob) Run() {
slog.Infof("Execing: %s", job.name)
containerJSON, err := job.client.ContainerInspect(
job.context,
job.containerID,
)
slog.OnErrPanicf(err, "Could not get container details for job %s", job.name)
if !containerJSON.State.Running {
slog.Warningf("Container not running. Skipping %s", job.name)
return
}
execID, err := job.client.ContainerExecCreate(
job.context,
job.containerID,
dockerTypes.ExecConfig{
Cmd: []string{"sh", "-c", strings.TrimSpace(job.shellCommand)},
},
)
slog.OnErrPanicf(err, "Could not create container exec job for %s", job.name)
err = job.client.ContainerExecStart(
job.context,
execID.ID,
dockerTypes.ExecStartCheck{},
)
slog.OnErrPanicf(err, "Could not start container exec job for %s", job.name)
// Wait for job results
execInfo := dockerTypes.ContainerExecInspect{Running: true}
for execInfo.Running {
slog.Debugf("Still execing %s", job.name)
execInfo, err = job.client.ContainerExecInspect(
job.context,
execID.ID,
)
slog.Debugf("Exec info: %+v", execInfo)
if err != nil {
// Nothing we can do if we got an error here, so let's go
slog.OnErrWarnf(err, "Could not get status for exec job %s", job.name)
return
}
time.Sleep(1 * time.Second)
}
slog.Debugf("Done execing %s. %+v", job.name, execInfo)
// Log exit code if failed
if execInfo.ExitCode != 0 {
slog.Errorf("Exec job %s existed with code %d", job.name, execInfo.ExitCode)
}
}
// QueryScheduledJobs queries Docker for all containers with a schedule and
// returns a list of ContainerStartJob records to be scheduled
func QueryScheduledJobs(client ContainerClient) (jobs []ContainerStartJob) {
if Debug {
log.Println("Scanning containers for new schedules...")
}
// returns a list of ContainerCronJob records to be scheduled
func QueryScheduledJobs(client ContainerClient) (jobs []ContainerCronJob) {
slog.Debugf("Scanning containers for new schedules...")
containers, err := client.ContainerList(
context.Background(),
dockerTypes.ContainerListOptions{All: true},
)
if err != nil {
panic(err)
}
slog.OnErrPanicf(err, "Failure querying docker containers")
for _, container := range containers {
// Add start job
if val, ok := container.Labels[schedLabel]; ok {
jobName := strings.Join(container.Names, "/")
jobs = append(jobs, ContainerStartJob{
Schedule: val,
Client: client,
ContainerID: container.ID,
Context: context.Background(),
Name: jobName,
client: client,
containerID: container.ID,
context: context.Background(),
schedule: val,
name: jobName,
})
}
// Add exec jobs
execJobs := map[string]map[string]string{}
for label, value := range container.Labels {
results := execLabelRegexp.FindStringSubmatch(label)
expectedLabelParts := 3
if len(results) == expectedLabelParts {
// We've got part of a new job
jobName, jobField := results[1], results[2]
if partJob, ok := execJobs[jobName]; ok {
// Partial exists, add the other value
partJob[jobField] = value
} else {
// No partial exists, add this part
execJobs[jobName] = map[string]string{
jobField: value,
}
}
}
}
for jobName, jobConfig := range execJobs {
schedule, ok := jobConfig["schedule"]
if !ok {
continue
}
shellCommand, ok := jobConfig["command"]
if !ok {
continue
}
jobs = append(jobs, ContainerExecJob{
ContainerStartJob: ContainerStartJob{
client: client,
containerID: container.ID,
context: context.Background(),
schedule: schedule,
name: strings.Join(append(container.Names, jobName), "/"),
},
shellCommand: shellCommand,
})
}
}
return
return jobs
}
// ScheduleJobs accepts a Cron instance and a list of jobs to schedule.
// It then schedules the provided jobs
func ScheduleJobs(c *cron.Cron, jobs []ContainerStartJob) {
func ScheduleJobs(c *cron.Cron, jobs []ContainerCronJob) {
// Fetch existing jobs from the cron
existingJobs := map[string]cron.EntryID{}
for _, entry := range c.Entries() {
existingJobs[entry.Job.(ContainerStartJob).UniqueName()] = entry.ID
// This should be safe since ContainerCronJob is the only type of job we use
existingJobs[entry.Job.(ContainerCronJob).UniqueName()] = entry.ID
}
for _, job := range jobs {
if _, ok := existingJobs[job.UniqueName()]; ok {
// Job already exists, remove it from existing jobs so we don't
// unschedule it later
if Debug {
log.Printf("Job %s is already scheduled. Skipping", job.Name)
}
slog.Debugf("Job %s is already scheduled. Skipping", job.Name())
delete(existingJobs, job.UniqueName())
continue
}
// Job doesn't exist yet, schedule it
_, err := c.AddJob(job.Schedule, job)
_, err := c.AddJob(job.Schedule(), job)
if err == nil {
log.Printf(
slog.Infof(
"Scheduled %s (%s) with schedule '%s'\n",
job.Name,
job.ContainerID[:10],
job.Schedule,
job.Name(),
job.UniqueName(),
job.Schedule(),
)
} else {
// TODO: Track something for a healthcheck here
log.Printf(
"Error scheduling %s (%s) with schedule '%s'. %v\n",
job.Name,
job.ContainerID[:10],
job.Schedule,
slog.Errorf(
"Could not schedule %s (%s) with schedule '%s'. %v\n",
job.Name(),
job.UniqueName(),
job.Schedule(),
err,
)
}
@ -146,15 +311,15 @@ func ScheduleJobs(c *cron.Cron, jobs []ContainerStartJob) {
func main() {
// Get a Docker Client
client, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv)
if err != nil {
panic(err)
}
slog.OnErrPanicf(err, "Could not create Docker client")
// Read interval for polling Docker
var watchInterval time.Duration
showVersion := flag.Bool("version", false, "Display the version of dockron and exit")
flag.DurationVar(&watchInterval, "watch", defaultWatchInterval, "Interval used to poll Docker for changes")
var showVersion = flag.Bool("version", false, "Display the version of dockron and exit")
flag.BoolVar(&Debug, "debug", false, "Show debug logs")
flag.BoolVar(&slog.DebugLevel, "debug", false, "Show debug logs")
flag.Parse()
// Print version if asked

View File

@ -1,8 +1,11 @@
package main
import (
"errors"
"fmt"
"log"
"reflect"
"sort"
"testing"
dockerTypes "github.com/docker/docker/api/types"
@ -10,28 +13,141 @@ import (
"golang.org/x/net/context"
)
var (
// ContainerJSON results for a running container
runningContainerInfo = dockerTypes.ContainerJSON{
ContainerJSONBase: &dockerTypes.ContainerJSONBase{
State: &dockerTypes.ContainerState{
Running: true,
},
},
}
// ContainerJSON results for a stopped container
stoppedContainerInfo = dockerTypes.ContainerJSON{
ContainerJSONBase: &dockerTypes.ContainerJSONBase{
State: &dockerTypes.ContainerState{
Running: false,
},
},
}
errGeneric = errors.New("error")
)
// FakeCall represents a faked method call
type FakeCall []interface{}
// FakeResult gives results of a fake method
type FakeResult []interface{}
// FakeDockerClient is used to test without interracting with Docker
type FakeDockerClient struct {
FakeContainers []dockerTypes.Container
FakeResults map[string][]FakeResult
FakeCalls map[string][]FakeCall
}
// ContainerStart pretends to start a container
func (fakeClient *FakeDockerClient) ContainerStart(context context.Context, containerID string, options dockerTypes.ContainerStartOptions) error {
return nil
// AssertFakeCalls checks expected against actual calls to fake methods
func (fakeClient FakeDockerClient) AssertFakeCalls(t *testing.T, expectedCalls map[string][]FakeCall, message string) {
if !reflect.DeepEqual(fakeClient.FakeCalls, expectedCalls) {
t.Errorf(
"%s: Expected and actual calls do not match. Expected %+v Actual %+v",
message,
expectedCalls,
fakeClient.FakeCalls,
)
}
}
func (fakeClient *FakeDockerClient) ContainerList(context context.Context, options dockerTypes.ContainerListOptions) ([]dockerTypes.Container, error) {
return fakeClient.FakeContainers, nil
// called is a helper method to get return values and log the method call
func (fakeClient *FakeDockerClient) called(method string, v ...interface{}) FakeResult {
if fakeClient.FakeCalls == nil {
fakeClient.FakeCalls = map[string][]FakeCall{}
}
// Log method call
fakeClient.FakeCalls[method] = append(fakeClient.FakeCalls[method], v)
// Get fake results
results := fakeClient.FakeResults[method][0]
// Remove fake result
fakeClient.FakeResults[method] = fakeClient.FakeResults[method][1:]
// Return fake results
return results
}
func (fakeClient *FakeDockerClient) ContainerStart(context context.Context, containerID string, options dockerTypes.ContainerStartOptions) (e error) {
results := fakeClient.called("ContainerStart", context, containerID, options)
if results[0] != nil {
e = results[0].(error)
}
return
}
func (fakeClient *FakeDockerClient) ContainerList(context context.Context, options dockerTypes.ContainerListOptions) (c []dockerTypes.Container, e error) {
results := fakeClient.called("ContainerList", context, options)
if results[0] != nil {
c = results[0].([]dockerTypes.Container)
}
if results[1] != nil {
e = results[1].(error)
}
return
}
func (fakeClient *FakeDockerClient) ContainerExecCreate(ctx context.Context, container string, config dockerTypes.ExecConfig) (r dockerTypes.IDResponse, e error) {
results := fakeClient.called("ContainerExecCreate", ctx, container, config)
if results[0] != nil {
r = results[0].(dockerTypes.IDResponse)
}
if results[1] != nil {
e = results[1].(error)
}
return
}
func (fakeClient *FakeDockerClient) ContainerExecStart(ctx context.Context, execID string, config dockerTypes.ExecStartCheck) (e error) {
results := fakeClient.called("ContainerExecStart", ctx, execID, config)
if results[0] != nil {
e = results[0].(error)
}
return
}
func (fakeClient *FakeDockerClient) ContainerExecInspect(ctx context.Context, execID string) (r dockerTypes.ContainerExecInspect, e error) {
results := fakeClient.called("ContainerExecInspect", ctx, execID)
if results[0] != nil {
r = results[0].(dockerTypes.ContainerExecInspect)
}
if results[1] != nil {
e = results[1].(error)
}
return
}
func (fakeClient *FakeDockerClient) ContainerInspect(ctx context.Context, containerID string) (r dockerTypes.ContainerJSON, e error) {
results := fakeClient.called("ContainerInspect", ctx, containerID)
if results[0] != nil {
r = results[0].(dockerTypes.ContainerJSON)
}
if results[1] != nil {
e = results[1].(error)
}
return
}
// NewFakeDockerClient creates an empty client
func NewFakeDockerClient() *FakeDockerClient {
return &FakeDockerClient{}
}
// NewFakeDockerClientWithContainers creates a client with the provided containers
func NewFakeDockerClientWithContainers(containers []dockerTypes.Container) *FakeDockerClient {
return &FakeDockerClient{FakeContainers: containers}
return &FakeDockerClient{
FakeResults: map[string][]FakeResult{},
}
}
// ErrorUnequal checks that two values are equal and fails the test if not
@ -49,12 +165,12 @@ func TestQueryScheduledJobs(t *testing.T) {
cases := []struct {
name string
fakeContainers []dockerTypes.Container
expectedJobs []ContainerStartJob
expectedJobs []ContainerCronJob
}{
{
name: "No containers",
fakeContainers: []dockerTypes.Container{},
expectedJobs: []ContainerStartJob{},
expectedJobs: []ContainerCronJob{},
},
{
name: "One container without schedule",
@ -64,7 +180,7 @@ func TestQueryScheduledJobs(t *testing.T) {
ID: "no_schedule_1",
},
},
expectedJobs: []ContainerStartJob{},
expectedJobs: []ContainerCronJob{},
},
{
name: "One container with schedule",
@ -77,13 +193,13 @@ func TestQueryScheduledJobs(t *testing.T) {
},
},
},
expectedJobs: []ContainerStartJob{
expectedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1",
Schedule: "* * * * *",
Context: context.Background(),
Client: client,
name: "has_schedule_1",
containerID: "has_schedule_1",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
},
},
@ -102,13 +218,101 @@ func TestQueryScheduledJobs(t *testing.T) {
},
},
},
expectedJobs: []ContainerStartJob{
expectedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1",
Schedule: "* * * * *",
Context: context.Background(),
Client: client,
name: "has_schedule_1",
containerID: "has_schedule_1",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
},
},
{
name: "Incomplete exec job, schedule only",
fakeContainers: []dockerTypes.Container{
dockerTypes.Container{
Names: []string{"exec_job_1"},
ID: "exec_job_1",
Labels: map[string]string{
"dockron.test.schedule": "* * * * *",
},
},
},
expectedJobs: []ContainerCronJob{},
},
{
name: "Incomplete exec job, command only",
fakeContainers: []dockerTypes.Container{
dockerTypes.Container{
Names: []string{"exec_job_1"},
ID: "exec_job_1",
Labels: map[string]string{
"dockron.test.command": "date",
},
},
},
expectedJobs: []ContainerCronJob{},
},
{
name: "Complete exec job",
fakeContainers: []dockerTypes.Container{
dockerTypes.Container{
Names: []string{"exec_job_1"},
ID: "exec_job_1",
Labels: map[string]string{
"dockron.test.schedule": "* * * * *",
"dockron.test.command": "date",
},
},
},
expectedJobs: []ContainerCronJob{
ContainerExecJob{
ContainerStartJob: ContainerStartJob{
name: "exec_job_1/test",
containerID: "exec_job_1",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
shellCommand: "date",
},
},
},
{
name: "Dual exec jobs on single container",
fakeContainers: []dockerTypes.Container{
dockerTypes.Container{
Names: []string{"exec_job_1"},
ID: "exec_job_1",
Labels: map[string]string{
"dockron.test1.schedule": "* * * * *",
"dockron.test1.command": "date",
"dockron.test2.schedule": "* * * * *",
"dockron.test2.command": "date",
},
},
},
expectedJobs: []ContainerCronJob{
ContainerExecJob{
ContainerStartJob: ContainerStartJob{
name: "exec_job_1/test1",
containerID: "exec_job_1",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
shellCommand: "date",
},
ContainerExecJob{
ContainerStartJob: ContainerStartJob{
name: "exec_job_1/test2",
containerID: "exec_job_1",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
shellCommand: "date",
},
},
},
@ -120,12 +324,17 @@ func TestQueryScheduledJobs(t *testing.T) {
// Load fake containers
t.Logf("Fake containers: %+v", c.fakeContainers)
client.FakeContainers = c.fakeContainers
client.FakeResults["ContainerList"] = []FakeResult{
FakeResult{c.fakeContainers, nil},
}
jobs := QueryScheduledJobs(client)
// Sort so we can compare each list of jobs
sort.Slice(jobs, func(i, j int) bool {
return jobs[i].UniqueName() < jobs[j].UniqueName()
})
t.Logf("Expected jobs: %+v, Actual jobs: %+v", c.expectedJobs, jobs)
ErrorUnequal(t, len(c.expectedJobs), len(jobs), "Job lengths don't match")
for i, job := range jobs {
ErrorUnequal(t, c.expectedJobs[i], job, "Job value does not match")
@ -142,82 +351,82 @@ func TestScheduleJobs(t *testing.T) {
// Tests must be executed sequentially!
cases := []struct {
name string
queriedJobs []ContainerStartJob
expectedJobs []ContainerStartJob
queriedJobs []ContainerCronJob
expectedJobs []ContainerCronJob
}{
{
name: "No containers",
queriedJobs: []ContainerStartJob{},
expectedJobs: []ContainerStartJob{},
queriedJobs: []ContainerCronJob{},
expectedJobs: []ContainerCronJob{},
},
{
name: "One container with schedule",
queriedJobs: []ContainerStartJob{
queriedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1",
Schedule: "* * * * *",
name: "has_schedule_1",
containerID: "has_schedule_1",
schedule: "* * * * *",
},
},
expectedJobs: []ContainerStartJob{
expectedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1",
Schedule: "* * * * *",
name: "has_schedule_1",
containerID: "has_schedule_1",
schedule: "* * * * *",
},
},
},
{
name: "Add a second job",
queriedJobs: []ContainerStartJob{
queriedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1",
Schedule: "* * * * *",
name: "has_schedule_1",
containerID: "has_schedule_1",
schedule: "* * * * *",
},
ContainerStartJob{
Name: "has_schedule_2",
ContainerID: "has_schedule_2",
Schedule: "* * * * *",
name: "has_schedule_2",
containerID: "has_schedule_2",
schedule: "* * * * *",
},
},
expectedJobs: []ContainerStartJob{
expectedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1",
Schedule: "* * * * *",
name: "has_schedule_1",
containerID: "has_schedule_1",
schedule: "* * * * *",
},
ContainerStartJob{
Name: "has_schedule_2",
ContainerID: "has_schedule_2",
Schedule: "* * * * *",
name: "has_schedule_2",
containerID: "has_schedule_2",
schedule: "* * * * *",
},
},
},
{
name: "Replace job 1",
queriedJobs: []ContainerStartJob{
queriedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1_prime",
Schedule: "* * * * *",
name: "has_schedule_1",
containerID: "has_schedule_1_prime",
schedule: "* * * * *",
},
ContainerStartJob{
Name: "has_schedule_2",
ContainerID: "has_schedule_2",
Schedule: "* * * * *",
name: "has_schedule_2",
containerID: "has_schedule_2",
schedule: "* * * * *",
},
},
expectedJobs: []ContainerStartJob{
expectedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_2",
ContainerID: "has_schedule_2",
Schedule: "* * * * *",
name: "has_schedule_2",
containerID: "has_schedule_2",
schedule: "* * * * *",
},
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1_prime",
Schedule: "* * * * *",
name: "has_schedule_1",
containerID: "has_schedule_1_prime",
schedule: "* * * * *",
},
},
},
@ -253,12 +462,12 @@ func TestDoLoop(t *testing.T) {
cases := []struct {
name string
fakeContainers []dockerTypes.Container
expectedJobs []ContainerStartJob
expectedJobs []ContainerCronJob
}{
{
name: "No containers",
fakeContainers: []dockerTypes.Container{},
expectedJobs: []ContainerStartJob{},
expectedJobs: []ContainerCronJob{},
},
{
name: "One container without schedule",
@ -268,7 +477,7 @@ func TestDoLoop(t *testing.T) {
ID: "no_schedule_1",
},
},
expectedJobs: []ContainerStartJob{},
expectedJobs: []ContainerCronJob{},
},
{
name: "One container with schedule",
@ -281,13 +490,13 @@ func TestDoLoop(t *testing.T) {
},
},
},
expectedJobs: []ContainerStartJob{
expectedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1",
Schedule: "* * * * *",
Context: context.Background(),
Client: client,
name: "has_schedule_1",
containerID: "has_schedule_1",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
},
},
@ -306,13 +515,13 @@ func TestDoLoop(t *testing.T) {
},
},
},
expectedJobs: []ContainerStartJob{
expectedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1",
Schedule: "* * * * *",
Context: context.Background(),
Client: client,
name: "has_schedule_1",
containerID: "has_schedule_1",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
},
},
@ -334,20 +543,20 @@ func TestDoLoop(t *testing.T) {
},
},
},
expectedJobs: []ContainerStartJob{
expectedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1",
Schedule: "* * * * *",
Context: context.Background(),
Client: client,
name: "has_schedule_1",
containerID: "has_schedule_1",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
ContainerStartJob{
Name: "has_schedule_2",
ContainerID: "has_schedule_2",
Schedule: "* * * * *",
Context: context.Background(),
Client: client,
name: "has_schedule_2",
containerID: "has_schedule_2",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
},
},
@ -369,20 +578,53 @@ func TestDoLoop(t *testing.T) {
},
},
},
expectedJobs: []ContainerStartJob{
expectedJobs: []ContainerCronJob{
ContainerStartJob{
Name: "has_schedule_2",
ContainerID: "has_schedule_2",
Schedule: "* * * * *",
Context: context.Background(),
Client: client,
name: "has_schedule_2",
containerID: "has_schedule_2",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
ContainerStartJob{
Name: "has_schedule_1",
ContainerID: "has_schedule_1_prime",
Schedule: "* * * * *",
Context: context.Background(),
Client: client,
name: "has_schedule_1",
containerID: "has_schedule_1_prime",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
},
},
{
name: "Remove second container and add exec to first",
fakeContainers: []dockerTypes.Container{
dockerTypes.Container{
Names: []string{"has_schedule_1"},
ID: "has_schedule_1_prime",
Labels: map[string]string{
"dockron.schedule": "* * * * *",
"dockron.test.schedule": "* * * * *",
"dockron.test.command": "date",
},
},
},
expectedJobs: []ContainerCronJob{
ContainerStartJob{
name: "has_schedule_1",
containerID: "has_schedule_1_prime",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
ContainerExecJob{
ContainerStartJob: ContainerStartJob{
name: "has_schedule_1/test",
containerID: "has_schedule_1_prime",
schedule: "* * * * *",
context: context.Background(),
client: client,
},
shellCommand: "date",
},
},
},
@ -394,7 +636,9 @@ func TestDoLoop(t *testing.T) {
// Load fake containers
t.Logf("Fake containers: %+v", c.fakeContainers)
client.FakeContainers = c.fakeContainers
client.FakeResults["ContainerList"] = []FakeResult{
FakeResult{c.fakeContainers, nil},
}
// Execute loop iteration loop
jobs := QueryScheduledJobs(client)
@ -415,3 +659,342 @@ func TestDoLoop(t *testing.T) {
// Make sure the cron stops
croner.Stop()
}
// TestRunExecJobs does some verification on handling of exec jobs
// These tests aren't great because there are no return values to check
// but some test is better than no test! Future maybe these can be moved
// to a subpackage that offers a single function for interfacing with the
// Docker client to start or exec a container so that Dockron needn't care.
func TestRunExecJobs(t *testing.T) {
var jobContext context.Context
jobContainerID := "container_id"
jobCommand := "true"
cases := []struct {
name string
client *FakeDockerClient
expectPanic bool
expectedCalls map[string][]FakeCall
}{
{
name: "Initial inspect call raises error",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{nil, errGeneric},
},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
},
},
expectPanic: true,
},
{
name: "Handle container not running",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{stoppedContainerInfo, nil},
},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
},
},
},
{
name: "Handle error creating exec",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{runningContainerInfo, nil},
},
"ContainerExecCreate": []FakeResult{
FakeResult{nil, errGeneric},
},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
},
"ContainerExecCreate": []FakeCall{
FakeCall{
jobContext,
jobContainerID,
dockerTypes.ExecConfig{
Cmd: []string{"sh", "-c", jobCommand},
},
},
},
},
expectPanic: true,
},
{
name: "Fail starting exec container",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{runningContainerInfo, nil},
},
"ContainerExecCreate": []FakeResult{
FakeResult{dockerTypes.IDResponse{ID: "id"}, nil},
},
"ContainerExecStart": []FakeResult{
FakeResult{errGeneric},
},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
},
"ContainerExecCreate": []FakeCall{
FakeCall{
jobContext,
jobContainerID,
dockerTypes.ExecConfig{
Cmd: []string{"sh", "-c", jobCommand},
},
},
},
"ContainerExecStart": []FakeCall{
FakeCall{jobContext, "id", dockerTypes.ExecStartCheck{}},
},
},
expectPanic: true,
},
{
name: "Successfully start an exec job fail on status",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{runningContainerInfo, nil},
},
"ContainerExecCreate": []FakeResult{
FakeResult{dockerTypes.IDResponse{ID: "id"}, nil},
},
"ContainerExecStart": []FakeResult{
FakeResult{nil},
},
"ContainerExecInspect": []FakeResult{
FakeResult{nil, errGeneric},
},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
},
"ContainerExecCreate": []FakeCall{
FakeCall{
jobContext,
jobContainerID,
dockerTypes.ExecConfig{
Cmd: []string{"sh", "-c", jobCommand},
},
},
},
"ContainerExecStart": []FakeCall{
FakeCall{jobContext, "id", dockerTypes.ExecStartCheck{}},
},
"ContainerExecInspect": []FakeCall{
FakeCall{jobContext, "id"},
},
},
},
{
name: "Successfully start an exec job and run to completion",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{runningContainerInfo, nil},
},
"ContainerExecCreate": []FakeResult{
FakeResult{dockerTypes.IDResponse{ID: "id"}, nil},
},
"ContainerExecStart": []FakeResult{
FakeResult{nil},
},
"ContainerExecInspect": []FakeResult{
FakeResult{dockerTypes.ContainerExecInspect{Running: true}, nil},
FakeResult{dockerTypes.ContainerExecInspect{Running: false}, nil},
},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
},
"ContainerExecCreate": []FakeCall{
FakeCall{
jobContext,
jobContainerID,
dockerTypes.ExecConfig{
Cmd: []string{"sh", "-c", jobCommand},
},
},
},
"ContainerExecStart": []FakeCall{
FakeCall{jobContext, "id", dockerTypes.ExecStartCheck{}},
},
"ContainerExecInspect": []FakeCall{
FakeCall{jobContext, "id"},
FakeCall{jobContext, "id"},
},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
log.Printf("Running %s", t.Name())
// Create test job
job := ContainerExecJob{
ContainerStartJob: ContainerStartJob{
name: "test_job",
context: jobContext,
client: c.client,
containerID: jobContainerID,
},
shellCommand: jobCommand,
}
defer func() {
// Recover from panics, if there were any
if err := recover(); err != nil {
t.Log("Recovered from panic")
t.Log(err)
}
c.client.AssertFakeCalls(t, c.expectedCalls, "Failed")
}()
job.Run()
if c.expectPanic {
t.Errorf("Expected panic but got none")
}
})
}
}
// TestRunStartJobs does some verification on handling of start jobs
// These tests aren't great because there are no return values to check
// but some test is better than no test! Future maybe these can be moved
// to a subpackage that offers a single function for interfacing with the
// Docker client to start or exec a container so that Dockron needn't care.
func TestRunStartJobs(t *testing.T) {
var jobContext context.Context
jobContainerID := "container_id"
cases := []struct {
name string
client *FakeDockerClient
expectPanic bool
expectedCalls map[string][]FakeCall
}{
{
name: "Initial inspect call raises error",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{nil, errGeneric},
},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
},
},
expectPanic: true,
},
{
name: "Handle container already running",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{runningContainerInfo, nil},
},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
},
},
},
{
name: "Handle error starting container",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{stoppedContainerInfo, nil},
},
"ContainerStart": []FakeResult{FakeResult{errGeneric}},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
},
"ContainerStart": []FakeCall{
FakeCall{jobContext, jobContainerID, dockerTypes.ContainerStartOptions{}},
},
},
},
{
name: "Successfully start a container",
client: &FakeDockerClient{
FakeResults: map[string][]FakeResult{
"ContainerInspect": []FakeResult{
FakeResult{stoppedContainerInfo, nil},
FakeResult{runningContainerInfo, nil},
FakeResult{stoppedContainerInfo, nil},
},
"ContainerStart": []FakeResult{FakeResult{nil}},
},
},
expectedCalls: map[string][]FakeCall{
"ContainerInspect": []FakeCall{
FakeCall{jobContext, jobContainerID},
FakeCall{jobContext, jobContainerID},
FakeCall{jobContext, jobContainerID},
},
"ContainerStart": []FakeCall{
FakeCall{jobContext, jobContainerID, dockerTypes.ContainerStartOptions{}},
},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
log.Printf("Running %s", t.Name())
// Create test job
job := ContainerStartJob{
name: "test_job",
context: jobContext,
client: c.client,
containerID: jobContainerID,
}
defer func() {
// Recover from panics, if there were any
_ = recover()
c.client.AssertFakeCalls(t, c.expectedCalls, "Failed")
}()
job.Run()
if c.expectPanic {
t.Errorf("Expected panic but got none")
}
})
}
}