From f5d58487b428c00135c02ed4a8b0c86cf826e627 Mon Sep 17 00:00:00 2001 From: Wallace Ko Date: Tue, 21 Mar 2023 23:16:49 +0800 Subject: [PATCH 1/2] add cli flag for docker container filtering --- README.md | 25 +++++++++++++++++++++++++ cli/config.go | 4 ++-- cli/daemon.go | 7 ++++--- cli/docker-labels.go | 28 +++++++++++++++++++++++----- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3c45df65..26a05cad 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cli/config.go b/cli/config.go index c60bb2b9..292ee15a 100644 --- a/cli/config.go +++ b/cli/config.go @@ -36,7 +36,7 @@ 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() @@ -44,7 +44,7 @@ func BuildFromDockerLabels() (*core.Scheduler, error) { return nil, err } - labels, err := getLabels(d) + labels, err := getLabels(d, filterFlags) if err != nil { return nil, err } diff --git a/cli/daemon.go b/cli/daemon.go index 491488df..4faeb167 100644 --- a/cli/daemon.go +++ b/cli/daemon.go @@ -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 @@ -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) } diff --git a/cli/docker-labels.go b/cli/docker-labels.go index 9ba796eb..3a3e99a5 100644 --- a/cli/docker-labels.go +++ b/cli/docker-labels.go @@ -18,7 +18,7 @@ const ( serviceLabel = labelPrefix + ".service" ) -func getLabels(d *docker.Client) (map[string]map[string]string, error) { +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 @@ -26,17 +26,35 @@ func getLabels(d *docker.Client) (map[string]map[string]string, error) { time.Sleep(1 * time.Second) } + var filters = map[string][]string{ + "label": {requiredLabelFilter}, + } + for _, f := range filterFlags { + parts := strings.SplitN(f, "=", 2) + if len(parts) != 2 { + return nil, errors.New("invalid docker filter: " + f) + } + key, value := parts[0], parts[1] + values, ok := filters[key] + if ok { + filters[key] = append(values, value) + } else { + filters[key] = []string{value} + } + } + conts, err := d.ListContainers(docker.ListContainersOptions{ - Filters: map[string][]string{ - "label": {requiredLabelFilter}, - }, + Filters: filters, }) if err != nil { return nil, err } if len(conts) == 0 { - return nil, errors.New("Couldn't find containers with label 'ofelia.enabled=true'") + if len(filterFlags) > 0 { + return nil, errors.New("couldn't find containers with label 'ofelia.enabled=true' and additional filters") + } + return nil, errors.New("couldn't find containers with label 'ofelia.enabled=true'") } var labels = make(map[string]map[string]string) From 1287a96bfea1e30d112dd958aba72d6853aa5153 Mon Sep 17 00:00:00 2001 From: Taras Date: Sun, 28 Jan 2024 19:03:36 +0000 Subject: [PATCH 2/2] Tests for docker label filters and a bit of refactoring --- cli/config.go | 2 +- cli/daemon.go | 2 +- cli/docker-labels.go | 44 ++++++------ cli/docker-labels_test.go | 137 ++++++++++++++++++++++++++++++++++++++ core/utils.go | 24 +++++++ 5 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 cli/docker-labels_test.go create mode 100644 core/utils.go diff --git a/cli/config.go b/cli/config.go index 292ee15a..42a5f452 100644 --- a/cli/config.go +++ b/cli/config.go @@ -36,7 +36,7 @@ type Config struct { } // BuildFromDockerLabels builds a scheduler using the config from a docker labels -func BuildFromDockerLabels(filterFlags []string) (*core.Scheduler, error) { +func BuildFromDockerLabels(filterFlags ...string) (*core.Scheduler, error) { c := &Config{} d, err := c.buildDockerClient() diff --git a/cli/daemon.go b/cli/daemon.go index 4faeb167..1ec5ea9f 100644 --- a/cli/daemon.go +++ b/cli/daemon.go @@ -42,7 +42,7 @@ func (c *DaemonCommand) Execute(args []string) error { func (c *DaemonCommand) boot() (err error) { if c.DockerLabelsConfig { - c.scheduler, err = BuildFromDockerLabels(c.DockerFilters) + c.scheduler, err = BuildFromDockerLabels(c.DockerFilters...) } else { c.scheduler, err = BuildFromFile(c.ConfigFile) } diff --git a/cli/docker-labels.go b/cli/docker-labels.go index 3a3e99a5..23a235d8 100644 --- a/cli/docker-labels.go +++ b/cli/docker-labels.go @@ -3,6 +3,7 @@ package cli import ( "encoding/json" "errors" + "fmt" "strings" "time" @@ -18,6 +19,20 @@ const ( serviceLabel = labelPrefix + ".service" ) +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 @@ -30,31 +45,18 @@ func getLabels(d *docker.Client, filterFlags []string) (map[string]map[string]st "label": {requiredLabelFilter}, } for _, f := range filterFlags { - parts := strings.SplitN(f, "=", 2) - if len(parts) != 2 { - return nil, errors.New("invalid docker filter: " + f) - } - key, value := parts[0], parts[1] - values, ok := filters[key] - if ok { - filters[key] = append(values, value) - } else { - filters[key] = []string{value} + key, value, err := parseFilter(f) + if err != nil { + return nil, fmt.Errorf("%w: %s", err, f) } + filters[key] = append(filters[key], value) } - conts, err := d.ListContainers(docker.ListContainersOptions{ - Filters: filters, - }) + conts, err := d.ListContainers(docker.ListContainersOptions{Filters: filters}) if err != nil { - return nil, err - } - - if len(conts) == 0 { - if len(filterFlags) > 0 { - return nil, errors.New("couldn't find containers with label 'ofelia.enabled=true' and additional filters") - } - return nil, errors.New("couldn't find containers with label 'ofelia.enabled=true'") + 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) diff --git a/cli/docker-labels_test.go b/cli/docker-labels_test.go new file mode 100644 index 00000000..8c5049d1 --- /dev/null +++ b/cli/docker-labels_test.go @@ -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 +} diff --git a/core/utils.go b/core/utils.go new file mode 100644 index 00000000..2730417d --- /dev/null +++ b/core/utils.go @@ -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, + }) +}