From b772f2aa452afed1b8397b44313d874c69ea62ab Mon Sep 17 00:00:00 2001 From: Chris Mark Date: Wed, 27 May 2020 16:33:50 +0300 Subject: [PATCH] Add k8s keystore backend (#18096) --- CHANGELOG.next.asciidoc | 1 + deploy/kubernetes/metricbeat-kubernetes.yaml | 1 + .../metricbeat/metricbeat-role.yaml | 1 + filebeat/autodiscover/builder/hints/logs.go | 8 +- .../autodiscover/builder/hints/monitors.go | 8 +- .../autodiscover/appenders/config/config.go | 2 +- libbeat/autodiscover/autodiscover.go | 1 - libbeat/autodiscover/builder.go | 44 +++-- libbeat/autodiscover/builder_test.go | 6 +- .../autodiscover/providers/docker/docker.go | 10 +- .../autodiscover/providers/jolokia/jolokia.go | 10 +- .../providers/kubernetes/config_test.go | 4 +- .../providers/kubernetes/kubernetes.go | 11 +- .../providers/kubernetes/node_test.go | 10 +- .../providers/kubernetes/pod_test.go | 11 +- .../providers/kubernetes/service_test.go | 10 +- libbeat/autodiscover/template/config.go | 60 ++++--- libbeat/autodiscover/template/config_test.go | 81 +++++++++- .../k8skeystore/kubernetes_keystore.go | 150 ++++++++++++++++++ .../k8skeystore/kubernetes_keystore_test.go | 66 ++++++++ libbeat/docs/shared-autodiscover.asciidoc | 16 -- libbeat/keystore/keystore.go | 8 +- .../autodiscover/builder/hints/metrics.go | 9 +- .../builder/hints/metrics_test.go | 2 +- metricbeat/docs/autodiscover-hints.asciidoc | 3 +- .../autodiscover-kubernetes-config.asciidoc | 70 +++++++- .../providers/aws/ec2/provider.go | 4 +- .../providers/aws/elb/provider.go | 4 +- 28 files changed, 484 insertions(+), 127 deletions(-) create mode 100644 libbeat/common/kubernetes/k8skeystore/kubernetes_keystore.go create mode 100644 libbeat/common/kubernetes/k8skeystore/kubernetes_keystore_test.go diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 91c108dc815..07d411ecc1b 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -243,6 +243,7 @@ field. You can revert this change by configuring tags for the module and omittin - Add support for AWS IAM `role_arn` in credentials config. {pull}17658[17658] {issue}12464[12464] - Add keystore support for autodiscover static configurations. {pull]16306[16306] - Add Kerberos support to Elasticsearch output. {pull}17927[17927] +- Add k8s keystore backend. {pull}18096[18096] - Add support for fixed length extraction in `dissect` processor. {pull}17191[17191] - Set `agent.name` to the hostname by default. {issue}16377[16377] {pull}18000[18000] - Add support for basic ECS logging. {pull}17974[17974] diff --git a/deploy/kubernetes/metricbeat-kubernetes.yaml b/deploy/kubernetes/metricbeat-kubernetes.yaml index 179f33089e1..216bbc24f9f 100644 --- a/deploy/kubernetes/metricbeat-kubernetes.yaml +++ b/deploy/kubernetes/metricbeat-kubernetes.yaml @@ -335,6 +335,7 @@ rules: - namespaces - events - pods + - secrets verbs: ["get", "list", "watch"] - apiGroups: ["extensions"] resources: diff --git a/deploy/kubernetes/metricbeat/metricbeat-role.yaml b/deploy/kubernetes/metricbeat/metricbeat-role.yaml index 55cea94fb3c..ba9bff7c28f 100644 --- a/deploy/kubernetes/metricbeat/metricbeat-role.yaml +++ b/deploy/kubernetes/metricbeat/metricbeat-role.yaml @@ -11,6 +11,7 @@ rules: - namespaces - events - pods + - secrets verbs: ["get", "list", "watch"] - apiGroups: ["extensions"] resources: diff --git a/filebeat/autodiscover/builder/hints/logs.go b/filebeat/autodiscover/builder/hints/logs.go index e2f37caee74..70758a8a028 100644 --- a/filebeat/autodiscover/builder/hints/logs.go +++ b/filebeat/autodiscover/builder/hints/logs.go @@ -21,6 +21,8 @@ import ( "fmt" "regexp" + "github.com/elastic/go-ucfg" + "github.com/elastic/beats/v7/filebeat/fileset" "github.com/elastic/beats/v7/filebeat/harvester" "github.com/elastic/beats/v7/libbeat/autodiscover" @@ -70,7 +72,7 @@ func NewLogHints(cfg *common.Config) (autodiscover.Builder, error) { } // Create config based on input hints in the bus event -func (l *logHints) CreateConfig(event bus.Event) []*common.Config { +func (l *logHints) CreateConfig(event bus.Event, options ...ucfg.Option) []*common.Config { var hints common.MapStr hIface, ok := event["hints"] if ok { @@ -109,7 +111,7 @@ func (l *logHints) CreateConfig(event bus.Event) []*common.Config { } logp.Debug("hints.builder", "generated config %+v", configs) // Apply information in event to the template to generate the final config - return template.ApplyConfigTemplate(event, configs, false) + return template.ApplyConfigTemplate(event, configs) } tempCfg := common.MapStr{} @@ -163,7 +165,7 @@ func (l *logHints) CreateConfig(event bus.Event) []*common.Config { logp.Debug("hints.builder", "generated config %+v", config) // Apply information in event to the template to generate the final config - return template.ApplyConfigTemplate(event, []*common.Config{config}, false) + return template.ApplyConfigTemplate(event, []*common.Config{config}) } func (l *logHints) getMultiline(hints common.MapStr) common.MapStr { diff --git a/heartbeat/autodiscover/builder/hints/monitors.go b/heartbeat/autodiscover/builder/hints/monitors.go index 836b5a9326c..f9fe8847d3e 100644 --- a/heartbeat/autodiscover/builder/hints/monitors.go +++ b/heartbeat/autodiscover/builder/hints/monitors.go @@ -23,6 +23,8 @@ import ( "strconv" "strings" + "github.com/elastic/go-ucfg" + "github.com/elastic/beats/v7/libbeat/autodiscover" "github.com/elastic/beats/v7/libbeat/autodiscover/builder" "github.com/elastic/beats/v7/libbeat/autodiscover/template" @@ -60,7 +62,7 @@ func NewHeartbeatHints(cfg *common.Config) (autodiscover.Builder, error) { } // Create config based on input hints in the bus event -func (hb *heartbeatHints) CreateConfig(event bus.Event) []*common.Config { +func (hb *heartbeatHints) CreateConfig(event bus.Event, options ...ucfg.Option) []*common.Config { var hints common.MapStr hIface, ok := event["hints"] if ok { @@ -91,7 +93,7 @@ func (hb *heartbeatHints) CreateConfig(event bus.Event) []*common.Config { } hb.logger.Debugf("generated config %+v", configs) // Apply information in event to the template to generate the final config - return template.ApplyConfigTemplate(event, configs, false) + return template.ApplyConfigTemplate(event, configs) } tempCfg := common.MapStr{} @@ -121,7 +123,7 @@ func (hb *heartbeatHints) CreateConfig(event bus.Event) []*common.Config { } // Apply information in event to the template to generate the final config - return template.ApplyConfigTemplate(event, configs, false) + return template.ApplyConfigTemplate(event, configs) } func (hb *heartbeatHints) getType(hints common.MapStr) common.MapStr { diff --git a/libbeat/autodiscover/appenders/config/config.go b/libbeat/autodiscover/appenders/config/config.go index 60f8a543f4a..018ee1b587d 100644 --- a/libbeat/autodiscover/appenders/config/config.go +++ b/libbeat/autodiscover/appenders/config/config.go @@ -104,7 +104,7 @@ func (c *configAppender) Append(event bus.Event) { } // Apply the template - template.ApplyConfigTemplate(event, cfgs, false) + template.ApplyConfigTemplate(event, cfgs) } // Replace old config with newly appended configs diff --git a/libbeat/autodiscover/autodiscover.go b/libbeat/autodiscover/autodiscover.go index fe209e722ea..974beb7253a 100644 --- a/libbeat/autodiscover/autodiscover.go +++ b/libbeat/autodiscover/autodiscover.go @@ -191,7 +191,6 @@ func (a *Autodiscover) handleStart(event bus.Event) bool { } if a.logger.IsDebug() { - for _, c := range configs { a.logger.Debugf("Generated config: %+v", common.DebugString(c, true)) } diff --git a/libbeat/autodiscover/builder.go b/libbeat/autodiscover/builder.go index b77ef847a97..ba091b46c4e 100644 --- a/libbeat/autodiscover/builder.go +++ b/libbeat/autodiscover/builder.go @@ -22,18 +22,25 @@ import ( "fmt" "strings" + "github.com/elastic/go-ucfg" + "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/libbeat/common/bus" + "github.com/elastic/beats/v7/libbeat/keystore" ) // Builder provides an interface by which configs can be built from provider metadata type Builder interface { // CreateConfig creates a config from hints passed from providers - CreateConfig(event bus.Event) []*common.Config + CreateConfig(event bus.Event, options ...ucfg.Option) []*common.Config } -// Builders is a list of Builder objects -type Builders []Builder +// builders is a struct of Builder list objects and a `keystoreProvider`, which +// has access to a keystores registry +type Builders struct { + builders []Builder + keystoreProvider keystore.Provider +} // BuilderConstructor is a func used to generate a Builder object type BuilderConstructor func(*common.Config) (Builder, error) @@ -89,9 +96,18 @@ func (r *registry) BuildBuilder(c *common.Config) (Builder, error) { // GetConfig creates configs for all builders initialized. func (b Builders) GetConfig(event bus.Event) []*common.Config { configs := []*common.Config{} - - for _, builder := range b { - if config := builder.CreateConfig(event); config != nil { + var opts []ucfg.Option + + if b.keystoreProvider != nil { + k8sKeystore := b.keystoreProvider.GetKeystore(event) + if k8sKeystore != nil { + opts = []ucfg.Option{ + ucfg.Resolve(keystore.ResolverWrap(k8sKeystore)), + } + } + } + for _, builder := range b.builders { + if config := builder.CreateConfig(event, opts...); config != nil { configs = append(configs, config...) } } @@ -100,12 +116,16 @@ func (b Builders) GetConfig(event bus.Event) []*common.Config { } // NewBuilders instances the given list of builders. hintsCfg holds `hints` settings -// for simplified mode (single 'hints' builder) -func NewBuilders(bConfigs []*common.Config, hintsCfg *common.Config) (Builders, error) { +// for simplified mode (single 'hints' builder), `keystoreProvider` has access to keystore registry +func NewBuilders( + bConfigs []*common.Config, + hintsCfg *common.Config, + keystoreProvider keystore.Provider, +) (Builders, error) { var builders Builders if hintsCfg.Enabled() { if len(bConfigs) > 0 { - return nil, errors.New("hints.enabled is incompatible with manually defining builders") + return Builders{}, errors.New("hints.enabled is incompatible with manually defining builders") } // pass rest of hints settings to the builder @@ -116,10 +136,10 @@ func NewBuilders(bConfigs []*common.Config, hintsCfg *common.Config) (Builders, for _, bcfg := range bConfigs { builder, err := Registry.BuildBuilder(bcfg) if err != nil { - return nil, err + return Builders{}, err } - builders = append(builders, builder) + builders.builders = append(builders.builders, builder) } - + builders.keystoreProvider = keystoreProvider return builders, nil } diff --git a/libbeat/autodiscover/builder_test.go b/libbeat/autodiscover/builder_test.go index 75cc0dafaeb..c5e08a77e39 100644 --- a/libbeat/autodiscover/builder_test.go +++ b/libbeat/autodiscover/builder_test.go @@ -22,13 +22,15 @@ import ( "github.com/stretchr/testify/assert" + "github.com/elastic/go-ucfg" + "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/libbeat/common/bus" ) type fakeBuilder struct{} -func (f *fakeBuilder) CreateConfig(event bus.Event) []*common.Config { +func (f *fakeBuilder) CreateConfig(event bus.Event, options ...ucfg.Option) []*common.Config { return []*common.Config{common.NewConfig()} } @@ -65,7 +67,7 @@ func TestBuilderRegistry(t *testing.T) { assert.Equal(t, len(res), 1) builders := Builders{} - builders = append(builders, builder) + builders.builders = append(builders.builders, builder) // Try using builders object for the same as above and expect // the same result diff --git a/libbeat/autodiscover/providers/docker/docker.go b/libbeat/autodiscover/providers/docker/docker.go index 9bfa13000b1..553b981177e 100644 --- a/libbeat/autodiscover/providers/docker/docker.go +++ b/libbeat/autodiscover/providers/docker/docker.go @@ -56,7 +56,6 @@ type Provider struct { stoppers map[string]*time.Timer stopTrigger chan *dockerContainerMetadata logger *logp.Logger - keystore keystore.Keystore } // AutodiscoverBuilder builds and returns an autodiscover provider @@ -78,15 +77,15 @@ func AutodiscoverBuilder(bus bus.Bus, uuid uuid.UUID, c *common.Config, keystore return nil, errWrap(err) } - mapper, err := template.NewConfigMapper(config.Templates) + mapper, err := template.NewConfigMapper(config.Templates, keystore, nil) if err != nil { return nil, errWrap(err) } - if len(mapper) == 0 && !config.Hints.Enabled() { + if len(mapper.ConditionMaps) == 0 && !config.Hints.Enabled() { return nil, errWrap(fmt.Errorf("no configs or hints defined for autodiscover provider")) } - builders, err := autodiscover.NewBuilders(config.Builders, config.Hints) + builders, err := autodiscover.NewBuilders(config.Builders, config.Hints, nil) if err != nil { return nil, errWrap(err) } @@ -117,7 +116,6 @@ func AutodiscoverBuilder(bus bus.Bus, uuid uuid.UUID, c *common.Config, keystore stoppers: make(map[string]*time.Timer), stopTrigger: make(chan *dockerContainerMetadata), logger: logger, - keystore: keystore, }, nil } @@ -306,8 +304,6 @@ func (d *Provider) emitContainer(container *docker.Container, meta *dockerMetada } func (d *Provider) publish(event bus.Event) { - // attach keystore to the event to be consumed by the static configs - event["keystore"] = d.keystore // Try to match a config if config := d.templates.GetConfig(event); config != nil { event["config"] = config diff --git a/libbeat/autodiscover/providers/jolokia/jolokia.go b/libbeat/autodiscover/providers/jolokia/jolokia.go index 4a18ffffec9..5a8876a011a 100644 --- a/libbeat/autodiscover/providers/jolokia/jolokia.go +++ b/libbeat/autodiscover/providers/jolokia/jolokia.go @@ -49,7 +49,6 @@ type Provider struct { appenders autodiscover.Appenders templates template.Mapper discovery DiscoveryProber - keystore keystore.Keystore } // AutodiscoverBuilder builds a Jolokia Discovery autodiscover provider, it fails if @@ -70,15 +69,15 @@ func AutodiscoverBuilder(bus bus.Bus, uuid uuid.UUID, c *common.Config, keystore Interfaces: config.Interfaces, } - mapper, err := template.NewConfigMapper(config.Templates) + mapper, err := template.NewConfigMapper(config.Templates, keystore, nil) if err != nil { return nil, errWrap(err) } - if len(mapper) == 0 { + if len(mapper.ConditionMaps) == 0 { return nil, errWrap(fmt.Errorf("no configs defined for autodiscover provider")) } - builders, err := autodiscover.NewBuilders(config.Builders, nil) + builders, err := autodiscover.NewBuilders(config.Builders, nil, nil) if err != nil { return nil, errWrap(err) } @@ -94,7 +93,6 @@ func AutodiscoverBuilder(bus bus.Bus, uuid uuid.UUID, c *common.Config, keystore builders: builders, appenders: appenders, discovery: discovery, - keystore: keystore, }, nil } @@ -109,8 +107,6 @@ func (p *Provider) Start() { } func (p *Provider) publish(event bus.Event) { - // attach keystore to the event to be consumed by the static configs - event["keystore"] = p.keystore if config := p.templates.GetConfig(event); config != nil { event["config"] = config } else if config := p.builders.GetConfig(event); config != nil { diff --git a/libbeat/autodiscover/providers/kubernetes/config_test.go b/libbeat/autodiscover/providers/kubernetes/config_test.go index 55fd601037d..0cbe2d14ef2 100644 --- a/libbeat/autodiscover/providers/kubernetes/config_test.go +++ b/libbeat/autodiscover/providers/kubernetes/config_test.go @@ -22,6 +22,8 @@ import ( "github.com/stretchr/testify/assert" + "github.com/elastic/go-ucfg" + "github.com/elastic/beats/v7/libbeat/autodiscover" "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/libbeat/common/bus" @@ -76,6 +78,6 @@ func newMockBuilder(_ *common.Config) (autodiscover.Builder, error) { return &mockBuilder{}, nil } -func (m *mockBuilder) CreateConfig(event bus.Event) []*common.Config { +func (m *mockBuilder) CreateConfig(event bus.Event, options ...ucfg.Option) []*common.Config { return nil } diff --git a/libbeat/autodiscover/providers/kubernetes/kubernetes.go b/libbeat/autodiscover/providers/kubernetes/kubernetes.go index 4a4a4566f8e..e1a2cb02ee0 100644 --- a/libbeat/autodiscover/providers/kubernetes/kubernetes.go +++ b/libbeat/autodiscover/providers/kubernetes/kubernetes.go @@ -30,6 +30,7 @@ import ( "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/libbeat/common/bus" "github.com/elastic/beats/v7/libbeat/common/kubernetes" + "github.com/elastic/beats/v7/libbeat/common/kubernetes/k8skeystore" "github.com/elastic/beats/v7/libbeat/keystore" "github.com/elastic/beats/v7/libbeat/logp" ) @@ -55,7 +56,6 @@ type Provider struct { appenders autodiscover.Appenders logger *logp.Logger eventer Eventer - keystore keystore.Keystore } // AutodiscoverBuilder builds and returns an autodiscover provider @@ -77,12 +77,14 @@ func AutodiscoverBuilder(bus bus.Bus, uuid uuid.UUID, c *common.Config, keystore return nil, errWrap(err) } - mapper, err := template.NewConfigMapper(config.Templates) + k8sKeystoreProvider := k8skeystore.NewKubernetesKeystoresRegistry(logger, client) + + mapper, err := template.NewConfigMapper(config.Templates, keystore, k8sKeystoreProvider) if err != nil { return nil, errWrap(err) } - builders, err := autodiscover.NewBuilders(config.Builders, config.Hints) + builders, err := autodiscover.NewBuilders(config.Builders, config.Hints, k8sKeystoreProvider) if err != nil { return nil, errWrap(err) } @@ -99,7 +101,6 @@ func AutodiscoverBuilder(bus bus.Bus, uuid uuid.UUID, c *common.Config, keystore builders: builders, appenders: appenders, logger: logger, - keystore: keystore, } switch config.Resource { @@ -138,8 +139,6 @@ func (p *Provider) String() string { } func (p *Provider) publish(event bus.Event) { - // attach keystore to the event to be consumed by the static configs - event["keystore"] = p.keystore // Try to match a config if config := p.templates.GetConfig(event); config != nil { event["config"] = config diff --git a/libbeat/autodiscover/providers/kubernetes/node_test.go b/libbeat/autodiscover/providers/kubernetes/node_test.go index f2fbe78dba6..59fb67ada7d 100644 --- a/libbeat/autodiscover/providers/kubernetes/node_test.go +++ b/libbeat/autodiscover/providers/kubernetes/node_test.go @@ -21,8 +21,6 @@ import ( "testing" "time" - "github.com/elastic/beats/v7/libbeat/common/kubernetes/metadata" - "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" @@ -33,7 +31,7 @@ import ( "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/libbeat/common/bus" "github.com/elastic/beats/v7/libbeat/common/kubernetes" - "github.com/elastic/beats/v7/libbeat/keystore" + "github.com/elastic/beats/v7/libbeat/common/kubernetes/metadata" "github.com/elastic/beats/v7/libbeat/logp" ) @@ -113,7 +111,6 @@ func TestGenerateHints_Node(t *testing.T) { } func TestEmitEvent_Node(t *testing.T) { - k, _ := keystore.NewFileKeystore("test") name := "metricbeat" nodeIP := "192.168.0.1" uid := "005f3b90-4b9d-12f8-acf0-31020a840133" @@ -162,7 +159,6 @@ func TestEmitEvent_Node(t *testing.T) { "host": "192.168.0.1", "id": uid, "provider": UUID, - "keystore": k, "kubernetes": common.MapStr{ "node": common.MapStr{ "name": "metricbeat", @@ -222,7 +218,6 @@ func TestEmitEvent_Node(t *testing.T) { "host": "", "id": uid, "provider": UUID, - "keystore": k, "kubernetes": common.MapStr{ "node": common.MapStr{ "name": "metricbeat", @@ -245,7 +240,7 @@ func TestEmitEvent_Node(t *testing.T) { for _, test := range tests { t.Run(test.Message, func(t *testing.T) { - mapper, err := template.NewConfigMapper(nil) + mapper, err := template.NewConfigMapper(nil, nil, nil) if err != nil { t.Fatal(err) } @@ -256,7 +251,6 @@ func TestEmitEvent_Node(t *testing.T) { bus: bus.New(logp.NewLogger("bus"), "test"), templates: mapper, logger: logp.NewLogger("kubernetes"), - keystore: k, } no := &node{ diff --git a/libbeat/autodiscover/providers/kubernetes/pod_test.go b/libbeat/autodiscover/providers/kubernetes/pod_test.go index 05b50987b2e..ce537523167 100644 --- a/libbeat/autodiscover/providers/kubernetes/pod_test.go +++ b/libbeat/autodiscover/providers/kubernetes/pod_test.go @@ -21,8 +21,6 @@ import ( "testing" "time" - "github.com/elastic/beats/v7/libbeat/common/kubernetes/metadata" - "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" @@ -33,7 +31,7 @@ import ( "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/libbeat/common/bus" "github.com/elastic/beats/v7/libbeat/common/kubernetes" - "github.com/elastic/beats/v7/libbeat/keystore" + "github.com/elastic/beats/v7/libbeat/common/kubernetes/metadata" "github.com/elastic/beats/v7/libbeat/logp" ) @@ -333,7 +331,6 @@ func TestGenerateHints(t *testing.T) { } func TestEmitEvent(t *testing.T) { - k, _ := keystore.NewFileKeystore("test") name := "filebeat" namespace := "default" podIP := "127.0.0.1" @@ -397,7 +394,6 @@ func TestEmitEvent(t *testing.T) { "host": "127.0.0.1", "id": cid, "provider": UUID, - "keystore": k, "kubernetes": common.MapStr{ "container": common.MapStr{ "id": "foobar", @@ -530,7 +526,6 @@ func TestEmitEvent(t *testing.T) { "host": "", "id": cid, "provider": UUID, - "keystore": k, "kubernetes": common.MapStr{ "container": common.MapStr{ "id": "", @@ -600,7 +595,6 @@ func TestEmitEvent(t *testing.T) { "host": "127.0.0.1", "id": cid, "provider": UUID, - "keystore": k, "kubernetes": common.MapStr{ "container": common.MapStr{ "id": "", @@ -639,7 +633,7 @@ func TestEmitEvent(t *testing.T) { for _, test := range tests { t.Run(test.Message, func(t *testing.T) { - mapper, err := template.NewConfigMapper(nil) + mapper, err := template.NewConfigMapper(nil, nil, nil) if err != nil { t.Fatal(err) } @@ -650,7 +644,6 @@ func TestEmitEvent(t *testing.T) { bus: bus.New(logp.NewLogger("bus"), "test"), templates: mapper, logger: logp.NewLogger("kubernetes"), - keystore: k, } pod := &pod{ diff --git a/libbeat/autodiscover/providers/kubernetes/service_test.go b/libbeat/autodiscover/providers/kubernetes/service_test.go index 0e3c8ddb0a8..7ead61fc3a0 100644 --- a/libbeat/autodiscover/providers/kubernetes/service_test.go +++ b/libbeat/autodiscover/providers/kubernetes/service_test.go @@ -21,8 +21,6 @@ import ( "testing" "time" - "github.com/elastic/beats/v7/libbeat/common/kubernetes/metadata" - "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" @@ -33,7 +31,7 @@ import ( "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/libbeat/common/bus" "github.com/elastic/beats/v7/libbeat/common/kubernetes" - "github.com/elastic/beats/v7/libbeat/keystore" + "github.com/elastic/beats/v7/libbeat/common/kubernetes/metadata" "github.com/elastic/beats/v7/libbeat/logp" ) @@ -234,7 +232,6 @@ func TestGenerateHints_Service(t *testing.T) { } func TestEmitEvent_Service(t *testing.T) { - k, _ := keystore.NewFileKeystore("test") name := "metricbeat" namespace := "default" clusterIP := "192.168.0.1" @@ -282,7 +279,6 @@ func TestEmitEvent_Service(t *testing.T) { "host": "192.168.0.1", "id": uid, "provider": UUID, - "keystore": k, "port": 8080, "kubernetes": common.MapStr{ "service": common.MapStr{ @@ -372,7 +368,6 @@ func TestEmitEvent_Service(t *testing.T) { "id": uid, "port": 8080, "provider": UUID, - "keystore": k, "kubernetes": common.MapStr{ "service": common.MapStr{ "name": "metricbeat", @@ -397,7 +392,7 @@ func TestEmitEvent_Service(t *testing.T) { for _, test := range tests { t.Run(test.Message, func(t *testing.T) { - mapper, err := template.NewConfigMapper(nil) + mapper, err := template.NewConfigMapper(nil, nil, nil) if err != nil { t.Fatal(err) } @@ -409,7 +404,6 @@ func TestEmitEvent_Service(t *testing.T) { bus: bus.New(logp.NewLogger("bus"), "test"), templates: mapper, logger: logp.NewLogger("kubernetes"), - keystore: k, } service := &service{ diff --git a/libbeat/autodiscover/template/config.go b/libbeat/autodiscover/template/config.go index 0ce05526ecb..a34cec10444 100644 --- a/libbeat/autodiscover/template/config.go +++ b/libbeat/autodiscover/template/config.go @@ -18,17 +18,24 @@ package template import ( + "github.com/elastic/go-ucfg" + "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/libbeat/common/bus" "github.com/elastic/beats/v7/libbeat/conditions" "github.com/elastic/beats/v7/libbeat/keystore" "github.com/elastic/beats/v7/libbeat/logp" - "github.com/elastic/go-ucfg" ) -// Mapper maps config templates with conditions, if a match happens on a discover event -// the given template will be used as config -type Mapper []*ConditionMap +// Mapper maps config templates with conditions in ConditionMaps, if a match happens on a discover event +// the given template will be used as config. +// Mapper also includes the global Keystore object at `keystore` and `keystoreProvider`, which +// has access to a keystores registry +type Mapper struct { + ConditionMaps []*ConditionMap + keystore keystore.Keystore + keystoreProvider keystore.Provider +} // ConditionMap maps a condition to the configs to use when it's triggered type ConditionMap struct { @@ -43,19 +50,24 @@ type MapperSettings []*struct { } // NewConfigMapper builds a template Mapper from given settings -func NewConfigMapper(configs MapperSettings) (mapper Mapper, err error) { +func NewConfigMapper( + configs MapperSettings, + keystore keystore.Keystore, + keystoreProvider keystore.Provider, +) (mapper Mapper, err error) { for _, c := range configs { condMap := &ConditionMap{Configs: c.Configs} if c.ConditionConfig != nil { condMap.Condition, err = conditions.NewCondition(c.ConditionConfig) if err != nil { - return nil, err + return Mapper{}, err } } - - mapper = append(mapper, condMap) + mapper.ConditionMaps = append(mapper.ConditionMaps, condMap) } + mapper.keystore = keystore + mapper.keystoreProvider = keystoreProvider return mapper, nil } @@ -74,15 +86,26 @@ func (e Event) GetValue(key string) (interface{}, error) { // GetConfig returns a matching Config if any, nil otherwise func (c Mapper) GetConfig(event bus.Event) []*common.Config { var result []*common.Config - - for _, mapping := range c { + opts := []ucfg.Option{} + // add k8s keystore in options list with higher priority + if c.keystoreProvider != nil { + k8sKeystore := c.keystoreProvider.GetKeystore(event) + if k8sKeystore != nil { + opts = append(opts, ucfg.Resolve(keystore.ResolverWrap(k8sKeystore))) + } + } + // add local keystore in options list with lower priority + if c.keystore != nil { + opts = append(opts, ucfg.Resolve(keystore.ResolverWrap(c.keystore))) + } + for _, mapping := range c.ConditionMaps { // An empty condition matches everything conditionOk := mapping.Condition == nil || mapping.Condition.Check(Event(event)) if mapping.Configs != nil && !conditionOk { continue } - configs := ApplyConfigTemplate(event, mapping.Configs, true) + configs := ApplyConfigTemplate(event, mapping.Configs, opts...) if configs != nil { result = append(result, configs...) } @@ -91,7 +114,7 @@ func (c Mapper) GetConfig(event bus.Event) []*common.Config { } // ApplyConfigTemplate takes a set of templated configs and applys information in an event map -func ApplyConfigTemplate(event bus.Event, configs []*common.Config, keystoreEnabled bool) []*common.Config { +func ApplyConfigTemplate(event bus.Event, configs []*common.Config, options ...ucfg.Option) []*common.Config { var result []*common.Config // unpack input vars, err := ucfg.NewFrom(map[string]interface{}{ @@ -106,18 +129,7 @@ func ApplyConfigTemplate(event bus.Event, configs []*common.Config, keystoreEnab ucfg.ResolveEnv, ucfg.VarExp, } - - if keystoreEnabled { - if val, ok := event["keystore"]; ok { - eventKeystore := val.(keystore.Keystore) - opts = append(opts, ucfg.Resolve(keystore.ResolverWrap(eventKeystore))) - delete(event, "keystore") - } - } else { - if _, ok := event["keystore"]; ok { - delete(event, "keystore") - } - } + opts = append(opts, options...) for _, config := range configs { c, err := ucfg.NewFrom(config, opts...) diff --git a/libbeat/autodiscover/template/config_test.go b/libbeat/autodiscover/template/config_test.go index ccb27a7127a..933c4fbb1c8 100644 --- a/libbeat/autodiscover/template/config_test.go +++ b/libbeat/autodiscover/template/config_test.go @@ -87,7 +87,7 @@ func TestConfigsMapping(t *testing.T) { t.Fatal(err) } - mapper, err := NewConfigMapper(mappings) + mapper, err := NewConfigMapper(mappings, nil, nil) if err != nil { t.Fatal(err) } @@ -124,8 +124,7 @@ func TestConfigsMappingKeystore(t *testing.T) { - correct: config password: "${PASSWORD}"`, event: bus.Event{ - "foo": 3, - "keystore": keystore, + "foo": 3, }, expected: []*common.Config{config}, }, @@ -142,7 +141,7 @@ func TestConfigsMappingKeystore(t *testing.T) { t.Fatal(err) } - mapper, err := NewConfigMapper(mappings) + mapper, err := NewConfigMapper(mappings, keystore, nil) if err != nil { t.Fatal(err) } @@ -152,6 +151,78 @@ func TestConfigsMappingKeystore(t *testing.T) { } } +func TestConfigsMappingKeystoreProvider(t *testing.T) { + secret := "mapping_provider_secret" + //expected config + config, _ := common.NewConfigFrom(map[string]interface{}{ + "correct": "config", + "password": secret, + }) + + path := getTemporaryKeystoreFile() + defer os.Remove(path) + // store the secret + keystore := createAnExistingKeystore(path, secret) + + tests := []struct { + mapping string + event bus.Event + expected []*common.Config + }{ + // Match config + { + mapping: ` +- condition.equals: + foo: 3 + config: + - correct: config + password: "${PASSWORD}"`, + event: bus.Event{ + "foo": 3, + }, + expected: []*common.Config{config}, + }, + } + + keystoreProvider := newMockKeystoreProvider(secret) + for _, test := range tests { + var mappings MapperSettings + config, err := common.NewConfigWithYAML([]byte(test.mapping), "") + if err != nil { + t.Fatal(err) + } + + if err := config.Unpack(&mappings); err != nil { + t.Fatal(err) + } + + mapper, err := NewConfigMapper(mappings, keystore, keystoreProvider) + if err != nil { + t.Fatal(err) + } + + res := mapper.GetConfig(test.event) + assert.Equal(t, test.expected, res) + } +} + +type mockKeystore struct { + secret string +} + +func newMockKeystoreProvider(secret string) keystore.Provider { + return &mockKeystore{secret} +} + +// GetKeystore return a KubernetesSecretsKeystore if it already exists for a given namespace or creates a new one. +func (kr *mockKeystore) GetKeystore(event bus.Event) keystore.Keystore { + path := getTemporaryKeystoreFile() + defer os.Remove(path) + // store the secret + keystore := createAnExistingKeystore(path, kr.secret) + return keystore +} + func TestNilConditionConfig(t *testing.T) { var mappings MapperSettings data := ` @@ -166,7 +237,7 @@ func TestNilConditionConfig(t *testing.T) { t.Fatal(err) } - _, err = NewConfigMapper(mappings) + _, err = NewConfigMapper(mappings, nil, nil) assert.NoError(t, err) assert.Nil(t, mappings[0].ConditionConfig) } diff --git a/libbeat/common/kubernetes/k8skeystore/kubernetes_keystore.go b/libbeat/common/kubernetes/k8skeystore/kubernetes_keystore.go new file mode 100644 index 00000000000..23a2c3fcf6c --- /dev/null +++ b/libbeat/common/kubernetes/k8skeystore/kubernetes_keystore.go @@ -0,0 +1,150 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package k8skeystore + +import ( + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8s "k8s.io/client-go/kubernetes" + + "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/libbeat/common/bus" + "github.com/elastic/beats/v7/libbeat/keystore" + "github.com/elastic/beats/v7/libbeat/logp" +) + +type KubernetesKeystores map[string]keystore.Keystore + +// KubernetesKeystoresRegistry holds KubernetesKeystores for known namespaces. Once a Keystore for one k8s namespace +// is initialized it will be reused every time it is needed. +type KubernetesKeystoresRegistry struct { + kubernetesKeystores KubernetesKeystores + logger *logp.Logger + client k8s.Interface +} + +// KubernetesSecretsKeystore allows to retrieve passwords from Kubernetes secrets for a given namespace +type KubernetesSecretsKeystore struct { + namespace string + client k8s.Interface + logger *logp.Logger +} + +// Factoryk8s Create the right keystore with the configured options +func Factoryk8s(keystoreNamespace string, ks8client k8s.Interface, logger *logp.Logger) (keystore.Keystore, error) { + keystore, err := NewKubernetesSecretsKeystore(keystoreNamespace, ks8client, logger) + return keystore, err +} + +// NewKubernetesKeystoresRegistry initializes a KubernetesKeystoresRegistry +func NewKubernetesKeystoresRegistry(logger *logp.Logger, client k8s.Interface) keystore.Provider { + return &KubernetesKeystoresRegistry{ + kubernetesKeystores: KubernetesKeystores{}, + logger: logger, + client: client, + } +} + +// GetKeystore return a KubernetesSecretsKeystore if it already exists for a given namespace or creates a new one. +func (kr *KubernetesKeystoresRegistry) GetKeystore(event bus.Event) keystore.Keystore { + namespace := "" + if val, ok := event["kubernetes"]; ok { + kubernetesMeta := val.(common.MapStr) + ns, err := kubernetesMeta.GetValue("namespace") + if err != nil { + kr.logger.Debugf("Cannot retrieve kubernetes namespace from event: %s", event) + return nil + } + namespace = ns.(string) + } + if namespace != "" { + // either retrieve already stored keystore or create a new one for the namespace + if storedKeystore, ok := kr.kubernetesKeystores[namespace]; ok { + return storedKeystore + } + k8sKeystore, _ := Factoryk8s(namespace, kr.client, kr.logger) + kr.kubernetesKeystores["namespace"] = k8sKeystore + return k8sKeystore + } + kr.logger.Debugf("Cannot retrieve kubernetes namespace from event: %s", event) + return nil +} + +// NewKubernetesSecretsKeystore returns an new k8s Keystore +func NewKubernetesSecretsKeystore(keystoreNamespace string, ks8client k8s.Interface, logger *logp.Logger) (keystore.Keystore, error) { + keystore := KubernetesSecretsKeystore{ + namespace: keystoreNamespace, + client: ks8client, + logger: logger, + } + return &keystore, nil +} + +// Retrieve return a SecureString instance that will contains both the key and the secret. +func (k *KubernetesSecretsKeystore) Retrieve(key string) (*keystore.SecureString, error) { + // key = "kubernetes.somenamespace.somesecret.value" + tokens := strings.Split(key, ".") + if len(tokens) != 4 { + k.logger.Debugf( + "not valid secret key: %v. Secrets should be of the following format %v", + key, + "kubernetes.somenamespace.somesecret.value", + ) + return nil, keystore.ErrKeyDoesntExists + } + ns := tokens[1] + secretName := tokens[2] + secretVar := tokens[3] + if ns != k.namespace { + k.logger.Debugf("cannot access Kubernetes secrets from a different namespace (%v) than: %v", ns, k.namespace) + return nil, keystore.ErrKeyDoesntExists + } + secretIntefrace := k.client.CoreV1().Secrets(ns) + secrets, err := secretIntefrace.List(metav1.ListOptions{}) + if err != nil { + k.logger.Errorf("Could not retrieve secrets from k8s API: %v", err) + return nil, keystore.ErrKeyDoesntExists + } + if len(secrets.Items) == 0 { + k.logger.Debugf("no secrets found for namespace: %v", ns) + return nil, keystore.ErrKeyDoesntExists + } + secret, err := secretIntefrace.Get(secretName, metav1.GetOptions{}) + if err != nil { + k.logger.Errorf("Could not retrieve secret from k8s API: %v", err) + return nil, keystore.ErrKeyDoesntExists + } + if _, ok := secret.Data[secretVar]; !ok { + k.logger.Errorf("Could not retrieve value %v for secret %v", secretVar, secretName) + return nil, keystore.ErrKeyDoesntExists + } + secretString := secret.Data[secretVar] + return keystore.NewSecureString(secretString), nil +} + +// GetConfig returns common.Config representation of the key / secret pair to be merged with other +// loaded configuration. +func (k *KubernetesSecretsKeystore) GetConfig() (*common.Config, error) { + return nil, nil +} + +// IsPersisted return if the keystore is physically persisted on disk. +func (k *KubernetesSecretsKeystore) IsPersisted() bool { + return true +} diff --git a/libbeat/common/kubernetes/k8skeystore/kubernetes_keystore_test.go b/libbeat/common/kubernetes/k8skeystore/kubernetes_keystore_test.go new file mode 100644 index 00000000000..60041bc0a08 --- /dev/null +++ b/libbeat/common/kubernetes/k8skeystore/kubernetes_keystore_test.go @@ -0,0 +1,66 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package k8skeystore + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/libbeat/common/bus" +) + +func TestGetKeystore(t *testing.T) { + kRegistry := NewKubernetesKeystoresRegistry(nil, nil) + k1 := kRegistry.GetKeystore(bus.Event{"kubernetes": common.MapStr{"namespace": "my_namespace"}}) + k2 := kRegistry.GetKeystore(bus.Event{"kubernetes": common.MapStr{"namespace": "my_namespace"}}) + assert.Equal(t, k1, k2) + k3 := kRegistry.GetKeystore(bus.Event{"kubernetes": common.MapStr{"namespace": "my_namespace_2"}}) + assert.NotEqual(t, k2, k3) +} + +// TODO: upgrade client dependency and use fake client to test retrieve +//func TestGetKeystoreAndRetrieve(t *testing.T) { +// client := k8sfake.NewSimpleClientset() +// ns := "test_namespace" +// pass := "testing_passpass" +// secret := &v1.Secret{ +// TypeMeta: metav1.TypeMeta{ +// Kind: "Secret", +// APIVersion: "apps/v1beta1", +// }, +// ObjectMeta: metav1.ObjectMeta{ +// Name: "testing_secret", +// Namespace: ns, +// }, +// Data: map[string][]byte{ +// "secret_value": []byte(pass), +// }, +// } +// client.CoreV1().Secrets(ns).Create(context.TODO(), secret, metav1.CreateOptions{}) +// +// kRegistry := NewKubernetesKeystoresRegistry(nil, nil) +// k1 := kRegistry.GetKeystore(bus.Event{"kubernetes": common.MapStr{"namespace": ns}}) +// key := "kubernetes.test_namespace.testing_secret.secret_value" +// secretVal, err := k1.Retrieve(key) +// if err != nil { +// t.Fatalf("could not retrive k8s secret", err) +// } +// assert.Equal(t, pass, secretVal) +//} diff --git a/libbeat/docs/shared-autodiscover.asciidoc b/libbeat/docs/shared-autodiscover.asciidoc index 5279b6f3ea0..5da68216b0e 100644 --- a/libbeat/docs/shared-autodiscover.asciidoc +++ b/libbeat/docs/shared-autodiscover.asciidoc @@ -244,22 +244,6 @@ running configuration for a container, 60s by default. include::../../{beatname_lc}/docs/autodiscover-kubernetes-config.asciidoc[] -[float] -===== Manually Defining Ports with Kubernetes - -Declare exposed ports in your pod spec if possible. Otherwise, you will need to use -multiple templates with complex filtering rules. The {port} variable will not be -present, and you will need to hardcode ports. Example: `{data.host}:1234` - -When ports are not declared, Autodiscover generates a config using your provided -template once per pod, and once per container. These generated configs are -de-duplicated after they are generated. If the generated configs for multiple -containers are identical, they will be merged into one config. - -Pods share an identical host. If only the `{data.host}` variable is interpolated, -then one config will be generated per host. The configs will be identical. -After they are de-duplicated, only one will be used. - ifdef::autodiscoverJolokia[] [float] ===== Jolokia diff --git a/libbeat/keystore/keystore.go b/libbeat/keystore/keystore.go index 340b83eb416..2dbf7ff144f 100644 --- a/libbeat/keystore/keystore.go +++ b/libbeat/keystore/keystore.go @@ -21,7 +21,8 @@ import ( "errors" "github.com/elastic/beats/v7/libbeat/common" - ucfg "github.com/elastic/go-ucfg" + "github.com/elastic/beats/v7/libbeat/common/bus" + "github.com/elastic/go-ucfg" "github.com/elastic/go-ucfg/parse" ) @@ -73,6 +74,11 @@ type ListingKeystore interface { List() ([]string, error) } +// Provider for keystore +type Provider interface { + GetKeystore(event bus.Event) Keystore +} + // ResolverWrap wrap a config resolver around an existing keystore. func ResolverWrap(keystore Keystore) func(string) (string, parse.Config, error) { return func(keyName string) (string, parse.Config, error) { diff --git a/metricbeat/autodiscover/builder/hints/metrics.go b/metricbeat/autodiscover/builder/hints/metrics.go index 1647fb9fbc7..b366789ba27 100644 --- a/metricbeat/autodiscover/builder/hints/metrics.go +++ b/metricbeat/autodiscover/builder/hints/metrics.go @@ -19,9 +19,10 @@ package hints import ( "fmt" - "strings" + "github.com/elastic/go-ucfg" + "github.com/elastic/beats/v7/libbeat/autodiscover" "github.com/elastic/beats/v7/libbeat/autodiscover/builder" "github.com/elastic/beats/v7/libbeat/autodiscover/template" @@ -69,7 +70,7 @@ func NewMetricHints(cfg *common.Config) (autodiscover.Builder, error) { } // Create configs based on hints passed from providers -func (m *metricHints) CreateConfig(event bus.Event) []*common.Config { +func (m *metricHints) CreateConfig(event bus.Event, options ...ucfg.Option) []*common.Config { var config []*common.Config host, _ := event["host"].(string) if host == "" { @@ -94,7 +95,7 @@ func (m *metricHints) CreateConfig(event bus.Event) []*common.Config { } logp.Debug("hints.builder", "generated config %+v", configs) // Apply information in event to the template to generate the final config - return template.ApplyConfigTemplate(event, configs, false) + return template.ApplyConfigTemplate(event, configs, options...) } @@ -155,7 +156,7 @@ func (m *metricHints) CreateConfig(event bus.Event) []*common.Config { // Apply information in event to the template to generate the final config // This especially helps in a scenario where endpoints are configured as: // co.elastic.metrics/hosts= "${data.host}:9090" - return template.ApplyConfigTemplate(event, config, false) + return template.ApplyConfigTemplate(event, config, options...) } func (m *metricHints) getModule(hints common.MapStr) string { diff --git a/metricbeat/autodiscover/builder/hints/metrics_test.go b/metricbeat/autodiscover/builder/hints/metrics_test.go index 4b3f7e0430b..a6dddb6b7a1 100644 --- a/metricbeat/autodiscover/builder/hints/metrics_test.go +++ b/metricbeat/autodiscover/builder/hints/metrics_test.go @@ -328,7 +328,7 @@ func TestGenerateHints(t *testing.T) { } } -func TestGenerateHintsDoesNotAccessKeystore(t *testing.T) { +func TestGenerateHintsDoesNotAccessGlobalKeystore(t *testing.T) { path := getTemporaryKeystoreFile() defer os.Remove(path) // store the secret diff --git a/metricbeat/docs/autodiscover-hints.asciidoc b/metricbeat/docs/autodiscover-hints.asciidoc index a34b623bd36..25029a61d05 100644 --- a/metricbeat/docs/autodiscover-hints.asciidoc +++ b/metricbeat/docs/autodiscover-hints.asciidoc @@ -45,7 +45,8 @@ The username to use for authentication The password to use for authentication. It is recommended to retrieve this sensitive information from an ENV variable and avoid placing passwords in plain text. Unlike static autodiscover configuration, hints based autodiscover has -no access to the keystore of Metricbeat since it could be a potential security issue. +no access to the keystore of Metricbeat since it could be a potential security issue. However hints based autodiscover +can make use of Kuberentes Secrets as described in <>. [float] ===== `co.elastic.metrics/ssl.*` diff --git a/metricbeat/docs/autodiscover-kubernetes-config.asciidoc b/metricbeat/docs/autodiscover-kubernetes-config.asciidoc index f3e6a74cdfb..a18993dd3cd 100644 --- a/metricbeat/docs/autodiscover-kubernetes-config.asciidoc +++ b/metricbeat/docs/autodiscover-kubernetes-config.asciidoc @@ -18,7 +18,29 @@ metricbeat.autodiscover: This configuration launches a `prometheus` module for all containers of pods annotated `prometheus.io/scrape=true`. -Also Metricbeat autodiscover supports leveraging <> in order to retrieve sensitive data like passwords. +[float] +===== Manually Defining Ports with Kubernetes + +Declare exposed ports in your pod spec if possible. Otherwise, you will need to use +multiple templates with complex filtering rules. The {port} variable will not be +present, and you will need to hardcode ports. Example: `{data.host}:1234` + +When ports are not declared, Autodiscover generates a config using your provided +template once per pod, and once per container. These generated configs are +de-duplicated after they are generated. If the generated configs for multiple +containers are identical, they will be merged into one config. + +Pods share an identical host. If only the `{data.host}` variable is interpolated, +then one config will be generated per host. The configs will be identical. +After they are de-duplicated, only one will be used. + +[float] +[[kubernetes-secrets]] +===== Metricbeat Autodiscover Secret Management + +[float] +====== Local Keystore +Metricbeat autodiscover supports leveraging <> in order to retrieve sensitive data like passwords. Here is an example of how a configuration using keystore would look like: ["source","yaml",subs="attributes"] @@ -38,3 +60,49 @@ metricbeat.autodiscover: ------------------------------------------------------------------------------------- where `REDIS_PASSWORD` is a key stored in local keystore of Metricbeat. + +[float] +===== Kubernetes Secrets +Metricbeat autodiscover supports leveraging https://kubernetes.io/docs/concepts/configuration/secret/[Kubernetes secrets] +in order to retrieve sensitive data like passwords. +Here is an example of how a configuration using Kubernetes secrets would look like: + +["source","yaml",subs="attributes"] +------------------------------------------------------------------------------------- +metricbeat.autodiscover: + providers: + - type: kubernetes + templates: + - condition: + contains: + kubernetes.labels.app: "redis" + config: + - module: redis + metricsets: ["info", "keyspace"] + hosts: "${data.host}:6379" + password: "${kubernetes.default.somesecret.value}" +------------------------------------------------------------------------------------- + +where `kubernetes.default.somesecret.value` specifies a key stored as Kubernetes secret as following: + +. Kubernetes Namespace: `default` +. Kubernetes Secret Name: `somesecret` +. Secret Data Key: `value` + +This secret can be created in a Kubernetes environment using the following the command: +["source","yaml",subs="attributes"] +------------------------------------------------------------------------------------- +cat << EOF | kubectl apply -f - +apiVersion: v1 +kind: Secret +metadata: + name: somesecret +type: Opaque +data: + value: $(echo -n "passpass" | base64) +EOF +------------------------------------------------------------------------------------- + + +Note that Pods can only consume secrets that belong to the same Kubernetes namespace. For instance if Pod `my-redis` +is running under `staging` namespace, it cannot access a secret under `testing` namespace for example `kubernetes.testing.xxx.yyy`. diff --git a/x-pack/libbeat/autodiscover/providers/aws/ec2/provider.go b/x-pack/libbeat/autodiscover/providers/aws/ec2/provider.go index 4d457c46a8c..06b153626fe 100644 --- a/x-pack/libbeat/autodiscover/providers/aws/ec2/provider.go +++ b/x-pack/libbeat/autodiscover/providers/aws/ec2/provider.go @@ -34,7 +34,6 @@ type Provider struct { stopListener bus.Listener watcher *watcher uuid uuid.UUID - keystore keystore.Keystore } // AutodiscoverBuilder is the main builder for this provider. @@ -86,7 +85,7 @@ func AutodiscoverBuilder(bus bus.Bus, uuid uuid.UUID, c *common.Config, keystore // internalBuilder is mainly intended for testing via mocks and stubs. // it can be configured to use a fetcher that doesn't actually hit the AWS API. func internalBuilder(uuid uuid.UUID, bus bus.Bus, config *awsauto.Config, fetcher fetcher, keystore keystore.Keystore) (*Provider, error) { - mapper, err := template.NewConfigMapper(config.Templates) + mapper, err := template.NewConfigMapper(config.Templates, keystore, nil) if err != nil { return nil, err } @@ -96,7 +95,6 @@ func internalBuilder(uuid uuid.UUID, bus bus.Bus, config *awsauto.Config, fetche bus: bus, templates: &mapper, uuid: uuid, - keystore: keystore, } p.watcher = newWatcher( diff --git a/x-pack/libbeat/autodiscover/providers/aws/elb/provider.go b/x-pack/libbeat/autodiscover/providers/aws/elb/provider.go index 522b5ba9a4f..54021a8db88 100644 --- a/x-pack/libbeat/autodiscover/providers/aws/elb/provider.go +++ b/x-pack/libbeat/autodiscover/providers/aws/elb/provider.go @@ -36,7 +36,6 @@ type Provider struct { stopListener bus.Listener watcher *watcher uuid uuid.UUID - keystore keystore.Keystore } // AutodiscoverBuilder is the main builder for this provider. @@ -93,7 +92,7 @@ func AutodiscoverBuilder(bus bus.Bus, uuid uuid.UUID, c *common.Config, keystore // internalBuilder is mainly intended for testing via mocks and stubs. // it can be configured to use a fetcher that doesn't actually hit the AWS API. func internalBuilder(uuid uuid.UUID, bus bus.Bus, config *awsauto.Config, fetcher fetcher, keystore keystore.Keystore) (*Provider, error) { - mapper, err := template.NewConfigMapper(config.Templates) + mapper, err := template.NewConfigMapper(config.Templates, keystore, nil) if err != nil { return nil, err } @@ -103,7 +102,6 @@ func internalBuilder(uuid uuid.UUID, bus bus.Bus, config *awsauto.Config, fetche bus: bus, templates: &mapper, uuid: uuid, - keystore: keystore, } p.watcher = newWatcher(