diff --git a/x-pack/elastic-agent/CHANGELOG.asciidoc b/x-pack/elastic-agent/CHANGELOG.asciidoc index 80b9f6820f5b..90a9ac419cf9 100644 --- a/x-pack/elastic-agent/CHANGELOG.asciidoc +++ b/x-pack/elastic-agent/CHANGELOG.asciidoc @@ -43,5 +43,6 @@ - Enable Filebeat input: S3, Azureeventhub, cloudfoundry, httpjson, netflow, o365audit. {pull}17909[17909] - Configurable log level {pull}18083[18083] - Use data subfolder as default for process logs {pull}17960[17960] +- Enable introspecting configuration {pull}18124[18124] - Follow home path for all config files {pull}18161[18161] - Do not require unnecessary configuration {pull}18003[18003] diff --git a/x-pack/elastic-agent/pkg/agent/application/emitter.go b/x-pack/elastic-agent/pkg/agent/application/emitter.go index 2279d78565d9..249acdd213fb 100644 --- a/x-pack/elastic-agent/pkg/agent/application/emitter.go +++ b/x-pack/elastic-agent/pkg/agent/application/emitter.go @@ -26,7 +26,11 @@ type configModifiers struct { Decorators []decoratorFunc } -func emitter(log *logger.Logger, router *router, modifiers *configModifiers, reloadables ...reloadable) emitterFunc { +type programsDispatcher interface { + Dispatch(id string, grpProg map[routingKey][]program.Program) error +} + +func emitter(log *logger.Logger, router programsDispatcher, modifiers *configModifiers, reloadables ...reloadable) emitterFunc { return func(c *config.Config) error { if err := InjectAgentConfig(c); err != nil { return err diff --git a/x-pack/elastic-agent/pkg/agent/application/introspect_config_cmd.go b/x-pack/elastic-agent/pkg/agent/application/introspect_config_cmd.go new file mode 100644 index 000000000000..f5ab634b84de --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/application/introspect_config_cmd.go @@ -0,0 +1,131 @@ +// 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 application + +import ( + "fmt" + + yaml "gopkg.in/yaml.v2" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/info" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/storage" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/fleetapi" +) + +// IntrospectConfigCmd is an introspect subcommand that shows configurations of the agent. +type IntrospectConfigCmd struct { + cfgPath string +} + +// NewIntrospectConfigCmd creates a new introspect command. +func NewIntrospectConfigCmd(configPath string, +) (*IntrospectConfigCmd, error) { + return &IntrospectConfigCmd{ + cfgPath: configPath, + }, nil +} + +// Execute introspects agent configuration. +func (c *IntrospectConfigCmd) Execute() error { + return c.introspectConfig() +} + +func (c *IntrospectConfigCmd) introspectConfig() error { + cfg, err := loadConfig(c.cfgPath) + if err != nil { + return err + } + + isLocal, err := isLocalMode(cfg) + if err != nil { + return err + } + + if isLocal { + return printConfig(cfg) + } + + fleetConfig, err := loadFleetConfig(cfg) + if err != nil { + return err + } else if fleetConfig == nil { + return errors.New("no fleet config retrieved yet") + } + + return printMapStringConfig(fleetConfig) +} + +func loadConfig(configPath string) (*config.Config, error) { + rawConfig, err := config.LoadYAML(configPath) + if err != nil { + return nil, err + } + + if err := InjectAgentConfig(rawConfig); err != nil { + return nil, err + } + + return rawConfig, nil +} + +func loadFleetConfig(cfg *config.Config) (map[string]interface{}, error) { + log, err := newErrorLogger() + if err != nil { + return nil, err + } + + as, err := newActionStore(log, storage.NewDiskStore(info.AgentActionStoreFile())) + if err != nil { + return nil, err + } + + for _, c := range as.Actions() { + cfgChange, ok := c.(*fleetapi.ActionConfigChange) + if !ok { + continue + } + + fmt.Println("Action ID:", cfgChange.ID()) + return cfgChange.Config, nil + } + return nil, nil +} + +func isLocalMode(rawConfig *config.Config) (bool, error) { + c := localDefaultConfig() + if err := rawConfig.Unpack(&c); err != nil { + return false, errors.New(err, "initiating application") + } + + managementConfig := struct { + Mode string `config:"mode" yaml:"mode"` + }{} + + if err := c.Management.Unpack(&managementConfig); err != nil { + return false, errors.New(err, "initiating application") + } + return managementConfig.Mode == "local", nil +} + +func printMapStringConfig(mapStr map[string]interface{}) error { + data, err := yaml.Marshal(mapStr) + if err != nil { + return errors.New(err, "could not marshal to YAML") + } + + fmt.Println(string(data)) + return nil +} + +func printConfig(cfg *config.Config) error { + mapStr, err := cfg.ToMapStr() + if err != nil { + return err + } + + return printMapStringConfig(mapStr) +} diff --git a/x-pack/elastic-agent/pkg/agent/application/introspect_output_cmd.go b/x-pack/elastic-agent/pkg/agent/application/introspect_output_cmd.go new file mode 100644 index 000000000000..26eb424fe977 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/application/introspect_output_cmd.go @@ -0,0 +1,211 @@ +// 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 application + +import ( + "fmt" + + "github.com/urso/ecslog" + "github.com/urso/ecslog/backend" + "github.com/urso/ecslog/backend/appender" + "github.com/urso/ecslog/backend/layout" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/filters" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/program" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/logger" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/core/plugin/app/monitoring/noop" +) + +// IntrospectOutputCmd is an introspect subcommand that shows configurations of the agent. +type IntrospectOutputCmd struct { + cfgPath string + output string + program string +} + +// NewIntrospectOutputCmd creates a new introspect command. +func NewIntrospectOutputCmd(configPath, output, program string) (*IntrospectOutputCmd, error) { + return &IntrospectOutputCmd{ + cfgPath: configPath, + output: output, + program: program, + }, nil +} + +// Execute tries to enroll the agent into Fleet. +func (c *IntrospectOutputCmd) Execute() error { + if c.output == "" { + return c.introspectOutputs() + } + + return c.introspectOutput() +} + +func (c *IntrospectOutputCmd) introspectOutputs() error { + cfg, err := loadConfig(c.cfgPath) + if err != nil { + return err + } + + isLocal, err := isLocalMode(cfg) + if err != nil { + return err + } + + l, err := newErrorLogger() + if err != nil { + return err + } + + if isLocal { + return listOutputsFromConfig(l, cfg) + } + + fleetConfig, err := loadFleetConfig(cfg) + if err != nil { + return err + } else if fleetConfig == nil { + return errors.New("no fleet config retrieved yet") + } + + return listOutputsFromMap(l, fleetConfig) +} + +func listOutputsFromConfig(log *logger.Logger, cfg *config.Config) error { + programsGroup, err := getProgramsFromConfig(log, cfg) + if err != nil { + return err + + } + + for k := range programsGroup { + fmt.Println(k) + } + + return nil +} + +func listOutputsFromMap(log *logger.Logger, cfg map[string]interface{}) error { + c, err := config.NewConfigFrom(cfg) + if err != nil { + return err + } + + return listOutputsFromConfig(log, c) +} + +func (c *IntrospectOutputCmd) introspectOutput() error { + cfg, err := loadConfig(c.cfgPath) + if err != nil { + return err + } + + l, err := newErrorLogger() + if err != nil { + return err + } + + isLocal, err := isLocalMode(cfg) + if err != nil { + return err + } + + if isLocal { + return printOutputFromConfig(l, c.output, c.program, cfg) + } + + fleetConfig, err := loadFleetConfig(cfg) + if err != nil { + return err + } else if fleetConfig == nil { + return errors.New("no fleet config retrieved yet") + } + + return printOutputFromMap(l, c.output, c.program, fleetConfig) +} + +func printOutputFromConfig(log *logger.Logger, output, programName string, cfg *config.Config) error { + programsGroup, err := getProgramsFromConfig(log, cfg) + if err != nil { + return err + + } + + for k, programs := range programsGroup { + if k != output { + continue + } + + var programFound bool + for _, p := range programs { + if programName != "" && programName != p.Spec.Cmd { + continue + } + + programFound = true + fmt.Printf("[%s] %s:\n", k, p.Spec.Cmd) + printMapStringConfig(p.Configuration()) + fmt.Println("---") + } + + if !programFound { + fmt.Printf("program '%s' is not recognized within output '%s', try running `elastic-agent introspect output` to find available outputs.\n", + programName, + output) + } + return nil + } + + fmt.Printf("output '%s' is not recognized, try running `elastic-agent introspect output` to find available outputs.\n", output) + + return nil +} + +func printOutputFromMap(log *logger.Logger, output, programName string, cfg map[string]interface{}) error { + c, err := config.NewConfigFrom(cfg) + if err != nil { + return err + } + + return printOutputFromConfig(log, output, programName, c) +} + +func getProgramsFromConfig(log *logger.Logger, cfg *config.Config) (map[string][]program.Program, error) { + monitor := noop.NewMonitor() + router := &inmemRouter{} + emit := emitter( + log, + router, + &configModifiers{ + Decorators: []decoratorFunc{injectMonitoring}, + Filters: []filterFunc{filters.ConstraintFilter}, + }, + monitor, + ) + + if err := emit(cfg); err != nil { + return nil, err + } + return router.programs, nil +} + +type inmemRouter struct { + programs map[string][]program.Program +} + +func (r *inmemRouter) Dispatch(id string, grpProg map[routingKey][]program.Program) error { + r.programs = grpProg + return nil +} + +func newErrorLogger() (*logger.Logger, error) { + backend, err := appender.Console(backend.Error, layout.Text(true)) + if err != nil { + return nil, err + } + return ecslog.New(backend), nil +} diff --git a/x-pack/elastic-agent/pkg/agent/cmd/common.go b/x-pack/elastic-agent/pkg/agent/cmd/common.go index 5fe9947e34fc..54b51202ef57 100644 --- a/x-pack/elastic-agent/pkg/agent/cmd/common.go +++ b/x-pack/elastic-agent/pkg/agent/cmd/common.go @@ -61,6 +61,7 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { cmd.AddCommand(basecmd.NewDefaultCommandsWithArgs(args, streams)...) cmd.AddCommand(newRunCommandWithArgs(flags, args, streams)) cmd.AddCommand(newEnrollCommandWithArgs(flags, args, streams)) + cmd.AddCommand(newIntrospectCommandWithArgs(flags, args, streams)) return cmd } diff --git a/x-pack/elastic-agent/pkg/agent/cmd/introspect.go b/x-pack/elastic-agent/pkg/agent/cmd/introspect.go new file mode 100644 index 000000000000..f6cb40e18946 --- /dev/null +++ b/x-pack/elastic-agent/pkg/agent/cmd/introspect.go @@ -0,0 +1,69 @@ +// 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 cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application" + "github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/cli" +) + +func newIntrospectCommandWithArgs(flags *globalFlags, s []string, streams *cli.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "inspect", + Short: "Shows configuration of the agent", + Long: "Shows current configuration of the agent", + Args: cobra.ExactArgs(0), + Run: func(c *cobra.Command, args []string) { + command, err := application.NewIntrospectConfigCmd(flags.Config()) + if err != nil { + fmt.Fprintf(streams.Err, "%v\n", err) + os.Exit(1) + } + + if err := command.Execute(); err != nil { + fmt.Fprintf(streams.Err, "%v\n", err) + os.Exit(1) + } + }, + } + + cmd.AddCommand(newIntrospectOutputCommandWithArgs(flags, s, streams)) + + return cmd +} + +func newIntrospectOutputCommandWithArgs(flags *globalFlags, _ []string, streams *cli.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "output", + Short: "Displays configuration generated for output", + Long: "Displays configuration generated for output.\nIf no output is specified list of output is displayed", + Args: cobra.MaximumNArgs(2), + Run: func(c *cobra.Command, args []string) { + outName, _ := c.Flags().GetString("output") + program, _ := c.Flags().GetString("program") + + command, err := application.NewIntrospectOutputCmd(flags.Config(), outName, program) + if err != nil { + fmt.Fprintf(streams.Err, "%v\n", err) + os.Exit(1) + } + + if err := command.Execute(); err != nil { + fmt.Fprintf(streams.Err, "%v\n", err) + os.Exit(1) + } + }, + } + + cmd.Flags().StringP("output", "o", "", "name of the output to be introspected") + cmd.Flags().StringP("program", "p", "", "type of program to introspect, needs to be combined with output. e.g filebeat") + + return cmd +}