Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cli flag for docker container filtering #212

Merged
merged 2 commits into from
Jan 28, 2024
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ services:
ofelia.job-exec.datecron.command: "uname -a"
```

**Ofelia** reads labels of all Docker containers for configuration by default. To apply on a subset of containers only, use the flag `--docker-filter` (or `-f`) similar to the [filtering for `docker ps`](https://docs.docker.com/engine/reference/commandline/ps/#filter). E.g. to apply to current docker compose project only using `label` filter:

```yaml
version: "3"
services:
ofelia:
image: mcuadros/ofelia:latest
depends_on:
- nginx
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
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"
```


### 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 @@ -36,15 +36,15 @@ type Config struct {
}

// BuildFromDockerLabels builds a scheduler using the config from a docker labels
func BuildFromDockerLabels() (*core.Scheduler, error) {
func BuildFromDockerLabels(filterFlags ...string) (*core.Scheduler, error) {
c := &Config{}

d, err := c.buildDockerClient()
if err != nil {
return nil, err
}

labels, err := getLabels(d)
labels, err := getLabels(d, filterFlags)
if err != nil {
return nil, err
}
Expand Down
7 changes: 4 additions & 3 deletions cli/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (

// DaemonCommand daemon process
type DaemonCommand struct {
ConfigFile string `long:"config" description:"configuration file" default:"/etc/ofelia.conf"`
DockerLabelsConfig bool `short:"d" long:"docker" description:"read configurations from docker labels"`
ConfigFile string `long:"config" description:"configuration file" default:"/etc/ofelia.conf"`
DockerLabelsConfig bool `short:"d" long:"docker" description:"read configurations from docker labels"`
DockerFilters []string `short:"f" long:"docker-filter" description:"filter to select docker containers"`

config *Config
scheduler *core.Scheduler
Expand Down Expand Up @@ -41,7 +42,7 @@ func (c *DaemonCommand) Execute(args []string) error {

func (c *DaemonCommand) boot() (err error) {
if c.DockerLabelsConfig {
c.scheduler, err = BuildFromDockerLabels()
c.scheduler, err = BuildFromDockerLabels(c.DockerFilters...)
} else {
c.scheduler, err = BuildFromFile(c.ConfigFile)
}
Expand Down
40 changes: 30 additions & 10 deletions cli/docker-labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"

Expand All @@ -18,25 +19,44 @@ const (
serviceLabel = labelPrefix + ".service"
)

func getLabels(d *docker.Client) (map[string]map[string]string, error) {
var (
errNoContainersMatchingFilters = errors.New("no containers matching filters")
errInvalidDockerFilter = errors.New("invalid docker filter")
errFailedToListContainers = errors.New("failed to list containers")
)

func parseFilter(filter string) (key, value string, err error) {
parts := strings.SplitN(filter, "=", 2)
if len(parts) != 2 {
return "", "", errInvalidDockerFilter
}
return parts[0], parts[1], nil
}

func getLabels(d *docker.Client, filterFlags []string) (map[string]map[string]string, error) {
// sleep before querying containers
// because docker not always propagating labels in time
// so ofelia app can't find it's own container
if IsDockerEnv {
time.Sleep(1 * time.Second)
}

conts, err := d.ListContainers(docker.ListContainersOptions{
Filters: map[string][]string{
"label": {requiredLabelFilter},
},
})
if err != nil {
return nil, err
var filters = map[string][]string{
"label": {requiredLabelFilter},
}
for _, f := range filterFlags {
key, value, err := parseFilter(f)
if err != nil {
return nil, fmt.Errorf("%w: %s", err, f)
}
filters[key] = append(filters[key], value)
}

if len(conts) == 0 {
return nil, errors.New("Couldn't find containers with label 'ofelia.enabled=true'")
conts, err := d.ListContainers(docker.ListContainersOptions{Filters: filters})
if err != nil {
return nil, fmt.Errorf("%w: %w", errFailedToListContainers, err)
} else if len(conts) == 0 {
return nil, fmt.Errorf("%w: %v", errNoContainersMatchingFilters, filters)
}

var labels = make(map[string]map[string]string)
Expand Down
137 changes: 137 additions & 0 deletions cli/docker-labels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package cli

import (
"errors"
"fmt"
"os"
"strings"

docker "github.com/fsouza/go-dockerclient"
"github.com/fsouza/go-dockerclient/testing"
"github.com/mcuadros/ofelia/core"
check "gopkg.in/check.v1"
)

var _ = check.Suite(&TestDockerSuit{})

const imageFixture = "ofelia/test-image"

type TestDockerSuit struct {
server *testing.DockerServer
client *docker.Client
}

func (s *TestDockerSuit) SetUpTest(c *check.C) {
var err error
s.server, err = testing.NewServer("127.0.0.1:0", nil, nil)
c.Assert(err, check.IsNil)

s.client, err = docker.NewClient(s.server.URL())
c.Assert(err, check.IsNil)

err = core.BuildTestImage(s.client, imageFixture)
c.Assert(err, check.IsNil)

os.Setenv("DOCKER_HOST", s.server.URL())
}

func (s *TestDockerSuit) TearDownTest(c *check.C) {
os.Unsetenv("DOCKER_HOST")
}

func (s *TestDockerSuit) TestLabelsFilterJobsCount(c *check.C) {
filterLabel := []string{"test_filter_label", "yesssss"}
containersToStartWithLabels := []map[string]string{
{
requiredLabel: "true",
filterLabel[0]: filterLabel[1],
labelPrefix + "." + jobExec + ".job2.schedule": "schedule2",
labelPrefix + "." + jobExec + ".job2.command": "command2",
},
{
requiredLabel: "true",
labelPrefix + "." + jobExec + ".job3.schedule": "schedule3",
labelPrefix + "." + jobExec + ".job3.command": "command3",
},
}

_, err := s.startTestContainersWithLabels(containersToStartWithLabels)
c.Assert(err, check.IsNil)

scheduler, err := BuildFromDockerLabels("label=" + strings.Join(filterLabel, "="))
c.Assert(err, check.IsNil)
c.Assert(scheduler, check.NotNil)

c.Skip("This test will not work until https://github.com/fsouza/go-dockerclient/pull/1031 is merged")
c.Assert(scheduler.Jobs, check.HasLen, 1)
}

func (s *TestDockerSuit) TestFilterErrorsLabel(c *check.C) {
containersToStartWithLabels := []map[string]string{
{
labelPrefix + "." + jobExec + ".job2.schedule": "schedule2",
labelPrefix + "." + jobExec + ".job2.command": "command2",
},
}

_, err := s.startTestContainersWithLabels(containersToStartWithLabels)
c.Assert(err, check.IsNil)

{
scheduler, err := BuildFromDockerLabels()
c.Assert(errors.Is(err, errNoContainersMatchingFilters), check.Equals, true)
c.Assert(strings.Contains(err.Error(), requiredLabelFilter), check.Equals, true)
c.Assert(scheduler, check.IsNil)
}

customLabelFilter := []string{"label", "test=123"}
{
scheduler, err := BuildFromDockerLabels(strings.Join(customLabelFilter, "="))
c.Assert(errors.Is(err, errNoContainersMatchingFilters), check.Equals, true)
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, "label", requiredLabel))
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, customLabelFilter[0], customLabelFilter[1]))
c.Assert(scheduler, check.IsNil)
}

{
customNameFilter := []string{"name", "test-name"}
scheduler, err := BuildFromDockerLabels(strings.Join(customLabelFilter, "="), strings.Join(customNameFilter, "="))
c.Assert(errors.Is(err, errNoContainersMatchingFilters), check.Equals, true)
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, "label", requiredLabel))
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, customLabelFilter[0], customLabelFilter[1]))
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, customNameFilter[0], customNameFilter[1]))
c.Assert(scheduler, check.IsNil)
}

{
customBadFilter := "label-test"
scheduler, err := BuildFromDockerLabels(customBadFilter)
c.Assert(errors.Is(err, errInvalidDockerFilter), check.Equals, true)
c.Assert(scheduler, check.IsNil)
}
}

func (s *TestDockerSuit) startTestContainersWithLabels(containerLabels []map[string]string) ([]*docker.Container, error) {
containers := []*docker.Container{}

for i := range containerLabels {
cont, err := s.client.CreateContainer(docker.CreateContainerOptions{
Name: fmt.Sprintf("ofelia-test%d", i),
Config: &docker.Config{
Cmd: []string{"sleep", "500"},
Labels: containerLabels[i],
Image: imageFixture,
},
})
if err != nil {
return containers, err
}

containers = append(containers, cont)
if err := s.client.StartContainer(cont.ID, nil); err != nil {
return containers, err
}
}

return containers, nil
}
24 changes: 24 additions & 0 deletions core/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package core

import (
"archive/tar"
"bytes"
"os"

docker "github.com/fsouza/go-dockerclient"
)

func BuildTestImage(client *docker.Client, name string) error {
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
tw.WriteHeader(&tar.Header{Name: "Dockerfile"})
tw.Write([]byte("FROM alpine\n"))
tw.Close()

return client.BuildImage(docker.BuildImageOptions{
Name: name,
Remote: "github.com/mcuadros/ofelia",
InputStream: &buf,
OutputStream: os.Stdout,
})
}
Loading