Skip to content

Commit 26c70c0

Browse files
lacektaraspos
andauthored
Add cli flag for docker container filtering (#212)
* add cli flag for docker container filtering * Tests for docker label filters and a bit of refactoring --------- Co-authored-by: Taras <[email protected]>
1 parent a1bb40e commit 26c70c0

File tree

6 files changed

+222
-15
lines changed

6 files changed

+222
-15
lines changed

README.md

+25
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,31 @@ services:
115115
ofelia.job-exec.datecron.command: "uname -a"
116116
```
117117
118+
**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:
119+
120+
```yaml
121+
version: "3"
122+
services:
123+
ofelia:
124+
image: mcuadros/ofelia:latest
125+
depends_on:
126+
- nginx
127+
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
128+
volumes:
129+
- /var/run/docker.sock:/var/run/docker.sock:ro
130+
labels:
131+
ofelia.job-local.my-test-job.schedule: "@every 5s"
132+
ofelia.job-local.my-test-job.command: "date"
133+
134+
nginx:
135+
image: nginx
136+
labels:
137+
ofelia.enabled: "true"
138+
ofelia.job-exec.datecron.schedule: "@every 5s"
139+
ofelia.job-exec.datecron.command: "uname -a"
140+
```
141+
142+
118143
### Logging
119144
**Ofelia** comes with three different logging drivers:
120145
- `mail` to send mails

cli/config.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ type Config struct {
3636
}
3737

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

4242
d, err := c.buildDockerClient()
4343
if err != nil {
4444
return nil, err
4545
}
4646

47-
labels, err := getLabels(d)
47+
labels, err := getLabels(d, filterFlags)
4848
if err != nil {
4949
return nil, err
5050
}

cli/daemon.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import (
1010

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

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

4243
func (c *DaemonCommand) boot() (err error) {
4344
if c.DockerLabelsConfig {
44-
c.scheduler, err = BuildFromDockerLabels()
45+
c.scheduler, err = BuildFromDockerLabels(c.DockerFilters...)
4546
} else {
4647
c.scheduler, err = BuildFromFile(c.ConfigFile)
4748
}

cli/docker-labels.go

+30-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"encoding/json"
55
"errors"
6+
"fmt"
67
"strings"
78
"time"
89

@@ -18,25 +19,44 @@ const (
1819
serviceLabel = labelPrefix + ".service"
1920
)
2021

21-
func getLabels(d *docker.Client) (map[string]map[string]string, error) {
22+
var (
23+
errNoContainersMatchingFilters = errors.New("no containers matching filters")
24+
errInvalidDockerFilter = errors.New("invalid docker filter")
25+
errFailedToListContainers = errors.New("failed to list containers")
26+
)
27+
28+
func parseFilter(filter string) (key, value string, err error) {
29+
parts := strings.SplitN(filter, "=", 2)
30+
if len(parts) != 2 {
31+
return "", "", errInvalidDockerFilter
32+
}
33+
return parts[0], parts[1], nil
34+
}
35+
36+
func getLabels(d *docker.Client, filterFlags []string) (map[string]map[string]string, error) {
2237
// sleep before querying containers
2338
// because docker not always propagating labels in time
2439
// so ofelia app can't find it's own container
2540
if IsDockerEnv {
2641
time.Sleep(1 * time.Second)
2742
}
2843

29-
conts, err := d.ListContainers(docker.ListContainersOptions{
30-
Filters: map[string][]string{
31-
"label": {requiredLabelFilter},
32-
},
33-
})
34-
if err != nil {
35-
return nil, err
44+
var filters = map[string][]string{
45+
"label": {requiredLabelFilter},
46+
}
47+
for _, f := range filterFlags {
48+
key, value, err := parseFilter(f)
49+
if err != nil {
50+
return nil, fmt.Errorf("%w: %s", err, f)
51+
}
52+
filters[key] = append(filters[key], value)
3653
}
3754

38-
if len(conts) == 0 {
39-
return nil, errors.New("Couldn't find containers with label 'ofelia.enabled=true'")
55+
conts, err := d.ListContainers(docker.ListContainersOptions{Filters: filters})
56+
if err != nil {
57+
return nil, fmt.Errorf("%w: %w", errFailedToListContainers, err)
58+
} else if len(conts) == 0 {
59+
return nil, fmt.Errorf("%w: %v", errNoContainersMatchingFilters, filters)
4060
}
4161

4262
var labels = make(map[string]map[string]string)

cli/docker-labels_test.go

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package cli
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
docker "github.com/fsouza/go-dockerclient"
10+
"github.com/fsouza/go-dockerclient/testing"
11+
"github.com/mcuadros/ofelia/core"
12+
check "gopkg.in/check.v1"
13+
)
14+
15+
var _ = check.Suite(&TestDockerSuit{})
16+
17+
const imageFixture = "ofelia/test-image"
18+
19+
type TestDockerSuit struct {
20+
server *testing.DockerServer
21+
client *docker.Client
22+
}
23+
24+
func (s *TestDockerSuit) SetUpTest(c *check.C) {
25+
var err error
26+
s.server, err = testing.NewServer("127.0.0.1:0", nil, nil)
27+
c.Assert(err, check.IsNil)
28+
29+
s.client, err = docker.NewClient(s.server.URL())
30+
c.Assert(err, check.IsNil)
31+
32+
err = core.BuildTestImage(s.client, imageFixture)
33+
c.Assert(err, check.IsNil)
34+
35+
os.Setenv("DOCKER_HOST", s.server.URL())
36+
}
37+
38+
func (s *TestDockerSuit) TearDownTest(c *check.C) {
39+
os.Unsetenv("DOCKER_HOST")
40+
}
41+
42+
func (s *TestDockerSuit) TestLabelsFilterJobsCount(c *check.C) {
43+
filterLabel := []string{"test_filter_label", "yesssss"}
44+
containersToStartWithLabels := []map[string]string{
45+
{
46+
requiredLabel: "true",
47+
filterLabel[0]: filterLabel[1],
48+
labelPrefix + "." + jobExec + ".job2.schedule": "schedule2",
49+
labelPrefix + "." + jobExec + ".job2.command": "command2",
50+
},
51+
{
52+
requiredLabel: "true",
53+
labelPrefix + "." + jobExec + ".job3.schedule": "schedule3",
54+
labelPrefix + "." + jobExec + ".job3.command": "command3",
55+
},
56+
}
57+
58+
_, err := s.startTestContainersWithLabels(containersToStartWithLabels)
59+
c.Assert(err, check.IsNil)
60+
61+
scheduler, err := BuildFromDockerLabels("label=" + strings.Join(filterLabel, "="))
62+
c.Assert(err, check.IsNil)
63+
c.Assert(scheduler, check.NotNil)
64+
65+
c.Skip("This test will not work until https://github.com/fsouza/go-dockerclient/pull/1031 is merged")
66+
c.Assert(scheduler.Jobs, check.HasLen, 1)
67+
}
68+
69+
func (s *TestDockerSuit) TestFilterErrorsLabel(c *check.C) {
70+
containersToStartWithLabels := []map[string]string{
71+
{
72+
labelPrefix + "." + jobExec + ".job2.schedule": "schedule2",
73+
labelPrefix + "." + jobExec + ".job2.command": "command2",
74+
},
75+
}
76+
77+
_, err := s.startTestContainersWithLabels(containersToStartWithLabels)
78+
c.Assert(err, check.IsNil)
79+
80+
{
81+
scheduler, err := BuildFromDockerLabels()
82+
c.Assert(errors.Is(err, errNoContainersMatchingFilters), check.Equals, true)
83+
c.Assert(strings.Contains(err.Error(), requiredLabelFilter), check.Equals, true)
84+
c.Assert(scheduler, check.IsNil)
85+
}
86+
87+
customLabelFilter := []string{"label", "test=123"}
88+
{
89+
scheduler, err := BuildFromDockerLabels(strings.Join(customLabelFilter, "="))
90+
c.Assert(errors.Is(err, errNoContainersMatchingFilters), check.Equals, true)
91+
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, "label", requiredLabel))
92+
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, customLabelFilter[0], customLabelFilter[1]))
93+
c.Assert(scheduler, check.IsNil)
94+
}
95+
96+
{
97+
customNameFilter := []string{"name", "test-name"}
98+
scheduler, err := BuildFromDockerLabels(strings.Join(customLabelFilter, "="), strings.Join(customNameFilter, "="))
99+
c.Assert(errors.Is(err, errNoContainersMatchingFilters), check.Equals, true)
100+
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, "label", requiredLabel))
101+
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, customLabelFilter[0], customLabelFilter[1]))
102+
c.Assert(err, check.ErrorMatches, fmt.Sprintf(`.*%s:.*%s.*`, customNameFilter[0], customNameFilter[1]))
103+
c.Assert(scheduler, check.IsNil)
104+
}
105+
106+
{
107+
customBadFilter := "label-test"
108+
scheduler, err := BuildFromDockerLabels(customBadFilter)
109+
c.Assert(errors.Is(err, errInvalidDockerFilter), check.Equals, true)
110+
c.Assert(scheduler, check.IsNil)
111+
}
112+
}
113+
114+
func (s *TestDockerSuit) startTestContainersWithLabels(containerLabels []map[string]string) ([]*docker.Container, error) {
115+
containers := []*docker.Container{}
116+
117+
for i := range containerLabels {
118+
cont, err := s.client.CreateContainer(docker.CreateContainerOptions{
119+
Name: fmt.Sprintf("ofelia-test%d", i),
120+
Config: &docker.Config{
121+
Cmd: []string{"sleep", "500"},
122+
Labels: containerLabels[i],
123+
Image: imageFixture,
124+
},
125+
})
126+
if err != nil {
127+
return containers, err
128+
}
129+
130+
containers = append(containers, cont)
131+
if err := s.client.StartContainer(cont.ID, nil); err != nil {
132+
return containers, err
133+
}
134+
}
135+
136+
return containers, nil
137+
}

core/utils.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package core
2+
3+
import (
4+
"archive/tar"
5+
"bytes"
6+
"os"
7+
8+
docker "github.com/fsouza/go-dockerclient"
9+
)
10+
11+
func BuildTestImage(client *docker.Client, name string) error {
12+
var buf bytes.Buffer
13+
tw := tar.NewWriter(&buf)
14+
tw.WriteHeader(&tar.Header{Name: "Dockerfile"})
15+
tw.Write([]byte("FROM alpine\n"))
16+
tw.Close()
17+
18+
return client.BuildImage(docker.BuildImageOptions{
19+
Name: name,
20+
Remote: "github.com/mcuadros/ofelia",
21+
InputStream: &buf,
22+
OutputStream: os.Stdout,
23+
})
24+
}

0 commit comments

Comments
 (0)