Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.25-alpine AS builder
FROM golang:1.26-alpine AS builder

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

Expand Down
71 changes: 70 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Ofelia - a job scheduler [![GitHub version](https://badge.fury.io/gh/mcuadros%2Fofelia.svg)](https://github.com/mcuadros/ofelia/releases) ![Test](https://github.com/mcuadros/ofelia/workflows/Test/badge.svg)
# Ofelia - a job scheduler [![GitHub version](https://img.shields.io/github/v/release/mcuadros/ofelia?&sort=semver&display_name=release&label=stable&color=green)](https://github.com/mcuadros/ofelia/releases) [![GitHub version](https://img.shields.io/github/v/release/mcuadros/ofelia?include_prereleases&sort=semver&display_name=release&label=edge)](https://github.com/mcuadros/ofelia/releases) ![Test](https://github.com/mcuadros/ofelia/workflows/Test/badge.svg)

<img src="https://weirdspace.dk/FranciscoIbanez/Graphics/Ofelia.gif" align="right" width="180px" height="300px" vspace="20" />

Expand Down Expand Up @@ -144,6 +144,75 @@ services:
```


#### Docker host configuration

By default, **Ofelia** connects to the Docker daemon via the default socket (`/var/run/docker.sock` on Linux). However, you can configure it to connect to a different Docker host using environment variables. This is particularly useful when:
- Using a Docker socket proxy for security
- Connecting to a remote Docker daemon
- Using Docker over TCP

**Ofelia** supports the following Docker environment variables:
- `DOCKER_HOST` - The Docker host to connect to (e.g., `tcp://docker-proxy:2375`, `unix:///custom/docker.sock`)
- `DOCKER_TLS_VERIFY` - Enable TLS verification (set to `1` to enable)
- `DOCKER_CERT_PATH` - Path to TLS certificates directory
- `DOCKER_API_VERSION` - Docker API version to use

##### Using with a socket proxy

```sh
docker run -it --rm \
-e DOCKER_HOST=tcp://docker-proxy:2375 \
--label ofelia.job-local.my-test-job.schedule="@every 5s" \
--label ofelia.job-local.my-test-job.command="date" \
mcuadros/ofelia:latest daemon --docker
```

Or with docker-compose:

```yaml
version: "3"
services:
docker-proxy:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: 1
SERVICES: 1
TASKS: 1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro

ofelia:
image: mcuadros/ofelia:latest
depends_on:
- docker-proxy
- nginx
command: daemon --docker
environment:
DOCKER_HOST: tcp://docker-proxy:2375
labels:
ofelia.job-local.my-test-job.schedule: "@every 5s"
ofelia.job-local.my-test-job.command: "date"

nginx:
image: nginx
labels:
ofelia.enabled: "true"
ofelia.job-exec.datecron.schedule: "@every 5s"
ofelia.job-exec.datecron.command: "uname -a"
```

##### Using with TLS

```sh
docker run -it --rm \
-e DOCKER_HOST=tcp://docker.example.com:2376 \
-e DOCKER_TLS_VERIFY=1 \
-e DOCKER_CERT_PATH=/certs \
-v /path/to/certs:/certs:ro \
mcuadros/ofelia:latest daemon --config=/path/to/config.ini
```


### Logging
**Ofelia** comes with three different logging drivers:
- `mail` to send mails
Expand Down
4 changes: 2 additions & 2 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (c *Config) dockerLabelsUpdate(labels map[string]map[string]string) {
newJob.Client = c.dockerHandler.GetInternalDockerClient()
newJob.Name = newJobsName
if newJob.Hash() != j.Hash() {
c.logger.Debugf("Job %s has changed, restarting", name)
c.logger.Debug("Job has changed, restarting", "job", name)
// Remove from the scheduler
c.sh.RemoveJob(j)
// Add the job back to the scheduler
Expand All @@ -163,7 +163,7 @@ func (c *Config) dockerLabelsUpdate(labels map[string]map[string]string) {
}
}
if !found {
c.logger.Debugf("Job %s is not found, Removing", name)
c.logger.Debug("Job not found, Removing", "job", name)
// Remove the job
c.sh.RemoveJob(j)
delete(c.ExecJobs, name)
Expand Down
9 changes: 4 additions & 5 deletions cli/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ var _ = Suite(&SuiteConfig{})

type TestLogger struct{}

func (*TestLogger) Criticalf(format string, args ...interface{}) {}
func (*TestLogger) Debugf(format string, args ...interface{}) {}
func (*TestLogger) Errorf(format string, args ...interface{}) {}
func (*TestLogger) Noticef(format string, args ...interface{}) {}
func (*TestLogger) Warningf(format string, args ...interface{}) {}
func (*TestLogger) Debug(format string, args ...interface{}) {}
func (*TestLogger) Error(format string, args ...interface{}) {}
func (*TestLogger) Info(format string, args ...interface{}) {}
func (*TestLogger) Warning(format string, args ...interface{}) {}

func (s *SuiteConfig) TestBuildFromString(c *C) {
conf, err := BuildFromString(`
Expand Down
12 changes: 5 additions & 7 deletions cli/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ func (c *DaemonCommand) boot() (err error) {
if !c.DockerLabelConfig {
return fmt.Errorf("can't read the config file: %w", err)
} else {
c.Logger.Debugf("Config file %v not found. Proceeding to read docker labels...", c.ConfigFile)
c.Logger.Debug("Config file not found. Proceeding to read docker labels...", "config", c.ConfigFile)
}
} else {
msg := "Found config file %v"
msg := "Found config file"
if c.DockerLabelConfig {
msg += ". Proceeding to read docker labels as well..."
}
c.Logger.Debugf(msg, c.ConfigFile)
c.Logger.Debug(msg, "config", c.ConfigFile)
}

scheduler := core.NewScheduler(c.Logger)
Expand Down Expand Up @@ -91,9 +91,7 @@ func (c *DaemonCommand) setSignals() {

go func() {
sig := <-c.signals
c.Logger.Warningf(
"Signal received: %s, shutting down the process\n", sig,
)
c.Logger.Warning("Shutting down the process", "signal", sig.String())

c.done <- true
}()
Expand All @@ -105,6 +103,6 @@ func (c *DaemonCommand) shutdown() error {
return nil
}

c.Logger.Warningf("Waiting running jobs.")
c.Logger.Warning("Waiting for running jobs.")
return c.scheduler.Stop()
}
10 changes: 5 additions & 5 deletions cli/docker_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ func (c *DockerHandler) ConfigFromLabelsEnabled() bool {

func (c *DockerHandler) watch() {
const pollInterval = 10 * time.Second
c.logger.Debugf("Watching for Docker labels changes every %s...", pollInterval)
c.logger.Debug("Watching for Docker labels changes...", "interval", pollInterval)
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
for range ticker.C {
labels, err := c.GetDockerLabels()
// Do not print or care if there is no container up right now
if err != nil && !errors.Is(err, errNoContainersMatchingFilters) {
c.logger.Debugf("%v", err)
c.logger.Debug("failed to get Docker labels", "error", err)
}
c.notifier.dockerLabelsUpdate(labels)
}
Expand All @@ -109,20 +109,20 @@ func (c *DockerHandler) WaitForLabels() {

// Check if .dockerenv file exists
if _, err := os.Stat(dockerEnvFile); os.IsNotExist(err) {
c.logger.Debugf(".dockerenv file not found, ofelia is not running in a Docker container")
c.logger.Debug(".dockerenv file not found, ofelia is not running in a Docker container")
return
}

id, err := getContainerID(mountinfoFilePath)
if err != nil {
c.logger.Debugf("Failed to extract ofelia's container ID. Trying with container hostname instead...")
c.logger.Debug("Failed to extract ofelia's container ID. Trying with container hostname instead...")
id, _ = os.Hostname()
}

for attempt := 0; attempt < maxRetries; attempt++ {
_, err := c.dockerClient.InspectContainerWithOptions(docker.InspectContainerOptions{ID: id})
if err == nil {
c.logger.Debugf("Found ofelia container with ID: %s", id)
c.logger.Debug("Found ofelia container", "container_id", id)
return
}

Expand Down
6 changes: 3 additions & 3 deletions cli/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ type ValidateCommand struct {

// Execute runs the validation command
func (c *ValidateCommand) Execute(args []string) error {
c.Logger.Debugf("Validating %q ... ", c.ConfigFile)
c.Logger.Debug("Validating config... ", "config", c.ConfigFile)
config, err := BuildFromFile(c.ConfigFile, c.Logger)
if err != nil {
c.Logger.Errorf("ERROR")
c.Logger.Error("Failed to validate config", "config", c.ConfigFile, "error", err)
return err
}

c.Logger.Noticef("OK. Found %d jobs.", config.JobsCount())
c.Logger.Info("OK", "jobs_count", config.JobsCount())

return nil
}
28 changes: 13 additions & 15 deletions core/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ var (
const (
// maximum size of a stdout/stderr stream to be kept in memory and optional stored/sent via mail
maxStreamSize = 10 * 1024 * 1024
logPrefix = "[Job %q (%s)] %s"
)

type Job interface {
Expand Down Expand Up @@ -115,22 +114,22 @@ func (c *Context) Stop(err error) {
c.Job.NotifyStop()
}

func (c *Context) Log(msg string) {
args := []interface{}{c.Job.GetName(), c.Execution.ID, msg}
func (c *Context) Log(msg string, args ...any) {
defaultArgs := []any{"job", c.Job.GetName(), "execution", c.Execution.ID}

switch {
case c.Execution.Failed:
c.Logger.Errorf(logPrefix, args...)
c.Logger.Error(msg, append(defaultArgs, args...)...)
case c.Execution.Skipped:
c.Logger.Warningf(logPrefix, args...)
c.Logger.Warning(msg, append(defaultArgs, args...)...)
default:
c.Logger.Noticef(logPrefix, args...)
c.Logger.Info(msg, append(defaultArgs, args...)...)
}
}

func (c *Context) Warn(msg string) {
args := []interface{}{c.Job.GetName(), c.Execution.ID, msg}
c.Logger.Warningf(logPrefix, args...)
func (c *Context) Warn(msg string, args ...any) {
defaultArgs := []any{"job", c.Job.GetName(), "execution", c.Execution.ID}
c.Logger.Warning(msg, append(defaultArgs, args...)...)
}

// Execution contains all the information relative to a Job execution.
Expand Down Expand Up @@ -225,11 +224,10 @@ func (c *middlewareContainer) Middlewares() []Middleware {
}

type Logger interface {
Criticalf(format string, args ...interface{})
Debugf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Noticef(format string, args ...interface{})
Warningf(format string, args ...interface{})
Debug(str string, args ...any)
Error(str string, args ...any)
Info(str string, args ...any)
Warning(str string, args ...any)
}

func randomID() string {
Expand All @@ -244,7 +242,7 @@ func randomID() string {
func buildFindLocalImageOptions(image string) docker.ListImagesOptions {
return docker.ListImagesOptions{
Filters: map[string][]string{
"reference": []string{image},
"reference": {image},
},
}
}
Expand Down
9 changes: 4 additions & 5 deletions core/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,10 @@ func (j *TestJob) Run(ctx *Context) error {

type TestLogger struct{}

func (*TestLogger) Criticalf(format string, args ...interface{}) {}
func (*TestLogger) Debugf(format string, args ...interface{}) {}
func (*TestLogger) Errorf(format string, args ...interface{}) {}
func (*TestLogger) Noticef(format string, args ...interface{}) {}
func (*TestLogger) Warningf(format string, args ...interface{}) {}
func (*TestLogger) Debug(format string, args ...any) {}
func (*TestLogger) Error(format string, args ...any) {}
func (*TestLogger) Info(format string, args ...any) {}
func (*TestLogger) Warning(format string, args ...any) {}

func (s *SuiteCommon) TestParseRegistry(c *C) {
c.Assert(parseRegistry("example.com:port/dir/image"), Equals, "example.com:port")
Expand Down
23 changes: 3 additions & 20 deletions core/cron_utils.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package core

import (
"log/slog"
)

// Implement the cron logger interface
type CronUtils struct {
Logger Logger
Expand All @@ -13,23 +9,10 @@ func NewCronUtils(l Logger) *CronUtils {
return &CronUtils{Logger: l}
}

func formatKeysAndValues(keysAndValues ...interface{}) string {
r := slog.Record{}
r.Add(keysAndValues...)

attrs := []slog.Attr{}
r.Attrs(func(a slog.Attr) bool {
attrs = append(attrs, a)
return true
})

return slog.GroupValue(attrs...).String()
}

func (c *CronUtils) Info(msg string, keysAndValues ...interface{}) {
c.Logger.Debugf("%v", formatKeysAndValues(append([]interface{}{"cron", msg}, keysAndValues...)...))
func (c *CronUtils) Info(msg string, keysAndValues ...any) {
c.Logger.Debug("cron update", append(keysAndValues, "cron", msg)...)
}

func (c *CronUtils) Error(err error, msg string, keysAndValues ...interface{}) {
c.Logger.Errorf("%v", formatKeysAndValues(append([]interface{}{"cron", msg, "error", err}, keysAndValues...)...))
c.Logger.Error("cron error", append(keysAndValues, "cron", msg, "error", err)...)
}
Comment thread
ma-04 marked this conversation as resolved.
50 changes: 50 additions & 0 deletions core/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package core

import (
"io"
"log/slog"
"os"
"strconv"

"github.com/lmittmann/tint"
)

type SlogLogger struct {
logger *slog.Logger
}

func NewSlogLogger(w io.Writer) Logger {
var logLevel slog.Level
l := os.Getenv("LOG_LEVEL")
if err := logLevel.UnmarshalText([]byte(l)); err != nil {
logLevel = slog.LevelInfo
}

return &SlogLogger{
logger: slog.New(tint.NewHandler(w, &tint.Options{
Level: logLevel,
AddSource: false,
NoColor: func() bool {
res, _ := strconv.ParseBool(os.Getenv("LOG_NO_COLOR"))
return res
}(),
})),
}

}

func (l *SlogLogger) Debug(str string, args ...any) {
l.logger.Debug(str, args...)
}

func (l *SlogLogger) Error(str string, args ...any) {
l.logger.Error(str, args...)
}

func (l *SlogLogger) Info(str string, args ...any) {
l.logger.Info(str, args...)
}

func (l *SlogLogger) Warning(str string, args ...any) {
l.logger.Warn(str, args...)
}
Loading