diff --git a/x-pack/elastic-agent/CHANGELOG.next.asciidoc b/x-pack/elastic-agent/CHANGELOG.next.asciidoc index 4e306f889a7c..d000ab1e3b56 100644 --- a/x-pack/elastic-agent/CHANGELOG.next.asciidoc +++ b/x-pack/elastic-agent/CHANGELOG.next.asciidoc @@ -78,4 +78,5 @@ - Add TLS support for Fleet Server {pull}24142[24142] - Add support for Fleet Server running under Elastic Agent {pull}24220[24220] - Add CA support to Elastic Agent docker image {pull}24486[24486] +- Add k8s secrets provider for Agent {pull}24789[24789] - Add STATE_PATH, CONFIG_PATH, LOGS_PATH to Elastic Agent docker image {pull}24817[24817] diff --git a/x-pack/elastic-agent/fleet.yml.lock b/x-pack/elastic-agent/fleet.yml.lock new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/x-pack/elastic-agent/pkg/agent/application/pipeline/emitter/controller.go b/x-pack/elastic-agent/pkg/agent/application/pipeline/emitter/controller.go index 557883dd8f99..d396385c4887 100644 --- a/x-pack/elastic-agent/pkg/agent/application/pipeline/emitter/controller.go +++ b/x-pack/elastic-agent/pkg/agent/application/pipeline/emitter/controller.go @@ -50,7 +50,7 @@ func NewController( caps capabilities.Capability, reloadables ...reloadable, ) *Controller { - init, _ := transpiler.NewVars(map[string]interface{}{}) + init, _ := transpiler.NewVars(map[string]interface{}{}, nil) return &Controller{ logger: log, diff --git a/x-pack/elastic-agent/pkg/agent/cmd/include.go b/x-pack/elastic-agent/pkg/agent/cmd/include.go index b955c85418ff..5bc763c6df06 100644 --- a/x-pack/elastic-agent/pkg/agent/cmd/include.go +++ b/x-pack/elastic-agent/pkg/agent/cmd/include.go @@ -11,6 +11,7 @@ import ( _ "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable/providers/env" _ "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable/providers/host" _ "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable/providers/kubernetes" + _ "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets" _ "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable/providers/local" _ "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable/providers/localdynamic" _ "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable/providers/path" diff --git a/x-pack/elastic-agent/pkg/agent/transpiler/ast_test.go b/x-pack/elastic-agent/pkg/agent/transpiler/ast_test.go index 4c56b993e648..46565756d8f9 100644 --- a/x-pack/elastic-agent/pkg/agent/transpiler/ast_test.go +++ b/x-pack/elastic-agent/pkg/agent/transpiler/ast_test.go @@ -1616,7 +1616,7 @@ func TestHash(t *testing.T) { } func mustMakeVars(mapping map[string]interface{}) *Vars { - v, err := NewVars(mapping) + v, err := NewVars(mapping, nil) if err != nil { panic(err) } diff --git a/x-pack/elastic-agent/pkg/agent/transpiler/utils_test.go b/x-pack/elastic-agent/pkg/agent/transpiler/utils_test.go index f94c87f64997..0de58a56d730 100644 --- a/x-pack/elastic-agent/pkg/agent/transpiler/utils_test.go +++ b/x-pack/elastic-agent/pkg/agent/transpiler/utils_test.go @@ -737,7 +737,7 @@ func TestRenderInputs(t *testing.T) { } func mustMakeVarsP(mapping map[string]interface{}, processorKey string, processors Processors) *Vars { - v, err := NewVarsWithProcessors(mapping, processorKey, processors) + v, err := NewVarsWithProcessors(mapping, processorKey, processors, nil) if err != nil { panic(err) } diff --git a/x-pack/elastic-agent/pkg/agent/transpiler/vars.go b/x-pack/elastic-agent/pkg/agent/transpiler/vars.go index 811c5ee95774..e1818cdd160e 100644 --- a/x-pack/elastic-agent/pkg/agent/transpiler/vars.go +++ b/x-pack/elastic-agent/pkg/agent/transpiler/vars.go @@ -9,6 +9,10 @@ import ( "regexp" "strings" "unicode" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" + + "github.com/elastic/beats/v7/libbeat/common" ) var varsRegex = regexp.MustCompile(`\${([\p{L}\d\s\\\-_|.'"]*)}`) @@ -18,23 +22,24 @@ var ErrNoMatch = fmt.Errorf("no matching vars") // Vars is a context of variables that also contain a list of processors that go with the mapping. type Vars struct { - tree *AST - processorsKey string - processors Processors + tree *AST + processorsKey string + processors Processors + fetchContextProviders common.MapStr } // NewVars returns a new instance of vars. -func NewVars(mapping map[string]interface{}) (*Vars, error) { - return NewVarsWithProcessors(mapping, "", nil) +func NewVars(mapping map[string]interface{}, fetchContextProviders common.MapStr) (*Vars, error) { + return NewVarsWithProcessors(mapping, "", nil, fetchContextProviders) } // NewVarsWithProcessors returns a new instance of vars with attachment of processors. -func NewVarsWithProcessors(mapping map[string]interface{}, processorKey string, processors Processors) (*Vars, error) { +func NewVarsWithProcessors(mapping map[string]interface{}, processorKey string, processors Processors, fetchContextProviders common.MapStr) (*Vars, error) { tree, err := NewAST(mapping) if err != nil { return nil, err } - return &Vars{tree, processorKey, processors}, nil + return &Vars{tree, processorKey, processors, fetchContextProviders}, nil } // Replace returns a new value based on variable replacement. @@ -44,7 +49,6 @@ func (v *Vars) Replace(value string) (Node, error) { if !validBrackets(value, matchIdxs) { return nil, fmt.Errorf("starting ${ is missing ending }") } - result := "" lastIndex := 0 for _, r := range matchIdxs { @@ -60,7 +64,7 @@ func (v *Vars) Replace(value string) (Node, error) { result += value[lastIndex:r[0]] + val.Value() set = true case *varString: - node, ok := Lookup(v.tree, val.Value()) + node, ok := v.lookupNode(val.Value()) if ok { node := nodeToValue(node) if v.processorsKey != "" && varPrefixMatched(val.Value(), v.processorsKey) { @@ -90,9 +94,29 @@ func (v *Vars) Replace(value string) (Node, error) { // Lookup returns the value from the vars. func (v *Vars) Lookup(name string) (interface{}, bool) { + // lookup in the AST tree return v.tree.Lookup(name) } +// lookupNode performs a lookup on the AST, but keeps the result as a `Node`. +// +// This is different from `Lookup` which returns the actual type, not the AST type. +func (v *Vars) lookupNode(name string) (Node, bool) { + // check if the value can be retrieved from a FetchContextProvider + for providerName, provider := range v.fetchContextProviders { + if varPrefixMatched(name, providerName) { + fetchProvider := provider.(composable.FetchContextProvider) + fval, found := fetchProvider.Fetch(name) + if found { + return &StrVal{value: fval}, true + } + return &StrVal{value: ""}, false + } + } + // lookup in the AST tree + return Lookup(v.tree, name) +} + // nodeToValue ensures that the node is an actual value. func nodeToValue(node Node) Node { switch n := node.(type) { diff --git a/x-pack/elastic-agent/pkg/agent/transpiler/vars_test.go b/x-pack/elastic-agent/pkg/agent/transpiler/vars_test.go index 0b6566a7a94e..b9a1fb2cd1d5 100644 --- a/x-pack/elastic-agent/pkg/agent/transpiler/vars_test.go +++ b/x-pack/elastic-agent/pkg/agent/transpiler/vars_test.go @@ -9,6 +9,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/elastic/beats/v7/libbeat/common" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" ) func TestVars_Replace(t *testing.T) { @@ -215,7 +218,8 @@ func TestVars_ReplaceWithProcessors(t *testing.T) { }, }, "dynamic", - processers) + processers, + nil) require.NoError(t, err) res, err := vars.Replace("${testing.key1}") @@ -246,3 +250,89 @@ func TestVars_ReplaceWithProcessors(t *testing.T) { NewKey("key2", NewStrVal("value2")), }, processers), res) } + +func TestVars_ReplaceWithFetchContextProvider(t *testing.T) { + processers := Processors{ + { + "add_fields": map[string]interface{}{ + "dynamic": "added", + }, + }, + } + + mockFetchProvider, err := MockContextProviderBuilder() + require.NoError(t, err) + + fetchContextProviders := common.MapStr{ + "kubernetes_secrets": mockFetchProvider, + } + vars, err := NewVarsWithProcessors( + map[string]interface{}{ + "testing": map[string]interface{}{ + "key1": "data1", + }, + "dynamic": map[string]interface{}{ + "key1": "dynamic1", + "list": []string{ + "array1", + "array2", + }, + "dict": map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + "dynamic", + processers, + fetchContextProviders) + require.NoError(t, err) + + res, err := vars.Replace("${testing.key1}") + require.NoError(t, err) + assert.Equal(t, NewStrVal("data1"), res) + + res, err = vars.Replace("${dynamic.key1}") + require.NoError(t, err) + assert.Equal(t, NewStrValWithProcessors("dynamic1", processers), res) + + res, err = vars.Replace("${other.key1|dynamic.key1}") + require.NoError(t, err) + assert.Equal(t, NewStrValWithProcessors("dynamic1", processers), res) + + res, err = vars.Replace("${dynamic.list}") + require.NoError(t, err) + assert.Equal(t, processers, res.Processors()) + assert.Equal(t, NewListWithProcessors([]Node{ + NewStrVal("array1"), + NewStrVal("array2"), + }, processers), res) + + res, err = vars.Replace("${dynamic.dict}") + require.NoError(t, err) + assert.Equal(t, processers, res.Processors()) + assert.Equal(t, NewDictWithProcessors([]Node{ + NewKey("key1", NewStrVal("value1")), + NewKey("key2", NewStrVal("value2")), + }, processers), res) + + res, err = vars.Replace("${kubernetes_secrets.test_namespace.testing_secret.secret_value}") + require.NoError(t, err) + assert.Equal(t, NewStrVal("mockedFetchContent"), res) +} + +type contextProviderMock struct { +} + +// MockContextProviderBuilder builds the mock context provider. +func MockContextProviderBuilder() (corecomp.ContextProvider, error) { + return &contextProviderMock{}, nil +} + +func (p *contextProviderMock) Fetch(key string) (string, bool) { + return "mockedFetchContent", true +} + +func (p *contextProviderMock) Run(comm corecomp.ContextProviderComm) error { + return nil +} diff --git a/x-pack/elastic-agent/pkg/composable/context.go b/x-pack/elastic-agent/pkg/composable/context.go index f77033d1d6d9..c5f1d187d42e 100644 --- a/x-pack/elastic-agent/pkg/composable/context.go +++ b/x-pack/elastic-agent/pkg/composable/context.go @@ -5,30 +5,16 @@ package composable import ( - "context" "fmt" "strings" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" ) -// ContextProviderComm is the interface that a context provider uses to communicate back to Elastic Agent. -type ContextProviderComm interface { - context.Context - - // Set sets the current mapping for this context. - Set(map[string]interface{}) error -} - -// ContextProvider is the interface that a context provider must implement. -type ContextProvider interface { - // Run runs the context provider. - Run(ContextProviderComm) error -} - // ContextProviderBuilder creates a new context provider based on the given config and returns it. -type ContextProviderBuilder func(log *logger.Logger, config *config.Config) (ContextProvider, error) +type ContextProviderBuilder func(log *logger.Logger, config *config.Config) (corecomp.ContextProvider, error) // AddContextProvider adds a new ContextProviderBuilder func (r *providerRegistry) AddContextProvider(name string, builder ContextProviderBuilder) error { diff --git a/x-pack/elastic-agent/pkg/composable/controller.go b/x-pack/elastic-agent/pkg/composable/controller.go index cb629f4c7e99..8818e0752bdd 100644 --- a/x-pack/elastic-agent/pkg/composable/controller.go +++ b/x-pack/elastic-agent/pkg/composable/controller.go @@ -8,16 +8,18 @@ import ( "context" "encoding/json" "fmt" - "strings" - "reflect" "sort" + "strings" "sync" "time" + "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/transpiler" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" ) @@ -98,6 +100,8 @@ func (c *controller) Run(ctx context.Context, cb VarsCallback) error { notify := make(chan bool, 5000) localCtx, cancel := context.WithCancel(ctx) + fetchContextProviders := common.MapStr{} + // run all the enabled context providers for name, state := range c.contextProviders { state.Context = localCtx @@ -107,6 +111,9 @@ func (c *controller) Run(ctx context.Context, cb VarsCallback) error { cancel() return errors.New(err, fmt.Sprintf("failed to run provider '%s'", name), errors.TypeConfig, errors.M("provider", name)) } + if p, ok := state.provider.(corecomp.FetchContextProvider); ok { + fetchContextProviders.Put(name, p) + } } // run all the enabled dynamic providers @@ -151,7 +158,7 @@ func (c *controller) Run(ctx context.Context, cb VarsCallback) error { mapping[name] = state.Current() } // this is ensured not to error, by how the mappings states are verified - vars[0], _ = transpiler.NewVars(mapping) + vars[0], _ = transpiler.NewVars(mapping, fetchContextProviders) // add to the vars list for each dynamic providers mappings for name, state := range c.dynamicProviders { @@ -159,7 +166,7 @@ func (c *controller) Run(ctx context.Context, cb VarsCallback) error { local, _ := cloneMap(mapping) // will not fail; already been successfully cloned once local[name] = mappings.mapping // this is ensured not to error, by how the mappings states are verified - v, _ := transpiler.NewVarsWithProcessors(local, name, mappings.processors) + v, _ := transpiler.NewVarsWithProcessors(local, name, mappings.processors, fetchContextProviders) vars = append(vars, v) } } @@ -175,7 +182,7 @@ func (c *controller) Run(ctx context.Context, cb VarsCallback) error { type contextProviderState struct { context.Context - provider ContextProvider + provider corecomp.ContextProvider lock sync.RWMutex mapping map[string]interface{} signal chan bool @@ -189,7 +196,7 @@ func (c *contextProviderState) Set(mapping map[string]interface{}) error { return err } // ensure creating vars will not error - _, err = transpiler.NewVars(mapping) + _, err = transpiler.NewVars(mapping, nil) if err != nil { return err } @@ -244,7 +251,7 @@ func (c *dynamicProviderState) AddOrUpdate(id string, priority int, mapping map[ return err } // ensure creating vars will not error - _, err = transpiler.NewVars(mapping) + _, err = transpiler.NewVars(mapping, nil) if err != nil { return err } diff --git a/x-pack/elastic-agent/pkg/composable/providers/agent/agent.go b/x-pack/elastic-agent/pkg/composable/providers/agent/agent.go index c6f1a91e321e..efba2598ad0b 100644 --- a/x-pack/elastic-agent/pkg/composable/providers/agent/agent.go +++ b/x-pack/elastic-agent/pkg/composable/providers/agent/agent.go @@ -9,6 +9,7 @@ import ( "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/release" ) @@ -20,7 +21,7 @@ func init() { type contextProvider struct{} // Run runs the Agent context provider. -func (*contextProvider) Run(comm composable.ContextProviderComm) error { +func (*contextProvider) Run(comm corecomp.ContextProviderComm) error { a, err := info.NewAgentInfo() if err != nil { return err @@ -41,6 +42,6 @@ func (*contextProvider) Run(comm composable.ContextProviderComm) error { } // ContextProviderBuilder builds the context provider. -func ContextProviderBuilder(_ *logger.Logger, _ *config.Config) (composable.ContextProvider, error) { +func ContextProviderBuilder(_ *logger.Logger, _ *config.Config) (corecomp.ContextProvider, error) { return &contextProvider{}, nil } diff --git a/x-pack/elastic-agent/pkg/composable/providers/env/env.go b/x-pack/elastic-agent/pkg/composable/providers/env/env.go index 1eefb7c2ff3b..e068aa7a14e9 100644 --- a/x-pack/elastic-agent/pkg/composable/providers/env/env.go +++ b/x-pack/elastic-agent/pkg/composable/providers/env/env.go @@ -11,6 +11,7 @@ import ( "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" ) @@ -21,7 +22,7 @@ func init() { type contextProvider struct{} // Run runs the environment context provider. -func (*contextProvider) Run(comm composable.ContextProviderComm) error { +func (*contextProvider) Run(comm corecomp.ContextProviderComm) error { err := comm.Set(getEnvMapping()) if err != nil { return errors.New(err, "failed to set mapping", errors.TypeUnexpected) @@ -30,7 +31,7 @@ func (*contextProvider) Run(comm composable.ContextProviderComm) error { } // ContextProviderBuilder builds the context provider. -func ContextProviderBuilder(_ *logger.Logger, _ *config.Config) (composable.ContextProvider, error) { +func ContextProviderBuilder(_ *logger.Logger, _ *config.Config) (corecomp.ContextProvider, error) { return &contextProvider{}, nil } diff --git a/x-pack/elastic-agent/pkg/composable/providers/host/host.go b/x-pack/elastic-agent/pkg/composable/providers/host/host.go index b8971adb4772..3bced796ac2c 100644 --- a/x-pack/elastic-agent/pkg/composable/providers/host/host.go +++ b/x-pack/elastic-agent/pkg/composable/providers/host/host.go @@ -16,6 +16,7 @@ import ( "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" ) @@ -38,7 +39,7 @@ type contextProvider struct { } // Run runs the environment context provider. -func (c *contextProvider) Run(comm composable.ContextProviderComm) error { +func (c *contextProvider) Run(comm corecomp.ContextProviderComm) error { current, err := c.fetcher() if err != nil { return err @@ -79,7 +80,7 @@ func (c *contextProvider) Run(comm composable.ContextProviderComm) error { } // ContextProviderBuilder builds the context provider. -func ContextProviderBuilder(log *logger.Logger, c *config.Config) (composable.ContextProvider, error) { +func ContextProviderBuilder(log *logger.Logger, c *config.Config) (corecomp.ContextProvider, error) { p := &contextProvider{ logger: log, fetcher: getHostInfo, diff --git a/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets/config.go b/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets/config.go new file mode 100644 index 000000000000..29463db148a2 --- /dev/null +++ b/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets/config.go @@ -0,0 +1,13 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// TODO review the need for this +// +build linux darwin windows + +package kubernetessecrets + +// Config for kubernetes provider +type Config struct { + KubeConfig string `config:"kube_config"` +} diff --git a/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets/kubernetes_secrets.go b/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets/kubernetes_secrets.go new file mode 100644 index 000000000000..4af00bc766e5 --- /dev/null +++ b/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets/kubernetes_secrets.go @@ -0,0 +1,94 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package kubernetessecrets + +import ( + "context" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sclient "k8s.io/client-go/kubernetes" + + "github.com/elastic/beats/v7/libbeat/common/kubernetes" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" +) + +var _ corecomp.FetchContextProvider = (*contextProviderK8sSecrets)(nil) +var getK8sClientFunc = getK8sClient + +func init() { + composable.Providers.AddContextProvider("kubernetes_secrets", ContextProviderBuilder) +} + +type contextProviderK8sSecrets struct { + logger *logger.Logger + config *Config + client k8sclient.Interface +} + +// ContextProviderBuilder builds the context provider. +func ContextProviderBuilder(logger *logger.Logger, c *config.Config) (corecomp.ContextProvider, error) { + var cfg Config + if c == nil { + c = config.New() + } + err := c.Unpack(&cfg) + if err != nil { + return nil, errors.New(err, "failed to unpack configuration") + } + return &contextProviderK8sSecrets{logger, &cfg, nil}, nil +} + +func (p *contextProviderK8sSecrets) Fetch(key string) (string, bool) { + // key = "kubernetes_secrets.somenamespace.somesecret.value" + tokens := strings.Split(key, ".") + if len(tokens) > 0 && tokens[0] != "kubernetes_secrets" { + return "", false + } + if len(tokens) != 4 { + p.logger.Debugf( + "not valid secret key: %v. Secrets should be of the following format %v", + key, + "kubernetes_secrets.somenamespace.somesecret.value", + ) + return "", false + } + ns := tokens[1] + secretName := tokens[2] + secretVar := tokens[3] + + secretIntefrace := p.client.CoreV1().Secrets(ns) + ctx := context.TODO() + secret, err := secretIntefrace.Get(ctx, secretName, metav1.GetOptions{}) + if err != nil { + p.logger.Errorf("Could not retrieve secret from k8s API: %v", err) + return "", false + } + if _, ok := secret.Data[secretVar]; !ok { + p.logger.Errorf("Could not retrieve value %v for secret %v", secretVar, secretName) + return "", false + } + secretString := secret.Data[secretVar] + return string(secretString), true +} + +// Run initializes the k8s secrets context provider. +func (p *contextProviderK8sSecrets) Run(comm corecomp.ContextProviderComm) error { + client, err := getK8sClientFunc(p.config.KubeConfig) + if err != nil { + p.logger.Debugf("Kubernetes_secrets provider skipped, unable to connect: %s", err) + return nil + } + p.client = client + return nil +} + +func getK8sClient(kubeconfig string) (k8sclient.Interface, error) { + return kubernetes.GetKubernetesClient(kubeconfig) +} diff --git a/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets/kubernetes_secrets_test.go b/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets/kubernetes_secrets_test.go new file mode 100644 index 000000000000..66943300427c --- /dev/null +++ b/x-pack/elastic-agent/pkg/composable/providers/kubernetessecrets/kubernetes_secrets_test.go @@ -0,0 +1,100 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package kubernetessecrets + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sclient "k8s.io/client-go/kubernetes" + k8sfake "k8s.io/client-go/kubernetes/fake" + + "github.com/elastic/beats/v7/libbeat/logp" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" +) + +func Test_K8sSecretsProvider_Fetch(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), + }, + } + _, err := client.CoreV1().Secrets(ns).Create(context.Background(), secret, metav1.CreateOptions{}) + require.NoError(t, err) + + logger := logp.NewLogger("test_k8s_secrets") + cfg, err := config.NewConfigFrom(map[string]string{"a": "b"}) + require.NoError(t, err) + + p, err := ContextProviderBuilder(logger, cfg) + require.NoError(t, err) + + fp := p.(corecomp.FetchContextProvider) + + getK8sClientFunc = func(kubeconfig string) (k8sclient.Interface, error) { + return client, nil + } + require.NoError(t, err) + fp.Run(nil) + val, found := fp.Fetch("kubernetes_secrets.test_namespace.testing_secret.secret_value") + assert.True(t, found) + assert.Equal(t, val, pass) +} + +func Test_K8sSecretsProvider_FetchWrongSecret(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), + }, + } + _, err := client.CoreV1().Secrets(ns).Create(context.Background(), secret, metav1.CreateOptions{}) + require.NoError(t, err) + + logger := logp.NewLogger("test_k8s_secrets") + cfg, err := config.NewConfigFrom(map[string]string{"a": "b"}) + require.NoError(t, err) + + p, err := ContextProviderBuilder(logger, cfg) + require.NoError(t, err) + + fp := p.(corecomp.FetchContextProvider) + + getK8sClientFunc = func(kubeconfig string) (k8sclient.Interface, error) { + return client, nil + } + require.NoError(t, err) + fp.Run(nil) + val, found := fp.Fetch("kubernetes_secrets.test_namespace.testing_secretHACK.secret_value") + assert.False(t, found) + assert.EqualValues(t, val, "") +} diff --git a/x-pack/elastic-agent/pkg/composable/providers/local/local.go b/x-pack/elastic-agent/pkg/composable/providers/local/local.go index 62fd2b65480f..45a9f71f89a3 100644 --- a/x-pack/elastic-agent/pkg/composable/providers/local/local.go +++ b/x-pack/elastic-agent/pkg/composable/providers/local/local.go @@ -10,6 +10,7 @@ import ( "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" ) @@ -22,7 +23,7 @@ type contextProvider struct { } // Run runs the environment context provider. -func (c *contextProvider) Run(comm composable.ContextProviderComm) error { +func (c *contextProvider) Run(comm corecomp.ContextProviderComm) error { err := comm.Set(c.Mapping) if err != nil { return errors.New(err, "failed to set mapping", errors.TypeUnexpected) @@ -31,7 +32,7 @@ func (c *contextProvider) Run(comm composable.ContextProviderComm) error { } // ContextProviderBuilder builds the context provider. -func ContextProviderBuilder(_ *logger.Logger, c *config.Config) (composable.ContextProvider, error) { +func ContextProviderBuilder(_ *logger.Logger, c *config.Config) (corecomp.ContextProvider, error) { p := &contextProvider{} if c != nil { err := c.Unpack(p) diff --git a/x-pack/elastic-agent/pkg/composable/providers/path/path.go b/x-pack/elastic-agent/pkg/composable/providers/path/path.go index 04e80a9ef416..990e1ecfbd2c 100644 --- a/x-pack/elastic-agent/pkg/composable/providers/path/path.go +++ b/x-pack/elastic-agent/pkg/composable/providers/path/path.go @@ -9,6 +9,7 @@ import ( "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + corecomp "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/composable" "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" ) @@ -19,7 +20,7 @@ func init() { type contextProvider struct{} // Run runs the Agent context provider. -func (*contextProvider) Run(comm composable.ContextProviderComm) error { +func (*contextProvider) Run(comm corecomp.ContextProviderComm) error { err := comm.Set(map[string]interface{}{ "home": paths.Home(), "data": paths.Data(), @@ -33,6 +34,6 @@ func (*contextProvider) Run(comm composable.ContextProviderComm) error { } // ContextProviderBuilder builds the context provider. -func ContextProviderBuilder(_ *logger.Logger, _ *config.Config) (composable.ContextProvider, error) { +func ContextProviderBuilder(_ *logger.Logger, _ *config.Config) (corecomp.ContextProvider, error) { return &contextProvider{}, nil } diff --git a/x-pack/elastic-agent/pkg/core/composable/providers.go b/x-pack/elastic-agent/pkg/core/composable/providers.go new file mode 100644 index 000000000000..cbd2e1db4f4a --- /dev/null +++ b/x-pack/elastic-agent/pkg/core/composable/providers.go @@ -0,0 +1,29 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package composable + +import "context" + +// FetchContextProvider is the interface that a context provider uses so as to be able to be called +// explicitely on demand by vars framework in order to fetch specific target values like a k8s secret. +type FetchContextProvider interface { + ContextProvider + // Run runs the inventory provider. + Fetch(string) (string, bool) +} + +// ContextProviderComm is the interface that a context provider uses to communicate back to Elastic Agent. +type ContextProviderComm interface { + context.Context + + // Set sets the current mapping for this context. + Set(map[string]interface{}) error +} + +// ContextProvider is the interface that a context provider must implement. +type ContextProvider interface { + // Run runs the context provider. + Run(ContextProviderComm) error +}