Skip to content

Commit 62009ac

Browse files
rdelcorroTheDevMinerTV
authored andcommitted
Applied changes from mcuadros#137
1 parent 94edcdf commit 62009ac

21 files changed

+451
-231
lines changed

Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.17.1-alpine AS builder
1+
FROM golang:1.18.1-alpine3.15 AS builder
22

33
RUN apk --no-cache add gcc musl-dev
44

@@ -7,7 +7,7 @@ COPY . ${GOPATH}/src/github.com/mcuadros/ofelia
77

88
RUN go build -o /go/bin/ofelia .
99

10-
FROM alpine:3.14.2
10+
FROM alpine:3.16.0
1111

1212
# this label is required to identify container with ofelia running
1313
LABEL ofelia.service=true

README.md

+37-7
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ In order to use this type of configurations, ofelia need access to docker socket
6565
```sh
6666
docker run -it --rm \
6767
-v /var/run/docker.sock:/var/run/docker.sock:ro \
68-
--label ofelia.job-local.my-test-job.schedule="@every 5s" \
69-
--label ofelia.job-local.my-test-job.command="date" \
7068
mcuadros/ofelia:latest daemon --docker
7169
```
7270

@@ -86,9 +84,8 @@ docker run -it --rm \
8684
nginx
8785
```
8886

89-
Now if we start `ofelia` container with the command provided above, it will pickup 2 jobs:
87+
Now if we start `ofelia` container with the command provided above, it will execute the task:
9088

91-
- Local - `date`
9289
- Exec - `uname -a`
9390

9491
Or with docker-compose:
@@ -103,9 +100,6 @@ services:
103100
command: daemon --docker
104101
volumes:
105102
- /var/run/docker.sock:/var/run/docker.sock:ro
106-
labels:
107-
ofelia.job-local.my-test-job.schedule: "@every 5s"
108-
ofelia.job-local.my-test-job.command: "date"
109103

110104
nginx:
111105
image: nginx
@@ -115,6 +109,42 @@ services:
115109
ofelia.job-exec.datecron.command: "uname -a"
116110
```
117111
112+
#### Dynamic docker configuration
113+
114+
You can start ofelia in its own container or on the host itself, and it will magically pick up any container that starts, stops or is modified on the fly.
115+
In order to achieve this, you simply have to use docker containers with the labels described above and let ofelia take care of the rest.
116+
117+
#### Hybrid configuration (INI files + Docker)
118+
119+
You can specify part of the configuration on the INI files, such as globals for the middlewares or even declare tasks in there but also merge them with docker.
120+
The docker labels will be parsed, added and removed on the fly but also, the file config can be used.
121+
122+
**Use the INI file to:**
123+
124+
- Configure the slack or other middleware integration
125+
- Configure any global setting
126+
- Create a job-run so it executes on a new container each time
127+
128+
```ini
129+
[global]
130+
slack-webhook = https://myhook.com/auth
131+
132+
[job-run "job-executed-on-new-container"]
133+
schedule = @hourly
134+
image = ubuntu:latest
135+
command = touch /tmp/example
136+
```
137+
138+
**Use docker to:**
139+
140+
```sh
141+
docker run -it --rm \
142+
--label ofelia.enabled=true \
143+
--label ofelia.job-exec.test-exec-job.schedule="@every 5s" \
144+
--label ofelia.job-exec.test-exec-job.command="uname -a" \
145+
nginx
146+
```
147+
118148
### Logging
119149
**Ofelia** comes with three different logging drivers that can be configured in the `[global]` section:
120150
- `mail` to send mails

cli/config.go

+159-72
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package cli
22

33
import (
4-
"os"
5-
6-
docker "github.com/fsouza/go-dockerclient"
74
"github.com/mcuadros/ofelia/core"
85
"github.com/mcuadros/ofelia/middlewares"
9-
logging "github.com/op/go-logging"
106

117
defaults "github.com/mcuadros/go-defaults"
128
gcfg "gopkg.in/gcfg.v1"
@@ -20,132 +16,223 @@ const (
2016
jobLocal = "job-local"
2117
)
2218

23-
var IsDockerEnv bool
24-
2519
// Config contains the configuration
2620
type Config struct {
2721
Global struct {
2822
middlewares.SlackConfig `mapstructure:",squash"`
2923
middlewares.SaveConfig `mapstructure:",squash"`
3024
middlewares.MailConfig `mapstructure:",squash"`
3125
}
32-
ExecJobs map[string]*ExecJobConfig `gcfg:"job-exec" mapstructure:"job-exec,squash"`
33-
RunJobs map[string]*RunJobConfig `gcfg:"job-run" mapstructure:"job-run,squash"`
34-
ServiceJobs map[string]*RunServiceConfig `gcfg:"job-service-run" mapstructure:"job-service-run,squash"`
35-
LocalJobs map[string]*LocalJobConfig `gcfg:"job-local" mapstructure:"job-local,squash"`
26+
ExecJobs map[string]*ExecJobConfig `gcfg:"job-exec" mapstructure:"job-exec,squash"`
27+
RunJobs map[string]*RunJobConfig `gcfg:"job-run" mapstructure:"job-run,squash"`
28+
ServiceJobs map[string]*RunServiceConfig `gcfg:"job-service-run" mapstructure:"job-service-run,squash"`
29+
LocalJobs map[string]*LocalJobConfig `gcfg:"job-local" mapstructure:"job-local,squash"`
30+
sh *core.Scheduler
31+
dockerHandler *DockerHandler
32+
logger core.Logger
3633
}
3734

38-
// BuildFromDockerLabels builds a scheduler using the config from a docker labels
39-
func BuildFromDockerLabels() (*core.Scheduler, error) {
40-
c := &Config{}
41-
42-
d, err := c.buildDockerClient()
43-
if err != nil {
44-
return nil, err
45-
}
46-
47-
labels, err := getLabels(d)
48-
if err != nil {
49-
return nil, err
50-
}
51-
52-
if err := c.buildFromDockerLabels(labels); err != nil {
53-
return nil, err
35+
func NewConfig(logger core.Logger) *Config {
36+
c := &Config{
37+
ExecJobs: make(map[string]*ExecJobConfig),
38+
RunJobs: make(map[string]*RunJobConfig),
39+
ServiceJobs: make(map[string]*RunServiceConfig),
40+
LocalJobs: make(map[string]*LocalJobConfig),
41+
logger: logger,
5442
}
5543

56-
return c.build()
44+
defaults.SetDefaults(c)
45+
return c
5746
}
5847

5948
// BuildFromFile builds a scheduler using the config from a file
60-
func BuildFromFile(filename string) (*core.Scheduler, error) {
61-
c := &Config{}
62-
if err := gcfg.ReadFileInto(c, filename); err != nil {
63-
return nil, err
64-
}
65-
66-
return c.build()
49+
func BuildFromFile(filename string, logger core.Logger) (*Config, error) {
50+
c := NewConfig(logger)
51+
err := gcfg.ReadFileInto(c, filename)
52+
return c, err
6753
}
6854

6955
// BuildFromString builds a scheduler using the config from a string
70-
func BuildFromString(config string) (*core.Scheduler, error) {
71-
c := &Config{}
56+
func BuildFromString(config string, logger core.Logger) (*Config, error) {
57+
c := NewConfig(logger)
7258
if err := gcfg.ReadStringInto(c, config); err != nil {
7359
return nil, err
7460
}
75-
76-
return c.build()
61+
return c, nil
7762
}
7863

79-
func (c *Config) build() (*core.Scheduler, error) {
80-
defaults.SetDefaults(c)
64+
// Call this only once at app init
65+
func (c *Config) InitializeApp() error {
66+
c.sh = core.NewScheduler(c.logger)
67+
c.buildSchedulerMiddlewares(c.sh)
8168

82-
d, err := c.buildDockerClient()
69+
var err error
70+
c.dockerHandler, err = NewDockerHandler(c, c.logger)
8371
if err != nil {
84-
return nil, err
72+
return err
8573
}
8674

87-
sh := core.NewScheduler(c.buildLogger())
88-
c.buildSchedulerMiddlewares(sh)
75+
// In order to support non dynamic job types such as Local or Run using labels
76+
// lets parse the labels and merge the job lists
77+
dockerLabels, err := c.dockerHandler.GetDockerLabels()
78+
var parsedLabelConfig Config
79+
parsedLabelConfig.buildFromDockerLabels(dockerLabels)
80+
for name, j := range parsedLabelConfig.RunJobs {
81+
c.RunJobs[name] = j
82+
}
83+
for name, j := range parsedLabelConfig.LocalJobs {
84+
c.LocalJobs[name] = j
85+
}
86+
for name, j := range parsedLabelConfig.ServiceJobs {
87+
c.ServiceJobs[name] = j
88+
}
8989

9090
for name, j := range c.ExecJobs {
9191
defaults.SetDefaults(j)
92-
93-
j.Client = d
92+
j.Client = c.dockerHandler.GetInternalDockerClient()
9493
j.Name = name
9594
j.buildMiddlewares()
96-
sh.AddJob(j)
95+
c.sh.AddJob(j)
9796
}
9897

9998
for name, j := range c.RunJobs {
10099
defaults.SetDefaults(j)
101-
102-
j.Client = d
100+
j.Client = c.dockerHandler.GetInternalDockerClient()
103101
j.Name = name
104102
j.buildMiddlewares()
105-
sh.AddJob(j)
103+
c.sh.AddJob(j)
106104
}
107105

108106
for name, j := range c.LocalJobs {
109107
defaults.SetDefaults(j)
110-
111108
j.Name = name
112109
j.buildMiddlewares()
113-
sh.AddJob(j)
110+
c.sh.AddJob(j)
114111
}
115112

116113
for name, j := range c.ServiceJobs {
117114
defaults.SetDefaults(j)
118115
j.Name = name
119-
j.Client = d
116+
j.Client = c.dockerHandler.GetInternalDockerClient()
120117
j.buildMiddlewares()
121-
sh.AddJob(j)
118+
c.sh.AddJob(j)
122119
}
123120

124-
return sh, nil
121+
return nil
125122
}
126123

127-
func (c *Config) buildDockerClient() (*docker.Client, error) {
128-
d, err := docker.NewClientFromEnv()
129-
if err != nil {
130-
return nil, err
124+
func (c *Config) buildSchedulerMiddlewares(sh *core.Scheduler) {
125+
sh.Use(middlewares.NewSlack(&c.Global.SlackConfig))
126+
sh.Use(middlewares.NewSave(&c.Global.SaveConfig))
127+
sh.Use(middlewares.NewMail(&c.Global.MailConfig))
128+
}
129+
130+
func (c *Config) dockerLabelsUpdate(labels map[string]map[string]string) {
131+
// Get the current labels
132+
var parsedLabelConfig Config
133+
parsedLabelConfig.buildFromDockerLabels(labels)
134+
135+
// Calculate the delta execJobs
136+
for name, j := range c.ExecJobs {
137+
found := false
138+
for newJobsName, newJob := range parsedLabelConfig.ExecJobs {
139+
// Check if the schedule has changed
140+
if name == newJobsName {
141+
found = true
142+
// There is a slight race condition were a job can be canceled / restarted with different params
143+
// so, lets take care of it by simply restarting
144+
// For the hash to work properly, we must fill the fields before calling it
145+
defaults.SetDefaults(newJob)
146+
newJob.Client = c.dockerHandler.GetInternalDockerClient()
147+
newJob.Name = newJobsName
148+
if newJob.Hash() != j.Hash() {
149+
// Remove from the scheduler
150+
c.sh.RemoveJob(j)
151+
// Add the job back to the scheduler
152+
newJob.buildMiddlewares()
153+
c.sh.AddJob(newJob)
154+
// Update the job config
155+
c.ExecJobs[name] = newJob
156+
}
157+
break
158+
}
159+
}
160+
if !found {
161+
// Remove the job
162+
c.sh.RemoveJob(j)
163+
delete(c.ExecJobs, name)
164+
}
131165
}
132166

133-
return d, nil
134-
}
167+
// Check for aditions
168+
for newJobsName, newJob := range parsedLabelConfig.ExecJobs {
169+
found := false
170+
for name := range c.ExecJobs {
171+
if name == newJobsName {
172+
found = true
173+
break
174+
}
175+
}
176+
if !found {
177+
defaults.SetDefaults(newJob)
178+
newJob.Client = c.dockerHandler.GetInternalDockerClient()
179+
newJob.Name = newJobsName
180+
newJob.buildMiddlewares()
181+
c.sh.AddJob(newJob)
182+
c.ExecJobs[newJobsName] = newJob
183+
}
184+
}
135185

136-
func (c *Config) buildLogger() core.Logger {
137-
stdout := logging.NewLogBackend(os.Stdout, "", 0)
138-
// Set the backends to be used.
139-
logging.SetBackend(stdout)
140-
logging.SetFormatter(logging.MustStringFormatter(logFormat))
186+
for name, j := range c.RunJobs {
187+
found := false
188+
for newJobsName, newJob := range parsedLabelConfig.RunJobs {
189+
// Check if the schedule has changed
190+
if name == newJobsName {
191+
found = true
192+
// There is a slight race condition were a job can be canceled / restarted with different params
193+
// so, lets take care of it by simply restarting
194+
// For the hash to work properly, we must fill the fields before calling it
195+
defaults.SetDefaults(newJob)
196+
newJob.Client = c.dockerHandler.GetInternalDockerClient()
197+
newJob.Name = newJobsName
198+
if newJob.Hash() != j.Hash() {
199+
// Remove from the scheduler
200+
c.sh.RemoveJob(j)
201+
// Add the job back to the scheduler
202+
newJob.buildMiddlewares()
203+
c.sh.AddJob(newJob)
204+
// Update the job config
205+
c.RunJobs[name] = newJob
206+
}
207+
break
208+
}
209+
}
210+
if !found {
211+
// Remove the job
212+
c.sh.RemoveJob(j)
213+
delete(c.RunJobs, name)
214+
}
215+
}
141216

142-
return logging.MustGetLogger("ofelia")
143-
}
217+
// Check for aditions
218+
for newJobsName, newJob := range parsedLabelConfig.RunJobs {
219+
found := false
220+
for name := range c.RunJobs {
221+
if name == newJobsName {
222+
found = true
223+
break
224+
}
225+
}
226+
if !found {
227+
defaults.SetDefaults(newJob)
228+
newJob.Client = c.dockerHandler.GetInternalDockerClient()
229+
newJob.Name = newJobsName
230+
newJob.buildMiddlewares()
231+
c.sh.AddJob(newJob)
232+
c.RunJobs[newJobsName] = newJob
233+
}
234+
}
144235

145-
func (c *Config) buildSchedulerMiddlewares(sh *core.Scheduler) {
146-
sh.Use(middlewares.NewSlack(&c.Global.SlackConfig))
147-
sh.Use(middlewares.NewSave(&c.Global.SaveConfig))
148-
sh.Use(middlewares.NewMail(&c.Global.MailConfig))
149236
}
150237

151238
// ExecJobConfig contains all configuration params needed to build a ExecJob

0 commit comments

Comments
 (0)