2018-08-01 15:56:13 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-09-26 18:48:11 +00:00
|
|
|
"bufio"
|
2018-08-04 00:45:53 +00:00
|
|
|
"flag"
|
2018-08-01 15:56:13 +00:00
|
|
|
"fmt"
|
2019-06-28 17:24:41 +00:00
|
|
|
"os"
|
2020-08-16 21:46:35 +00:00
|
|
|
"regexp"
|
2018-08-01 15:56:13 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
2020-08-07 22:00:30 +00:00
|
|
|
|
2021-04-29 16:35:36 +00:00
|
|
|
"git.iamthefij.com/iamthefij/slog"
|
2020-08-07 22:00:30 +00:00
|
|
|
dockerTypes "github.com/docker/docker/api/types"
|
2024-09-25 18:58:41 +00:00
|
|
|
"github.com/docker/docker/api/types/container"
|
2020-08-07 22:00:30 +00:00
|
|
|
dockerClient "github.com/docker/docker/client"
|
|
|
|
"github.com/robfig/cron/v3"
|
|
|
|
"golang.org/x/net/context"
|
2018-08-01 15:56:13 +00:00
|
|
|
)
|
|
|
|
|
2019-06-28 17:24:41 +00:00
|
|
|
var (
|
|
|
|
// defaultWatchInterval is the duration we should sleep until polling Docker
|
|
|
|
defaultWatchInterval = (1 * time.Minute)
|
2018-08-03 01:56:02 +00:00
|
|
|
|
2019-06-28 17:24:41 +00:00
|
|
|
// schedLabel is the string label to search for cron expressions
|
|
|
|
schedLabel = "dockron.schedule"
|
2020-08-16 21:46:35 +00:00
|
|
|
// execLabelRegex is will capture labels for an exec job
|
|
|
|
execLabelRegexp = regexp.MustCompile(`dockron\.([a-zA-Z0-9_-]+)\.(schedule|command)`)
|
2019-06-28 17:24:41 +00:00
|
|
|
|
|
|
|
// version of dockron being run
|
|
|
|
version = "dev"
|
|
|
|
)
|
2018-08-04 00:30:17 +00:00
|
|
|
|
2024-09-26 18:48:11 +00:00
|
|
|
// ContainerClient provides an interface for interracting with Docker. Makes it possible to mock in tests
|
2020-08-07 16:55:19 +00:00
|
|
|
type ContainerClient interface {
|
2024-09-25 18:58:41 +00:00
|
|
|
ContainerExecCreate(ctx context.Context, container string, config container.ExecOptions) (dockerTypes.IDResponse, error)
|
|
|
|
ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error)
|
|
|
|
ContainerExecStart(ctx context.Context, execID string, config container.ExecStartOptions) error
|
2024-09-26 18:48:11 +00:00
|
|
|
ContainerExecAttach(ctx context.Context, execID string, options container.ExecAttachOptions) (dockerTypes.HijackedResponse, error)
|
2020-08-16 21:46:35 +00:00
|
|
|
ContainerInspect(ctx context.Context, containerID string) (dockerTypes.ContainerJSON, error)
|
2024-09-25 18:58:41 +00:00
|
|
|
ContainerList(context context.Context, options container.ListOptions) ([]dockerTypes.Container, error)
|
|
|
|
ContainerStart(context context.Context, containerID string, options container.StartOptions) error
|
2020-08-16 21:46:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ContainerCronJob is an interface of a job to run on containers
|
|
|
|
type ContainerCronJob interface {
|
|
|
|
Run()
|
|
|
|
Name() string
|
|
|
|
UniqueName() string
|
|
|
|
Schedule() string
|
2020-08-07 16:55:19 +00:00
|
|
|
}
|
|
|
|
|
2018-08-04 00:30:17 +00:00
|
|
|
// 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
|
2018-08-01 15:56:13 +00:00
|
|
|
type ContainerStartJob struct {
|
2020-08-16 21:46:35 +00:00
|
|
|
client ContainerClient
|
|
|
|
context context.Context
|
|
|
|
name string
|
|
|
|
containerID string
|
|
|
|
schedule string
|
2018-08-01 15:56:13 +00:00
|
|
|
}
|
|
|
|
|
2018-08-04 00:30:17 +00:00
|
|
|
// Run is executed based on the ContainerStartJob Schedule and starts the
|
|
|
|
// container
|
2018-08-01 15:56:13 +00:00
|
|
|
func (job ContainerStartJob) Run() {
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.Infof("Starting: %s", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
|
|
|
|
// Check if container is already running
|
|
|
|
containerJSON, err := job.client.ContainerInspect(
|
|
|
|
job.context,
|
|
|
|
job.containerID,
|
|
|
|
)
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.OnErrPanicf(err, "Could not get container details for job %s", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
|
|
|
|
if containerJSON.State.Running {
|
2024-09-26 18:48:11 +00:00
|
|
|
slog.Warningf("%s: Container is already running. Skipping start.", job.name)
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start job
|
|
|
|
err = job.client.ContainerStart(
|
|
|
|
job.context,
|
|
|
|
job.containerID,
|
2024-09-25 18:58:41 +00:00
|
|
|
container.StartOptions{},
|
2020-04-07 16:58:12 +00:00
|
|
|
)
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.OnErrPanicf(err, "Could not start container for job %s", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
|
|
|
|
// Check results of job
|
|
|
|
for check := true; check; check = containerJSON.State.Running {
|
2024-09-26 18:48:11 +00:00
|
|
|
slog.Debugf("%s: Still running", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
|
|
|
|
containerJSON, err = job.client.ContainerInspect(
|
|
|
|
job.context,
|
|
|
|
job.containerID,
|
|
|
|
)
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.OnErrPanicf(err, "Could not get container details for job %s", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
|
|
|
|
time.Sleep(1 * time.Second)
|
2018-08-01 15:56:13 +00:00
|
|
|
}
|
2024-09-26 18:48:11 +00:00
|
|
|
slog.Debugf("%s: Done running. %+v", job.name, containerJSON.State)
|
2024-09-25 18:58:41 +00:00
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
// Log exit code if failed
|
|
|
|
if containerJSON.State.ExitCode != 0 {
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.Errorf(
|
2024-09-26 18:48:11 +00:00
|
|
|
"%s: Exec job exited with code %d",
|
2020-08-16 21:46:35 +00:00
|
|
|
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
|
2018-08-01 15:56:13 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 16:58:12 +00:00
|
|
|
// 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
|
2020-08-16 21:46:35 +00:00
|
|
|
return job.name + "/" + job.containerID
|
2020-04-07 16:58:12 +00:00
|
|
|
}
|
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
// 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() {
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.Infof("Execing: %s", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
containerJSON, err := job.client.ContainerInspect(
|
|
|
|
job.context,
|
|
|
|
job.containerID,
|
|
|
|
)
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.OnErrPanicf(err, "Could not get container details for job %s", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
|
|
|
|
if !containerJSON.State.Running {
|
2024-09-26 18:48:11 +00:00
|
|
|
slog.Warningf("%s: Container not running. Skipping exec.", job.name)
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
return
|
2020-04-07 16:58:12 +00:00
|
|
|
}
|
2020-08-16 21:46:35 +00:00
|
|
|
|
|
|
|
execID, err := job.client.ContainerExecCreate(
|
|
|
|
job.context,
|
|
|
|
job.containerID,
|
2024-09-25 18:58:41 +00:00
|
|
|
container.ExecOptions{
|
2024-09-26 18:48:11 +00:00
|
|
|
AttachStdout: true,
|
|
|
|
AttachStderr: true,
|
|
|
|
Cmd: []string{"sh", "-c", strings.TrimSpace(job.shellCommand)},
|
2020-08-16 21:46:35 +00:00
|
|
|
},
|
|
|
|
)
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.OnErrPanicf(err, "Could not create container exec job for %s", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
|
2024-09-26 18:48:11 +00:00
|
|
|
hj, err := job.client.ContainerExecAttach(job.context, execID.ID, container.ExecAttachOptions{})
|
|
|
|
slog.OnErrWarnf(err, "%s: Error attaching to exec: %s", job.name, err)
|
|
|
|
defer hj.Close()
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(hj.Reader)
|
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
err = job.client.ContainerExecStart(
|
|
|
|
job.context,
|
|
|
|
execID.ID,
|
2024-09-25 18:58:41 +00:00
|
|
|
container.ExecStartOptions{},
|
2020-08-16 21:46:35 +00:00
|
|
|
)
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.OnErrPanicf(err, "Could not start container exec job for %s", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
|
|
|
|
// Wait for job results
|
2024-09-25 18:58:41 +00:00
|
|
|
execInfo := container.ExecInspect{Running: true}
|
2020-08-16 21:46:35 +00:00
|
|
|
for execInfo.Running {
|
2024-09-26 18:48:11 +00:00
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.Debugf("Still execing %s", job.name)
|
2020-08-16 21:46:35 +00:00
|
|
|
execInfo, err = job.client.ContainerExecInspect(
|
|
|
|
job.context,
|
|
|
|
execID.ID,
|
|
|
|
)
|
2024-09-26 18:48:11 +00:00
|
|
|
|
|
|
|
// Maybe print output
|
|
|
|
if hj.Reader != nil {
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := scanner.Text()
|
|
|
|
if len(line) > 0 {
|
|
|
|
slog.Infof("%s: Exec output: %s", job.name, line)
|
|
|
|
} else {
|
|
|
|
slog.Debugf("%s: Empty exec output", job.name)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
slog.OnErrWarnf(err, "%s: Error reading from exec", job.name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
slog.Debugf("%s: No exec reader", job.name)
|
|
|
|
}
|
|
|
|
|
|
|
|
slog.Debugf("%s: Exec info: %+v", job.name, execInfo)
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
if err != nil {
|
|
|
|
// Nothing we can do if we got an error here, so let's go
|
2024-09-26 18:48:11 +00:00
|
|
|
slog.OnErrWarnf(err, "%s: Could not get status for exec job", job.name)
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2024-09-26 18:48:11 +00:00
|
|
|
slog.Debugf("%s: Done execing. %+v", job.name, execInfo)
|
2020-08-16 21:46:35 +00:00
|
|
|
// Log exit code if failed
|
|
|
|
if execInfo.ExitCode != 0 {
|
2024-09-26 18:48:11 +00:00
|
|
|
slog.Errorf("%s: Exec job existed with code %d", job.name, execInfo.ExitCode)
|
2020-08-16 21:46:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// QueryScheduledJobs queries Docker for all containers with a schedule and
|
|
|
|
// returns a list of ContainerCronJob records to be scheduled
|
|
|
|
func QueryScheduledJobs(client ContainerClient) (jobs []ContainerCronJob) {
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.Debugf("Scanning containers for new schedules...")
|
2020-08-16 21:46:35 +00:00
|
|
|
|
2020-04-07 16:58:12 +00:00
|
|
|
containers, err := client.ContainerList(
|
|
|
|
context.Background(),
|
2024-09-25 18:58:41 +00:00
|
|
|
container.ListOptions{All: true},
|
2020-04-07 16:58:12 +00:00
|
|
|
)
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.OnErrPanicf(err, "Failure querying docker containers")
|
2018-08-01 15:56:13 +00:00
|
|
|
|
|
|
|
for _, container := range containers {
|
2020-08-16 21:46:35 +00:00
|
|
|
// Add start job
|
2019-06-28 17:24:41 +00:00
|
|
|
if val, ok := container.Labels[schedLabel]; ok {
|
2018-08-01 15:56:13 +00:00
|
|
|
jobName := strings.Join(container.Names, "/")
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2018-08-01 15:59:52 +00:00
|
|
|
jobs = append(jobs, ContainerStartJob{
|
2020-08-16 21:46:35 +00:00
|
|
|
client: client,
|
|
|
|
containerID: container.ID,
|
|
|
|
context: context.Background(),
|
|
|
|
schedule: val,
|
|
|
|
name: jobName,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add exec jobs
|
|
|
|
execJobs := map[string]map[string]string{}
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
for label, value := range container.Labels {
|
|
|
|
results := execLabelRegexp.FindStringSubmatch(label)
|
2021-04-30 18:24:14 +00:00
|
|
|
expectedLabelParts := 3
|
|
|
|
|
|
|
|
if len(results) == expectedLabelParts {
|
2020-08-16 21:46:35 +00:00
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
for jobName, jobConfig := range execJobs {
|
|
|
|
schedule, ok := jobConfig["schedule"]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
shellCommand, ok := jobConfig["command"]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2020-08-16 21:46:35 +00:00
|
|
|
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,
|
2018-08-01 15:56:13 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-30 18:24:14 +00:00
|
|
|
return jobs
|
2018-08-03 01:56:02 +00:00
|
|
|
}
|
2018-08-01 15:59:52 +00:00
|
|
|
|
2018-08-04 00:30:17 +00:00
|
|
|
// ScheduleJobs accepts a Cron instance and a list of jobs to schedule.
|
|
|
|
// It then schedules the provided jobs
|
2020-08-16 21:46:35 +00:00
|
|
|
func ScheduleJobs(c *cron.Cron, jobs []ContainerCronJob) {
|
2020-04-07 16:58:12 +00:00
|
|
|
// Fetch existing jobs from the cron
|
|
|
|
existingJobs := map[string]cron.EntryID{}
|
|
|
|
for _, entry := range c.Entries() {
|
2020-08-16 21:46:35 +00:00
|
|
|
// This should be safe since ContainerCronJob is the only type of job we use
|
|
|
|
existingJobs[entry.Job.(ContainerCronJob).UniqueName()] = entry.ID
|
2020-04-07 16:58:12 +00:00
|
|
|
}
|
|
|
|
|
2018-08-01 15:59:52 +00:00
|
|
|
for _, job := range jobs {
|
2020-04-07 16:58:12 +00:00
|
|
|
if _, ok := existingJobs[job.UniqueName()]; ok {
|
|
|
|
// Job already exists, remove it from existing jobs so we don't
|
|
|
|
// unschedule it later
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.Debugf("Job %s is already scheduled. Skipping", job.Name())
|
2020-04-07 16:58:12 +00:00
|
|
|
delete(existingJobs, job.UniqueName())
|
2021-04-30 18:24:14 +00:00
|
|
|
|
2020-04-07 16:58:12 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Job doesn't exist yet, schedule it
|
2020-08-16 21:46:35 +00:00
|
|
|
_, err := c.AddJob(job.Schedule(), job)
|
2020-04-06 23:53:28 +00:00
|
|
|
if err == nil {
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.Infof(
|
2020-04-07 16:58:12 +00:00
|
|
|
"Scheduled %s (%s) with schedule '%s'\n",
|
2020-08-16 21:46:35 +00:00
|
|
|
job.Name(),
|
|
|
|
job.UniqueName(),
|
|
|
|
job.Schedule(),
|
2020-04-07 16:58:12 +00:00
|
|
|
)
|
2020-04-06 23:53:28 +00:00
|
|
|
} else {
|
|
|
|
// TODO: Track something for a healthcheck here
|
2021-04-29 16:35:36 +00:00
|
|
|
slog.Errorf(
|
2020-08-16 21:46:35 +00:00
|
|
|
"Could not schedule %s (%s) with schedule '%s'. %v\n",
|
|
|
|
job.Name(),
|
|
|
|
job.UniqueName(),
|
|
|
|
job.Schedule(),
|
2020-04-07 16:58:12 +00:00
|
|
|
err,
|
|
|
|
)
|
2020-04-06 23:53:28 +00:00
|
|
|
}
|
2018-08-01 15:59:52 +00:00
|
|
|
}
|
2020-04-07 16:58:12 +00:00
|
|
|
|
|
|
|
// Remove remaining scheduled jobs that weren't in the new list
|
|
|
|
for _, entryID := range existingJobs {
|
|
|
|
c.Remove(entryID)
|
|
|
|
}
|
2018-08-03 01:56:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
2018-08-04 00:30:17 +00:00
|
|
|
// Get a Docker Client
|
2020-08-07 22:43:17 +00:00
|
|
|
client, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv)
|
2021-04-30 18:24:14 +00:00
|
|
|
slog.OnErrPanicf(err, "Could not create Docker client")
|
2018-08-01 15:59:52 +00:00
|
|
|
|
2018-08-04 00:45:53 +00:00
|
|
|
// Read interval for polling Docker
|
|
|
|
var watchInterval time.Duration
|
2021-04-30 18:24:14 +00:00
|
|
|
|
|
|
|
showVersion := flag.Bool("version", false, "Display the version of dockron and exit")
|
|
|
|
|
2019-06-28 17:24:41 +00:00
|
|
|
flag.DurationVar(&watchInterval, "watch", defaultWatchInterval, "Interval used to poll Docker for changes")
|
2020-08-16 21:46:35 +00:00
|
|
|
flag.BoolVar(&slog.DebugLevel, "debug", false, "Show debug logs")
|
2018-08-04 00:45:53 +00:00
|
|
|
flag.Parse()
|
|
|
|
|
2019-06-28 17:24:41 +00:00
|
|
|
// Print version if asked
|
|
|
|
if *showVersion {
|
|
|
|
fmt.Println("Dockron version:", version)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
2018-08-04 00:30:17 +00:00
|
|
|
// Create a Cron
|
2018-08-03 01:56:02 +00:00
|
|
|
c := cron.New()
|
2020-04-07 16:58:12 +00:00
|
|
|
c.Start()
|
2018-08-01 15:56:13 +00:00
|
|
|
|
|
|
|
// Start the loop
|
|
|
|
for {
|
2020-04-07 16:58:12 +00:00
|
|
|
// Schedule jobs again
|
2020-08-07 16:55:19 +00:00
|
|
|
jobs := QueryScheduledJobs(client)
|
2018-08-03 01:56:02 +00:00
|
|
|
ScheduleJobs(c, jobs)
|
2020-08-07 22:00:30 +00:00
|
|
|
|
2018-08-03 01:56:02 +00:00
|
|
|
// Sleep until the next query time
|
2018-08-04 00:45:53 +00:00
|
|
|
time.Sleep(watchInterval)
|
2018-08-01 15:56:13 +00:00
|
|
|
}
|
|
|
|
}
|