Skip to content

Commit

Permalink
enhance: container discovery via labels (crowdsecurity#2959)
Browse files Browse the repository at this point in the history
* wip: attempt to autodiscover via labels

* wip: remove labels dep on docker acquistion

* wip: remove labels dep on docker acquistion

* wip: add debug

* wip: try fix parser maps

* wip: remove redundant pointer

* wip: add debug

* wip: cant type assert

* wip: reinstate debug

* wip: reinstate debug

* wip: reinstate debug

* wip: oops

* wip: add a debug

* wip: fix labels

* wip: remove redundant paramter

* wip: rename config option to be more self declarative

* wip: update log wording

* wip: the if check was not correct

* wip: me lost

* fix: add checks to typecast and log useful information

* add tests for parseLabels

* return nil instead of pointer to empty struct

* simplify EvalContainer return value

---------

Co-authored-by: Sebastien Blot <[email protected]>
  • Loading branch information
LaurenceJJones and blotus authored May 24, 2024
1 parent f06e3e7 commit 9088f31
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 12 deletions.
5 changes: 4 additions & 1 deletion pkg/acquisition/acquisition.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ func LoadAcquisitionFromFile(config *csconfig.CrowdsecServiceCfg, prom *csconfig
log.Debugf("skipping empty item in %s", acquisFile)
continue
}
return nil, fmt.Errorf("missing labels in %s (position: %d)", acquisFile, idx)
if sub.Source != "docker" {
//docker is the only source that can be empty
return nil, fmt.Errorf("missing labels in %s (position: %d)", acquisFile, idx)
}
}
if sub.Source == "" {
return nil, fmt.Errorf("data source type is empty ('source') in %s (position: %d)", acquisFile, idx)
Expand Down
75 changes: 64 additions & 11 deletions pkg/acquisition/modules/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type DockerConfiguration struct {
ContainerID []string `yaml:"container_id"`
ContainerNameRegexp []string `yaml:"container_name_regexp"`
ContainerIDRegexp []string `yaml:"container_id_regexp"`
ForceInotify bool `yaml:"force_inotify"`
UseContainerLabels bool `yaml:"use_container_labels"`
configuration.DataSourceCommonCfg `yaml:",inline"`
}

Expand Down Expand Up @@ -87,10 +87,14 @@ func (d *DockerSource) UnmarshalConfig(yamlConfig []byte) error {
d.logger.Tracef("DockerAcquisition configuration: %+v", d.Config)
}

if len(d.Config.ContainerName) == 0 && len(d.Config.ContainerID) == 0 && len(d.Config.ContainerIDRegexp) == 0 && len(d.Config.ContainerNameRegexp) == 0 {
if len(d.Config.ContainerName) == 0 && len(d.Config.ContainerID) == 0 && len(d.Config.ContainerIDRegexp) == 0 && len(d.Config.ContainerNameRegexp) == 0 && !d.Config.UseContainerLabels {
return fmt.Errorf("no containers names or containers ID configuration provided")
}

if d.Config.UseContainerLabels && (len(d.Config.ContainerName) > 0 || len(d.Config.ContainerID) > 0 || len(d.Config.ContainerIDRegexp) > 0 || len(d.Config.ContainerNameRegexp) > 0) {
return fmt.Errorf("use_container_labels and container_name, container_id, container_id_regexp, container_name_regexp are mutually exclusive")
}

d.CheckIntervalDuration, err = time.ParseDuration(d.Config.CheckInterval)
if err != nil {
return fmt.Errorf("parsing 'check_interval' parameters: %s", d.CheckIntervalDuration)
Expand Down Expand Up @@ -293,7 +297,7 @@ func (d *DockerSource) OneShotAcquisition(out chan types.Event, t *tomb.Tomb) er
d.logger.Debugf("container with id %s is already being read from", container.ID)
continue
}
if containerConfig, ok := d.EvalContainer(container); ok {
if containerConfig := d.EvalContainer(container); containerConfig != nil {
d.logger.Infof("reading logs from container %s", containerConfig.Name)
d.logger.Debugf("logs options: %+v", *d.containerLogsOptions)
dockerReader, err := d.Client.ContainerLogs(context.Background(), containerConfig.ID, *d.containerLogsOptions)
Expand Down Expand Up @@ -375,10 +379,18 @@ func (d *DockerSource) getContainerTTY(containerId string) bool {
return containerDetails.Config.Tty
}

func (d *DockerSource) EvalContainer(container dockerTypes.Container) (*ContainerConfig, bool) {
func (d *DockerSource) getContainerLabels(containerId string) map[string]interface{} {
containerDetails, err := d.Client.ContainerInspect(context.Background(), containerId)
if err != nil {
return map[string]interface{}{}
}
return parseLabels(containerDetails.Config.Labels)
}

func (d *DockerSource) EvalContainer(container dockerTypes.Container) *ContainerConfig {
for _, containerID := range d.Config.ContainerID {
if containerID == container.ID {
return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}, true
return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}
}
}

Expand All @@ -388,28 +400,69 @@ func (d *DockerSource) EvalContainer(container dockerTypes.Container) (*Containe
name = name[1:]
}
if name == containerName {
return &ContainerConfig{ID: container.ID, Name: name, Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}, true
return &ContainerConfig{ID: container.ID, Name: name, Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}
}
}

}

for _, cont := range d.compiledContainerID {
if matched := cont.MatchString(container.ID); matched {
return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}, true
return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}
}
}

for _, cont := range d.compiledContainerName {
for _, name := range container.Names {
if matched := cont.MatchString(name); matched {
return &ContainerConfig{ID: container.ID, Name: name, Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}, true
return &ContainerConfig{ID: container.ID, Name: name, Labels: d.Config.Labels, Tty: d.getContainerTTY(container.ID)}
}
}

}

return &ContainerConfig{}, false
if d.Config.UseContainerLabels {
parsedLabels := d.getContainerLabels(container.ID)
if len(parsedLabels) == 0 {
d.logger.Tracef("container has no 'crowdsec' labels set, ignoring container: %s", container.ID)
return nil
}
if _, ok := parsedLabels["enable"]; !ok {
d.logger.Errorf("container has 'crowdsec' labels set but no 'crowdsec.enable' key found")
return nil
}
enable, ok := parsedLabels["enable"].(string)
if !ok {
d.logger.Error("container has 'crowdsec.enable' label set but it's not a string")
return nil
}
if strings.ToLower(enable) != "true" {
d.logger.Debugf("container has 'crowdsec.enable' label not set to true ignoring container: %s", container.ID)
return nil
}
if _, ok = parsedLabels["labels"]; !ok {
d.logger.Error("container has 'crowdsec.enable' label set to true but no 'labels' keys found")
return nil
}
labelsTypeCast, ok := parsedLabels["labels"].(map[string]interface{})
if !ok {
d.logger.Error("container has 'crowdsec.enable' label set to true but 'labels' is not a map")
return nil
}
d.logger.Debugf("container labels %+v", labelsTypeCast)
labels := make(map[string]string)
for k, v := range labelsTypeCast {
if v, ok := v.(string); ok {
log.Debugf("label %s is a string with value %s", k, v)
labels[k] = v
continue
}
d.logger.Errorf("label %s is not a string", k)
}
return &ContainerConfig{ID: container.ID, Name: container.Names[0], Labels: labels, Tty: d.getContainerTTY(container.ID)}
}

return nil
}

func (d *DockerSource) WatchContainer(monitChan chan *ContainerConfig, deleteChan chan *ContainerConfig) error {
Expand Down Expand Up @@ -449,7 +502,7 @@ func (d *DockerSource) WatchContainer(monitChan chan *ContainerConfig, deleteCha
if _, ok := d.runningContainerState[container.ID]; ok {
continue
}
if containerConfig, ok := d.EvalContainer(container); ok {
if containerConfig := d.EvalContainer(container); containerConfig != nil {
monitChan <- containerConfig
}
}
Expand Down Expand Up @@ -522,7 +575,7 @@ func (d *DockerSource) TailDocker(container *ContainerConfig, outChan chan types
}
l := types.Line{}
l.Raw = line
l.Labels = d.Config.Labels
l.Labels = container.Labels
l.Time = time.Now().UTC()
l.Src = container.Name
l.Process = true
Expand Down
52 changes: 52 additions & 0 deletions pkg/acquisition/modules/docker/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,55 @@ func TestOneShot(t *testing.T) {
}
}
}

func TestParseLabels(t *testing.T) {
tests := []struct {
name string
labels map[string]string
expected map[string]interface{}
}{
{
name: "bad label",
labels: map[string]string{"crowdsecfoo": "bar"},
expected: map[string]interface{}{},
},
{
name: "simple label",
labels: map[string]string{"crowdsec.bar": "baz"},
expected: map[string]interface{}{"bar": "baz"},
},
{
name: "multiple simple labels",
labels: map[string]string{"crowdsec.bar": "baz", "crowdsec.foo": "bar"},
expected: map[string]interface{}{"bar": "baz", "foo": "bar"},
},
{
name: "multiple simple labels 2",
labels: map[string]string{"crowdsec.bar": "baz", "bla": "foo"},
expected: map[string]interface{}{"bar": "baz"},
},
{
name: "end with dot",
labels: map[string]string{"crowdsec.bar.": "baz"},
expected: map[string]interface{}{},
},
{
name: "consecutive dots",
labels: map[string]string{"crowdsec......bar": "baz"},
expected: map[string]interface{}{},
},
{
name: "crowdsec labels",
labels: map[string]string{"crowdsec.labels.type": "nginx"},
expected: map[string]interface{}{"labels": map[string]interface{}{"type": "nginx"}},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
labels := parseLabels(test.labels)
assert.Equal(t, test.expected, labels)
})
}

}
38 changes: 38 additions & 0 deletions pkg/acquisition/modules/docker/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package dockeracquisition

import (
"strings"
)

func parseLabels(labels map[string]string) map[string]interface{} {
result := make(map[string]interface{})
for key, value := range labels {
parseKeyToMap(result, key, value)
}
return result
}

func parseKeyToMap(m map[string]interface{}, key string, value string) {
if !strings.HasPrefix(key, "crowdsec") {
return
}
parts := strings.Split(key, ".")

if len(parts) < 2 || parts[0] != "crowdsec" {
return
}

for i := 0; i < len(parts); i++ {
if parts[i] == "" {
return
}
}

for i := 1; i < len(parts)-1; i++ {
if _, ok := m[parts[i]]; !ok {
m[parts[i]] = make(map[string]interface{})
}
m = m[parts[i]].(map[string]interface{})
}
m[parts[len(parts)-1]] = value
}

0 comments on commit 9088f31

Please sign in to comment.