From df03819cbbe2d97ff4b687bc96e3c1bd5ced9dd8 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Fri, 29 Aug 2025 11:54:00 +0200 Subject: [PATCH 01/15] Moved processing of windows events to init --- internal/pkg/agent/cmd/run.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index 281d5a8870b..af301107b5e 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -65,6 +65,14 @@ const ( flagRunDevelopment = "develop" ) +var ( + stopBeatChan chan bool +) + +func init() { + go service.ProcessWindowsControlEvents(stopBeat) +} + func newRunCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "run", @@ -119,6 +127,12 @@ func newRunCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { return cmd } +func stopBeat() { + if stopBeatChan != nil { + close(stopBeatChan) + } +} + func run(override application.CfgOverrider, testingMode bool, fleetInitTimeout time.Duration, modifiers ...component.PlatformModifier) error { // Windows: Mark service as stopped. // After this is run, the service is considered by the OS to be stopped. @@ -132,14 +146,10 @@ func run(override application.CfgOverrider, testingMode bool, fleetInitTimeout t defer service.Cleanup() // register as a service - stop := make(chan bool) + stopBeatChan = make(chan bool) ctx, cancel := context.WithCancel(context.Background()) - stopBeat := func() { - close(stop) - } defer cancel() - go service.ProcessWindowsControlEvents(stopBeat) if err := handleUpgrade(); err != nil { return fmt.Errorf("error checking for and handling upgrade: %w", err) @@ -153,7 +163,7 @@ func run(override application.CfgOverrider, testingMode bool, fleetInitTimeout t _ = locker.Unlock() }() - return runElasticAgent(ctx, cancel, override, stop, testingMode, fleetInitTimeout, modifiers...) + return runElasticAgent(ctx, cancel, override, stopBeatChan, testingMode, fleetInitTimeout, modifiers...) } func logReturn(l *logger.Logger, err error) error { From 9878a565c5cab244b9ec3e506cc3e7978edf3682 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Mon, 1 Sep 2025 10:57:36 +0200 Subject: [PATCH 02/15] cmd refactor --- go.mod | 2 +- .../pkg/agent/agentservice/agentservice.go | 9 + .../agent/agentservice/agentservice_unix.go | 3 + .../agentservice/agentservice_windows.go | 169 +++++++++ internal/pkg/agent/cmd/agentrun/run.go | 92 +++++ .../cmd/{run.go => agentrun/run_agent.go} | 94 +---- .../pkg/agent/cmd/{ => agentrun}/run_test.go | 2 +- internal/pkg/agent/cmd/agentrun/run_unix.go | 15 + .../pkg/agent/cmd/agentrun/run_windows.go | 9 + .../cmd/{ => apply_flavor}/apply_flavor.go | 9 +- internal/pkg/agent/cmd/cmd_test.go | 2 +- internal/pkg/agent/cmd/common.go | 63 ++-- internal/pkg/agent/cmd/common/cmd.go | 62 +++ internal/pkg/agent/cmd/common/config.go | 33 ++ internal/pkg/agent/cmd/common/container.go | 172 +++++++++ internal/pkg/agent/cmd/common/env.go | 103 +++++ internal/pkg/agent/cmd/common/env_test.go | 47 +++ .../cmd/{run_unix.go => common/log_unix.go} | 4 +- .../{run_windows.go => common/log_windows.go} | 4 +- internal/pkg/agent/cmd/common/logs.go | 41 ++ internal/pkg/agent/cmd/common/setup_config.go | 75 ++++ .../agent/cmd/{ => component}/component.go | 4 +- .../cmd/{ => component}/component_spec.go | 2 +- .../agent/cmd/{ => container}/container.go | 354 +++++------------- .../{ => container}/container_init_linux.go | 4 +- .../{ => container}/container_init_other.go | 4 +- .../{ => container}/container_init_test.go | 2 +- .../cmd/{ => container}/container_test.go | 113 ++---- .../cmd/{ => diagnostics}/diagnostics.go | 9 +- .../cmd/{ => diagnostics}/diagnostics_test.go | 2 +- internal/pkg/agent/cmd/include.go | 1 + .../pkg/agent/cmd/{ => inspect}/inspect.go | 13 +- .../pkg/agent/cmd/{ => install}/enroll.go | 35 +- .../pkg/agent/cmd/{ => install}/enroll_cmd.go | 24 +- .../{ => install}/enroll_cmd_nofips_test.go | 4 +- .../cmd/{ => install}/enroll_cmd_test.go | 40 +- .../enroll_match_fileowner_unix.go | 2 +- .../enroll_match_fileowner_unix_test.go | 2 +- .../enroll_match_fileowner_windows.go | 2 +- .../enroll_match_fileowner_windows_test.go | 2 +- .../agent/cmd/{ => install}/enroll_unix.go | 2 +- .../agent/cmd/{ => install}/enroll_windows.go | 2 +- .../pkg/agent/cmd/{ => install}/install.go | 9 +- .../agent/cmd/{ => install}/install_enroll.go | 2 +- .../{ => install}/install_enroll_windows.go | 2 +- .../agent/cmd/{ => install}/install_test.go | 4 +- .../cmd/{ => install}/install_windows_test.go | 4 +- internal/pkg/agent/cmd/{ => logs}/logs.go | 215 +++++------ .../pkg/agent/cmd/{ => logs}/logs_test.go | 4 +- .../cmd/{ => otel}/command_components.go | 7 +- .../command_components_fips_test.go | 2 +- .../command_components_nofips_test.go | 2 +- internal/pkg/agent/cmd/{ => otel}/otel.go | 20 +- .../pkg/agent/cmd/{ => otel}/otel_flags.go | 4 +- .../agent/cmd/{ => otel}/otel_flags_test.go | 10 +- .../pkg/agent/cmd/{ => otel}/otel_test.go | 2 +- internal/pkg/agent/cmd/{ => otel}/validate.go | 7 +- .../pkg/agent/cmd/{ => otel}/validate_test.go | 2 +- internal/pkg/agent/cmd/{ => reexec}/reexec.go | 4 +- .../agent/cmd/{ => reexec}/reexec_windows.go | 11 +- internal/pkg/agent/cmd/setup_config.go | 144 ------- internal/pkg/agent/cmd/{ => status}/status.go | 11 +- .../pkg/agent/cmd/{ => status}/status_test.go | 2 +- .../pkg/agent/cmd/{ => switch}/privileged.go | 11 +- .../agent/cmd/{ => switch}/unprivileged.go | 26 +- .../agent/cmd/{ => uninstall}/uninstall.go | 9 +- .../pkg/agent/cmd/{ => upgrade}/upgrade.go | 7 +- .../agent/cmd/{ => upgrade}/upgrade_test.go | 16 +- internal/pkg/agent/cmd/{ => watch}/watch.go | 53 +-- .../pkg/agent/cmd/{ => watch}/watch_impl.go | 2 +- .../pkg/agent/cmd/{ => watch}/watch_test.go | 2 +- internal/pkg/otel/manager/testing/testing.go | 4 +- 72 files changed, 1296 insertions(+), 935 deletions(-) create mode 100644 internal/pkg/agent/agentservice/agentservice.go create mode 100644 internal/pkg/agent/agentservice/agentservice_unix.go create mode 100644 internal/pkg/agent/agentservice/agentservice_windows.go create mode 100644 internal/pkg/agent/cmd/agentrun/run.go rename internal/pkg/agent/cmd/{run.go => agentrun/run_agent.go} (85%) rename internal/pkg/agent/cmd/{ => agentrun}/run_test.go (99%) create mode 100644 internal/pkg/agent/cmd/agentrun/run_unix.go create mode 100644 internal/pkg/agent/cmd/agentrun/run_windows.go rename internal/pkg/agent/cmd/{ => apply_flavor}/apply_flavor.go (81%) create mode 100644 internal/pkg/agent/cmd/common/cmd.go create mode 100644 internal/pkg/agent/cmd/common/config.go create mode 100644 internal/pkg/agent/cmd/common/container.go create mode 100644 internal/pkg/agent/cmd/common/env.go create mode 100644 internal/pkg/agent/cmd/common/env_test.go rename internal/pkg/agent/cmd/{run_unix.go => common/log_unix.go} (88%) rename internal/pkg/agent/cmd/{run_windows.go => common/log_windows.go} (93%) create mode 100644 internal/pkg/agent/cmd/common/logs.go create mode 100644 internal/pkg/agent/cmd/common/setup_config.go rename internal/pkg/agent/cmd/{ => component}/component.go (88%) rename internal/pkg/agent/cmd/{ => component}/component_spec.go (98%) rename internal/pkg/agent/cmd/{ => container}/container.go (80%) rename internal/pkg/agent/cmd/{ => container}/container_init_linux.go (99%) rename internal/pkg/agent/cmd/{ => container}/container_init_other.go (85%) rename internal/pkg/agent/cmd/{ => container}/container_init_test.go (99%) rename internal/pkg/agent/cmd/{ => container}/container_test.go (86%) rename internal/pkg/agent/cmd/{ => diagnostics}/diagnostics.go (94%) rename internal/pkg/agent/cmd/{ => diagnostics}/diagnostics_test.go (98%) rename internal/pkg/agent/cmd/{ => inspect}/inspect.go (97%) rename internal/pkg/agent/cmd/{ => install}/enroll.go (97%) rename internal/pkg/agent/cmd/{ => install}/enroll_cmd.go (97%) rename internal/pkg/agent/cmd/{ => install}/enroll_cmd_nofips_test.go (98%) rename internal/pkg/agent/cmd/{ => install}/enroll_cmd_test.go (96%) rename internal/pkg/agent/cmd/{ => install}/enroll_match_fileowner_unix.go (98%) rename internal/pkg/agent/cmd/{ => install}/enroll_match_fileowner_unix_test.go (97%) rename internal/pkg/agent/cmd/{ => install}/enroll_match_fileowner_windows.go (95%) rename internal/pkg/agent/cmd/{ => install}/enroll_match_fileowner_windows_test.go (97%) rename internal/pkg/agent/cmd/{ => install}/enroll_unix.go (98%) rename internal/pkg/agent/cmd/{ => install}/enroll_windows.go (98%) rename internal/pkg/agent/cmd/{ => install}/install.go (97%) rename internal/pkg/agent/cmd/{ => install}/install_enroll.go (98%) rename internal/pkg/agent/cmd/{ => install}/install_enroll_windows.go (97%) rename internal/pkg/agent/cmd/{ => install}/install_test.go (97%) rename internal/pkg/agent/cmd/{ => install}/install_windows_test.go (95%) rename internal/pkg/agent/cmd/{ => logs}/logs.go (98%) rename internal/pkg/agent/cmd/{ => logs}/logs_test.go (99%) rename internal/pkg/agent/cmd/{ => otel}/command_components.go (88%) rename internal/pkg/agent/cmd/{ => otel}/command_components_fips_test.go (99%) rename internal/pkg/agent/cmd/{ => otel}/command_components_nofips_test.go (99%) rename internal/pkg/agent/cmd/{ => otel}/otel.go (93%) rename internal/pkg/agent/cmd/{ => otel}/otel_flags.go (98%) rename internal/pkg/agent/cmd/{ => otel}/otel_flags_test.go (95%) rename internal/pkg/agent/cmd/{ => otel}/otel_test.go (99%) rename internal/pkg/agent/cmd/{ => otel}/validate.go (90%) rename internal/pkg/agent/cmd/{ => otel}/validate_test.go (98%) rename internal/pkg/agent/cmd/{ => reexec}/reexec.go (83%) rename internal/pkg/agent/cmd/{ => reexec}/reexec_windows.go (93%) delete mode 100644 internal/pkg/agent/cmd/setup_config.go rename internal/pkg/agent/cmd/{ => status}/status.go (96%) rename internal/pkg/agent/cmd/{ => status}/status_test.go (99%) rename internal/pkg/agent/cmd/{ => switch}/privileged.go (90%) rename internal/pkg/agent/cmd/{ => switch}/unprivileged.go (86%) rename internal/pkg/agent/cmd/{ => uninstall}/uninstall.go (92%) rename internal/pkg/agent/cmd/{ => upgrade}/upgrade.go (96%) rename internal/pkg/agent/cmd/{ => upgrade}/upgrade_test.go (95%) rename internal/pkg/agent/cmd/{ => watch}/watch.go (85%) rename internal/pkg/agent/cmd/{ => watch}/watch_impl.go (98%) rename internal/pkg/agent/cmd/{ => watch}/watch_test.go (99%) diff --git a/go.mod b/go.mod index 633be6ba673..64d48e82118 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/elastic/opentelemetry-collector-components/receiver/elasticapmintakereceiver v0.2.1 github.com/fatih/color v1.18.0 github.com/fsnotify/fsnotify v1.9.0 + github.com/ghodss/yaml v1.0.0 github.com/go-viper/mapstructure/v2 v2.3.0 github.com/gofrs/flock v0.12.1 github.com/gofrs/uuid/v5 v5.3.1 @@ -366,7 +367,6 @@ require ( github.com/foxboron/go-tpm-keyfiles v0.0.0-20250323135004-b31fac66206e // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/getsentry/sentry-go v0.31.1 // indirect - github.com/ghodss/yaml v1.0.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect diff --git a/internal/pkg/agent/agentservice/agentservice.go b/internal/pkg/agent/agentservice/agentservice.go new file mode 100644 index 00000000000..84ff2ba3cd0 --- /dev/null +++ b/internal/pkg/agent/agentservice/agentservice.go @@ -0,0 +1,9 @@ +package agentservice + +var ( + StopChanBeat chan bool +) + +func init() { + StopChanBeat = make(chan bool) +} diff --git a/internal/pkg/agent/agentservice/agentservice_unix.go b/internal/pkg/agent/agentservice/agentservice_unix.go new file mode 100644 index 00000000000..600ddc9b376 --- /dev/null +++ b/internal/pkg/agent/agentservice/agentservice_unix.go @@ -0,0 +1,3 @@ +//go:build !windows + +package agentservice diff --git a/internal/pkg/agent/agentservice/agentservice_windows.go b/internal/pkg/agent/agentservice/agentservice_windows.go new file mode 100644 index 00000000000..87e9d19867d --- /dev/null +++ b/internal/pkg/agent/agentservice/agentservice_windows.go @@ -0,0 +1,169 @@ +//go:build windows + +package agentservice + +import ( + "os" + "sync" + "syscall" + "time" + + "github.com/elastic/elastic-agent-libs/logp" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" +) + +// couldNotConnect is the errno for ERROR_FAILED_SERVICE_CONTROLLER_CONNECT. +const couldNotConnect syscall.Errno = 1063 + +type beatService struct { + stopCallback func() + done chan struct{} + executeFinished chan struct{} +} + +var serviceInstance = &beatService{ + stopCallback: nil, + done: make(chan struct{}), + executeFinished: make(chan struct{}), +} + +func init() { + var wg sync.WaitGroup + wg.Add(1) + + go func() { + wg.Done() + processWindowsControlEvents(stopBeat) + }() + // wait with subsequent execution so we don't starve us from resources when trying + // to check with manager. + // resources could be consumed in a way of additional init calls during startup + // or when execution proceeds and spins up additional goroutine that could get priority + // e.g starting beats, coordinator, dispatcher... other components + wg.Wait() +} + +func stopBeat() { + if StopChanBeat != nil { + close(StopChanBeat) + } +} + +// ProcessWindowsControlEvents on Windows machines creates a loop +// that only finishes when a Stop or Shutdown request is received. +// On non-windows platforms, the function does nothing. The +// stopCallback function is called when the Stop/Shutdown +// request is received. +func processWindowsControlEvents(stopCallback func()) { + defer close(serviceInstance.executeFinished) + + //nolint:staticcheck // keep using the deprecated method in order to maintain the existing behavior + isWindowsService, err := svc.IsWindowsService() + if err != nil { + logp.Err("IsAnInteractiveSession: %v", err) + return + } + logp.Debug("service", "Windows is interactive: %v", isWindowsService) + + run := svc.Run + if isWindowsService { + run = debug.Run + } + + serviceInstance.stopCallback = stopCallback + err = run(os.Args[0], serviceInstance) + if err == nil { + return + } + + //nolint:errorlint // this system error is a special case + if errnoErr, ok := err.(syscall.Errno); ok && errnoErr == couldNotConnect { + /* + If, as in the case of Jenkins, the process is started as an interactive process, but the invoking process + is itself a service, beats will incorrectly try to register a service handler. We don't want to swallow + errors, so we should still log this, but only as Info. The only ill effect should be a couple extra + idle go routines. + + Ideally we could detect this better, but the only reliable way is with StartServiceCtrlDispatcherW, which + is invoked in go with svc.Run. Unfortunately, this also starts some goroutines ahead of time for various + reasons. As the docs state for StartServiceCtrlDispatcherW when a 1063 errno is returned: + + "This error is returned if the program is being run as a console application rather than as a service. + If the program will be run as a console application for debugging purposes, structure it such that + service-specific code is not called when this error is returned." + */ + logp.Info("Attempted to register Windows service handlers, but this is not a service. No action necessary") + return + } + + logp.Err("Windows service setup failed: %+v", err) +} + +// Execute runs the beat service with the arguments and manages changes that +// occur in the environment or runtime that may affect the beat. +func (m *beatService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + + log := logp.NewLogger("service_windows") + log.Info("reported Running to Service manager") + combinedChan := make(chan svc.ChangeRequest) + go func() { + for { + select { + case c := <-r: + combinedChan <- c + case <-m.done: + // exits consumption loop on termination and reports stopping + combinedChan <- svc.ChangeRequest{Cmd: svc.Shutdown} + return + } + } + }() + +loop: + for c := range combinedChan { + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + // Testing deadlock from https://code.google.com/p/winsvc/issues/detail?id=4 + time.Sleep(100 * time.Millisecond) + changes <- c.CurrentStatus + + // The svc.Cmd tye does not implement the Stringer interface and its + // underlying type is an integer, therefore it's needed to manually log them. + case svc.Stop: + log.Info("received state change 'svc.Stop' from windows service manager") + break loop + case svc.Shutdown: + log.Info("received state change 'svc.Shutdown' from windows service manager") + break loop + + default: + log.Errorf("Unexpected control request: $%d. Ignored.", c) + } + } + + trySendState(svc.StopPending, changes) + defer trySendState(svc.Stopped, changes) + + log.Info("changed windows service state to svc.StopPending, invoking stopCallback") + m.stopCallback() + + // Block until notifyWindowsServiceStopped below is called. This is required + // as the windows/svc package will transition the service to STOPPED state + // once this function returns. + <-m.done + log.Debug("windows service state changed to svc.Stopped") + return ssec, errno +} + +func trySendState(s svc.State, changes chan<- svc.Status) { + select { + case changes <- svc.Status{State: s}: + case <-time.After(500 * time.Millisecond): // should never happen, but don't make this blocking + } +} diff --git a/internal/pkg/agent/cmd/agentrun/run.go b/internal/pkg/agent/cmd/agentrun/run.go new file mode 100644 index 00000000000..c9f8eb5ad0c --- /dev/null +++ b/internal/pkg/agent/cmd/agentrun/run.go @@ -0,0 +1,92 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package agentrun + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/elastic/elastic-agent-libs/service" + + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" + "github.com/elastic/elastic-agent/internal/pkg/cli" +) + +const ( + flagInstallDevelopment = "develop" +) + +func NewRunCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "run", + Short: "Start the Elastic Agent", + Long: "This command starts the Elastic Agent.", + RunE: func(cmd *cobra.Command, _ []string) error { + isDevelopmentMode, _ := cmd.Flags().GetBool(flagInstallDevelopment) + if isDevelopmentMode { + fmt.Fprintln(streams.Out, "Development installation mode enabled; this is an experimental feature.") + // For now, development mode only makes the agent behave as if it was running in a namespace to allow + // multiple agents on the same machine. + paths.SetInstallNamespace(paths.DevelopmentNamespace) + } + + // done very early so the encrypted store is never used. Always done in development mode to remove the need to be root. + disableEncryptedStore, _ := cmd.Flags().GetBool("disable-encrypted-store") + disableEncyption(disableEncryptedStore, isDevelopmentMode) + + fleetInitTimeout, _ := cmd.Flags().GetDuration("fleet-init-timeout") + testingMode, _ := cmd.Flags().GetBool("testing-mode") + if err := runService(testingMode, fleetInitTimeout); err != nil && !errors.Is(err, context.Canceled) { + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) + common.LogExternal(fmt.Sprintf("%s run failed: %s", paths.BinaryName, err)) + return err + } + return nil + }, + } + + // --disable-encrypted-store only has meaning on Mac OS, and it disables the encrypted disk store + // feature of the Elastic Agent. On Mac OS root privileges are required to perform the disk + // store encryption, by setting this flag it disables that feature and allows the Elastic Agent to + // run as non-root. + // + // Deprecated: MacOS can be run/installed without root privileges + cmd.Flags().Bool("disable-encrypted-store", false, "Disable the encrypted disk storage (Only useful on Mac OS)") + _ = cmd.Flags().MarkHidden("disable-encrypted-store") + _ = cmd.Flags().MarkDeprecated("disable-encrypted-store", "agent on Mac OS can be run/installed without root privileges, see elastic-agent install --help") + + // --testing-mode is a hidden flag that spawns the Elastic Agent in testing mode + // it is hidden because we really don't want users to execute Elastic Agent to run + // this way, only the integration testing framework runs the Elastic Agent in this mode + cmd.Flags().Bool("testing-mode", false, "Run with testing mode enabled") + + cmd.Flags().Duration("fleet-init-timeout", common.EnvTimeout(fleetInitTimeoutName), " Sets the initial timeout when starting up the fleet server under agent") + _ = cmd.Flags().MarkHidden("testing-mode") + + cmd.Flags().Bool(flagRunDevelopment, false, "Run agent in development mode. Allows running when there is already an installed Elastic Agent. (experimental)") + _ = cmd.Flags().MarkHidden(flagRunDevelopment) // For internal use only. + + return cmd +} + +func runService(testingMode bool, fleetInitTimeout time.Duration) error { + // Windows: Mark service as stopped. + // After this is run, the service is considered by the OS to be stopped. + // This must be the first deferred cleanup task (last to execute). + defer func() { + service.NotifyTermination() + service.WaitExecutionDone() + }() + + service.BeforeRun() + defer service.Cleanup() + + return Run(nil, testingMode, fleetInitTimeout) +} diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/agentrun/run_agent.go similarity index 85% rename from internal/pkg/agent/cmd/run.go rename to internal/pkg/agent/cmd/agentrun/run_agent.go index af301107b5e..82aca649226 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/agentrun/run_agent.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package agentrun import ( "context" @@ -16,6 +16,7 @@ import ( "syscall" "time" + "github.com/elastic/elastic-agent/internal/pkg/agent/agentservice" "github.com/elastic/elastic-agent/internal/pkg/agent/application/enroll" fleetgateway "github.com/elastic/elastic-agent/internal/pkg/agent/application/gateway/fleet" @@ -23,12 +24,9 @@ import ( apmtransport "go.elastic.co/apm/v2/transport" "gopkg.in/yaml.v2" - "github.com/spf13/cobra" - "github.com/elastic/elastic-agent-libs/api" "github.com/elastic/elastic-agent-libs/logp" monitoringLib "github.com/elastic/elastic-agent-libs/monitoring" - "github.com/elastic/elastic-agent-libs/service" "github.com/elastic/elastic-agent-system-metrics/report" "github.com/elastic/elastic-agent/internal/pkg/agent/vault" @@ -42,6 +40,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/reexec" "github.com/elastic/elastic-agent/internal/pkg/agent/application/secret" "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade" + installcmd "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/install" "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/agent/install" @@ -65,88 +64,7 @@ const ( flagRunDevelopment = "develop" ) -var ( - stopBeatChan chan bool -) - -func init() { - go service.ProcessWindowsControlEvents(stopBeat) -} - -func newRunCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "run", - Short: "Start the Elastic Agent", - Long: "This command starts the Elastic Agent.", - RunE: func(cmd *cobra.Command, _ []string) error { - isDevelopmentMode, _ := cmd.Flags().GetBool(flagInstallDevelopment) - if isDevelopmentMode { - fmt.Fprintln(streams.Out, "Development installation mode enabled; this is an experimental feature.") - // For now, development mode only makes the agent behave as if it was running in a namespace to allow - // multiple agents on the same machine. - paths.SetInstallNamespace(paths.DevelopmentNamespace) - } - - // done very early so the encrypted store is never used. Always done in development mode to remove the need to be root. - disableEncryptedStore, _ := cmd.Flags().GetBool("disable-encrypted-store") - if disableEncryptedStore || isDevelopmentMode { - storage.DisableEncryptionDarwin() - } - fleetInitTimeout, _ := cmd.Flags().GetDuration("fleet-init-timeout") - testingMode, _ := cmd.Flags().GetBool("testing-mode") - if err := run(nil, testingMode, fleetInitTimeout); err != nil && !errors.Is(err, context.Canceled) { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) - logExternal(fmt.Sprintf("%s run failed: %s", paths.BinaryName, err)) - return err - } - return nil - }, - } - - // --disable-encrypted-store only has meaning on Mac OS, and it disables the encrypted disk store - // feature of the Elastic Agent. On Mac OS root privileges are required to perform the disk - // store encryption, by setting this flag it disables that feature and allows the Elastic Agent to - // run as non-root. - // - // Deprecated: MacOS can be run/installed without root privileges - cmd.Flags().Bool("disable-encrypted-store", false, "Disable the encrypted disk storage (Only useful on Mac OS)") - _ = cmd.Flags().MarkHidden("disable-encrypted-store") - _ = cmd.Flags().MarkDeprecated("disable-encrypted-store", "agent on Mac OS can be run/installed without root privileges, see elastic-agent install --help") - - // --testing-mode is a hidden flag that spawns the Elastic Agent in testing mode - // it is hidden because we really don't want users to execute Elastic Agent to run - // this way, only the integration testing framework runs the Elastic Agent in this mode - cmd.Flags().Bool("testing-mode", false, "Run with testing mode enabled") - - cmd.Flags().Duration("fleet-init-timeout", envTimeout(fleetInitTimeoutName), " Sets the initial timeout when starting up the fleet server under agent") - _ = cmd.Flags().MarkHidden("testing-mode") - - cmd.Flags().Bool(flagRunDevelopment, false, "Run agent in development mode. Allows running when there is already an installed Elastic Agent. (experimental)") - _ = cmd.Flags().MarkHidden(flagRunDevelopment) // For internal use only. - - return cmd -} - -func stopBeat() { - if stopBeatChan != nil { - close(stopBeatChan) - } -} - -func run(override application.CfgOverrider, testingMode bool, fleetInitTimeout time.Duration, modifiers ...component.PlatformModifier) error { - // Windows: Mark service as stopped. - // After this is run, the service is considered by the OS to be stopped. - // This must be the first deferred cleanup task (last to execute). - defer func() { - service.NotifyTermination() - service.WaitExecutionDone() - }() - - service.BeforeRun() - defer service.Cleanup() - - // register as a service - stopBeatChan = make(chan bool) +func Run(override application.CfgOverrider, testingMode bool, fleetInitTimeout time.Duration, modifiers ...component.PlatformModifier) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -163,7 +81,7 @@ func run(override application.CfgOverrider, testingMode bool, fleetInitTimeout t _ = locker.Unlock() }() - return runElasticAgent(ctx, cancel, override, stopBeatChan, testingMode, fleetInitTimeout, modifiers...) + return runElasticAgent(ctx, cancel, override, agentservice.StopChanBeat, testingMode, fleetInitTimeout, modifiers...) } func logReturn(l *logger.Logger, err error) error { @@ -554,7 +472,7 @@ func tryDelayEnroll(ctx context.Context, logger *logger.Logger, cfg *configurati info.DefaultAgentFleetConfig, encStore, ) - c, err := newEnrollCmd( + c, err := installcmd.NewEnrollCmd( logger, &options, paths.ConfigFile(), diff --git a/internal/pkg/agent/cmd/run_test.go b/internal/pkg/agent/cmd/agentrun/run_test.go similarity index 99% rename from internal/pkg/agent/cmd/run_test.go rename to internal/pkg/agent/cmd/agentrun/run_test.go index f2782866cd0..5336e86126f 100644 --- a/internal/pkg/agent/cmd/run_test.go +++ b/internal/pkg/agent/cmd/agentrun/run_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package agentrun import ( "fmt" diff --git a/internal/pkg/agent/cmd/agentrun/run_unix.go b/internal/pkg/agent/cmd/agentrun/run_unix.go new file mode 100644 index 00000000000..07c518677d6 --- /dev/null +++ b/internal/pkg/agent/cmd/agentrun/run_unix.go @@ -0,0 +1,15 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +//go:build !windows + +package agentrun + +import "github.com/elastic/elastic-agent/internal/pkg/agent/storage" + +func disableEncyption(disableEncryptedStore, isDevelopmentMode bool) { + if disableEncryptedStore || isDevelopmentMode { + storage.DisableEncryptionDarwin() + } +} diff --git a/internal/pkg/agent/cmd/agentrun/run_windows.go b/internal/pkg/agent/cmd/agentrun/run_windows.go new file mode 100644 index 00000000000..db49ff39e83 --- /dev/null +++ b/internal/pkg/agent/cmd/agentrun/run_windows.go @@ -0,0 +1,9 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +//go:build windows + +package agentrun + +func disableEncyption(disableEncryptedStore, isDevelopmentMode bool) {} diff --git a/internal/pkg/agent/cmd/apply_flavor.go b/internal/pkg/agent/cmd/apply_flavor/apply_flavor.go similarity index 81% rename from internal/pkg/agent/cmd/apply_flavor.go rename to internal/pkg/agent/cmd/apply_flavor/apply_flavor.go index f184df21185..db5c4cdb611 100644 --- a/internal/pkg/agent/cmd/apply_flavor.go +++ b/internal/pkg/agent/cmd/apply_flavor/apply_flavor.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package applyflavor import ( "errors" @@ -13,19 +13,20 @@ import ( "github.com/spf13/cobra" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/install" "github.com/elastic/elastic-agent/internal/pkg/cli" v1 "github.com/elastic/elastic-agent/pkg/api/v1" ) -func newApplyFlavorCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewApplyFlavorCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "apply-flavor", Short: "Apply Flavor cleans up unnecessary components from agent installation directory", Run: func(c *cobra.Command, _ []string) { if err := applyCmd(); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) - logExternal(fmt.Sprintf("%s apply flavor failed: %s", paths.BinaryName, err)) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) + common.LogExternal(fmt.Sprintf("%s apply flavor failed: %s", paths.BinaryName, err)) os.Exit(1) } }, diff --git a/internal/pkg/agent/cmd/cmd_test.go b/internal/pkg/agent/cmd/cmd_test.go index 66033e80947..9de3e56b3aa 100644 --- a/internal/pkg/agent/cmd/cmd_test.go +++ b/internal/pkg/agent/cmd/cmd_test.go @@ -18,7 +18,7 @@ func TestAgent(t *testing.T) { // t.Run("test run subcommand", func(t *testing.T) { // streams, _, out, _ := cli.NewTestingIOStreams() - // cmd := newRunCommandWithArgs(globalFlags{ + // cmd := NewRunCommandWithArgs(globalFlags{ // PathConfigFile: filepath.Join("build", "elastic-agent.yml"), // }, []string{}, streams) // cmd.SetOutput(streams.Out) diff --git a/internal/pkg/agent/cmd/common.go b/internal/pkg/agent/cmd/common.go index 5c98b07cff9..d40e4f54d34 100644 --- a/internal/pkg/agent/cmd/common.go +++ b/internal/pkg/agent/cmd/common.go @@ -6,27 +6,34 @@ package cmd import ( "flag" - "fmt" "os" - "strings" "github.com/spf13/cobra" // import logp flags _ "github.com/elastic/elastic-agent-libs/logp/configure" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/agentrun" + applyflavor "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/apply_flavor" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/component" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/container" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/diagnostics" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/inspect" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/install" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/logs" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/otel" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/reexec" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/status" + switchcmd "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/switch" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/uninstall" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/upgrade" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/watch" "github.com/elastic/elastic-agent/internal/pkg/basecmd" "github.com/elastic/elastic-agent/internal/pkg/cli" - "github.com/elastic/elastic-agent/internal/pkg/release" "github.com/elastic/elastic-agent/version" ) -func troubleshootMessage() string { - v := strings.Split(release.Version(), ".") - version := strings.Join(v[:2], ".") - return fmt.Sprintf("For help, please see our troubleshooting guide at https://www.elastic.co/guide/en/fleet/%s/fleet-troubleshooting.html", version) -} - // NewCommand returns the default command for the agent. func NewCommand() *cobra.Command { return NewCommandWithArgs(os.Args, cli.NewIOStreams()) @@ -42,10 +49,10 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { // before tryContainerLoadPaths as this will try to read/write from // the agent state dir which might not have proper permissions when // running inside a container - initContainer(streams) + container.InitContainer(streams) } - return tryContainerLoadPaths() + return common.TryContainerLoadPaths() }, } @@ -76,28 +83,28 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("environment")) // sub-commands - run := newRunCommandWithArgs(args, streams) + run := agentrun.NewRunCommandWithArgs(args, streams) cmd.AddCommand(basecmd.NewDefaultCommandsWithArgs(args, streams)...) cmd.AddCommand(run) - cmd.AddCommand(newInstallCommandWithArgs(args, streams)) - cmd.AddCommand(newUninstallCommandWithArgs(args, streams)) - cmd.AddCommand(newUpgradeCommandWithArgs(args, streams)) - cmd.AddCommand(newEnrollCommandWithArgs(args, streams)) - cmd.AddCommand(newInspectCommandWithArgs(args, streams)) - cmd.AddCommand(newPrivilegedCommandWithArgs(args, streams)) - cmd.AddCommand(newUnprivilegedCommandWithArgs(args, streams)) - cmd.AddCommand(newWatchCommandWithArgs(args, streams)) - cmd.AddCommand(newContainerCommand(args, streams)) - cmd.AddCommand(newStatusCommand(args, streams)) - cmd.AddCommand(newDiagnosticsCommand(args, streams)) - cmd.AddCommand(newComponentCommandWithArgs(args, streams)) - cmd.AddCommand(newLogsCommandWithArgs(args, streams)) - cmd.AddCommand(newOtelCommandWithArgs(args, streams)) - cmd.AddCommand(newApplyFlavorCommandWithArgs(args, streams)) + cmd.AddCommand(install.NewInstallCommandWithArgs(args, streams)) + cmd.AddCommand(uninstall.NewUninstallCommandWithArgs(args, streams)) + cmd.AddCommand(upgrade.NewUpgradeCommandWithArgs(args, streams)) + cmd.AddCommand(install.NewEnrollCommandWithArgs(args, streams)) + cmd.AddCommand(inspect.NewInspectCommandWithArgs(args, streams)) + cmd.AddCommand(switchcmd.NewPrivilegedCommandWithArgs(args, streams)) + cmd.AddCommand(switchcmd.NewUnprivilegedCommandWithArgs(args, streams)) + cmd.AddCommand(watch.NewWatchCommandWithArgs(args, streams)) + cmd.AddCommand(container.NewContainerCommand(args, streams)) + cmd.AddCommand(status.NewStatusCommand(args, streams)) + cmd.AddCommand(diagnostics.NewDiagnosticsCommand(args, streams)) + cmd.AddCommand(component.NewComponentCommandWithArgs(args, streams)) + cmd.AddCommand(logs.NewLogsCommandWithArgs(args, streams)) + cmd.AddCommand(otel.NewOtelCommandWithArgs(args, streams)) + cmd.AddCommand(applyflavor.NewApplyFlavorCommandWithArgs(args, streams)) // windows special hidden sub-command (only added on Windows) - reexec := newReExecWindowsCommand(args, streams) + reexec := reexec.NewReExecWindowsCommand(args, streams) if reexec != nil { cmd.AddCommand(reexec) } diff --git a/internal/pkg/agent/cmd/common/cmd.go b/internal/pkg/agent/cmd/common/cmd.go new file mode 100644 index 00000000000..d20409a5335 --- /dev/null +++ b/internal/pkg/agent/cmd/common/cmd.go @@ -0,0 +1,62 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package common + +import ( + "context" + "os" + "os/signal" + "syscall" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/elastic/elastic-agent/pkg/control/v2/client" +) + +const ( + DefaultStateDirectory = agentBaseDirectory + "/state" // directory that will hold the state data + + DaemonTimeout = 30 * time.Second // max amount of for communication to running Agent daemon +) + +func HideInheritedFlags(c *cobra.Command) { + c.InheritedFlags().VisitAll(func(f *pflag.Flag) { + f.Hidden = true + }) +} + +func HandleSignal(ctx context.Context) context.Context { + ctx, cfunc := context.WithCancel(ctx) + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + go func() { + select { + case <-sigs: + cfunc() + case <-ctx.Done(): + } + + signal.Stop(sigs) + close(sigs) + }() + + return ctx +} + +func GetDaemonState(ctx context.Context) (*client.AgentState, error) { + ctx, cancel := context.WithTimeout(ctx, DaemonTimeout) + defer cancel() + daemon := client.New() + err := daemon.Connect(ctx) + if err != nil { + return nil, err + } + defer daemon.Disconnect() + return daemon.State(ctx) +} diff --git a/internal/pkg/agent/cmd/common/config.go b/internal/pkg/agent/cmd/common/config.go new file mode 100644 index 00000000000..8ab4f64515d --- /dev/null +++ b/internal/pkg/agent/cmd/common/config.go @@ -0,0 +1,33 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package common + +import ( + "fmt" + + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" + "github.com/elastic/elastic-agent/internal/pkg/cli" + "github.com/elastic/elastic-agent/internal/pkg/config" +) + +func GetConfig(streams *cli.IOStreams) *configuration.Configuration { + defaultCfg := configuration.DefaultConfiguration() + + pathConfigFile := paths.ConfigFile() + rawConfig, err := config.LoadFile(pathConfigFile) + if err != nil { + fmt.Fprintf(streams.Err, "could not read configuration file %s", pathConfigFile) + return defaultCfg + } + + cfg, err := configuration.NewFromConfig(rawConfig) + if err != nil { + fmt.Fprintf(streams.Err, "could not parse configuration file %s", pathConfigFile) + return defaultCfg + } + + return cfg +} diff --git a/internal/pkg/agent/cmd/common/container.go b/internal/pkg/agent/cmd/common/container.go new file mode 100644 index 00000000000..cc0ef3af7c3 --- /dev/null +++ b/internal/pkg/agent/cmd/common/container.go @@ -0,0 +1,172 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package common + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/ghodss/yaml" + + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/config" + "github.com/elastic/elastic-agent/pkg/core/logger" + "github.com/elastic/elastic-agent/pkg/utils" +) + +const ( + requestRetrySleepEnv = "KIBANA_REQUEST_RETRY_SLEEP" + maxRequestRetriesEnv = "KIBANA_REQUEST_RETRY_COUNT" + defaultRequestRetrySleep = "1s" // sleep 1 sec between retries for HTTP requests + defaultMaxRequestRetries = "30" // maximum number of retries for HTTP requests + agentBaseDirectory = "/usr/share/elastic-agent" // directory that holds all elastic-agent related files + + logsPathPerms = 0775 +) + +type ContainerPaths struct { + StatePath string `config:"state_path" yaml:"state_path"` + ConfigPath string `config:"config_path" yaml:"config_path,omitempty"` + LogsPath string `config:"logs_path" yaml:"logs_path,omitempty"` + SocketPath string `config:"socket_path" yaml:"socket_path,omitempty"` +} + +func TryContainerLoadPaths() error { + statePath := EnvWithDefault("", "STATE_PATH") + if statePath == "" { + statePath = DefaultStateDirectory + } + pathFile := filepath.Join(statePath, "container-paths.yml") + _, err := os.Stat(pathFile) + if os.IsNotExist(err) { + // no container-paths.yml file exists, so nothing to do + return nil + } + cfg, err := config.LoadFile(pathFile) + if err != nil { + return fmt.Errorf("failed to load %s: %w", pathFile, err) + } + var paths ContainerPaths + err = cfg.UnpackTo(&paths) + if err != nil { + return fmt.Errorf("failed to unpack %s: %w", pathFile, err) + } + return SetPaths(paths.StatePath, paths.ConfigPath, paths.LogsPath, paths.SocketPath, false) +} + +func SetPaths(statePath, configPath, logsPath, socketPath string, writePaths bool) error { + statePath = EnvWithDefault(statePath, "STATE_PATH") + if statePath == "" { + statePath = DefaultStateDirectory + } + + topPath := filepath.Join(statePath, "data") + configPath = EnvWithDefault(configPath, "CONFIG_PATH") + if configPath == "" { + configPath = statePath + } + if _, err := os.Stat(configPath); errors.Is(err, fs.ErrNotExist) { + if err := os.MkdirAll(configPath, 0755); err != nil { + return fmt.Errorf("cannot create folders for config path '%s': %w", configPath, err) + } + } + + if socketPath == "" { + socketPath = utils.SocketURLWithFallback(statePath, topPath) + } + // ensure that the directory and sub-directory data exists + if err := os.MkdirAll(topPath, 0755); err != nil { + return fmt.Errorf("preparing STATE_PATH(%s) failed: %w", statePath, err) + } + // ensure that the elastic-agent.yml exists in the state directory or if given in the config directory + baseConfig := filepath.Join(configPath, paths.DefaultConfigName) + if _, err := os.Stat(baseConfig); os.IsNotExist(err) { + if err := copyFile(baseConfig, paths.ConfigFile(), 0); err != nil { + return err + } + } + + originalInstall := paths.Install() + paths.SetTop(topPath) + paths.SetConfig(configPath) + paths.SetControlSocket(socketPath) + // when custom top path is provided the home directory is not versioned + paths.SetVersionHome(false) + // install path stays on container default mount (otherwise a bind mounted directory could have noexec set) + paths.SetInstall(originalInstall) + // set LOGS_PATH is given + logsPath = EnvWithDefault(logsPath, "LOGS_PATH") + if logsPath != "" { + paths.SetLogs(logsPath) + // ensure that the logs directory exists + if err := os.MkdirAll(filepath.Join(logsPath), logsPathPerms); err != nil { + return fmt.Errorf("preparing LOGS_PATH(%s) failed: %w", logsPath, err) + } + } + + // ensure that the internal logger directory exists + loggerPath := filepath.Join(paths.Home(), logger.DefaultLogDirectory) + if err := os.MkdirAll(loggerPath, logsPathPerms); err != nil { + return fmt.Errorf("preparing internal log path(%s) failed: %w", loggerPath, err) + } + + // persist the paths so other commands in the container will use the correct paths + if writePaths { + if err := writeContainerPaths(statePath, configPath, logsPath, socketPath); err != nil { + return err + } + } + return nil +} + +func writeContainerPaths(statePath, configPath, logsPath, socketPath string) error { + pathFile := filepath.Join(statePath, "container-paths.yml") + fp, err := os.Create(pathFile) + if err != nil { + return fmt.Errorf("failed creating %s: %w", pathFile, err) + } + b, err := yaml.Marshal(ContainerPaths{ + StatePath: statePath, + ConfigPath: configPath, + LogsPath: logsPath, + SocketPath: socketPath, + }) + if err != nil { + return fmt.Errorf("failed to marshal for %s: %w", pathFile, err) + } + _, err = fp.Write(b) + if err != nil { + return fmt.Errorf("failed to write %s: %w", pathFile, err) + } + return nil +} + +func copyFile(destPath string, srcPath string, mode os.FileMode) error { + // if mode is unset; set to the same as the source file + if mode == 0 { + info, err := os.Stat(srcPath) + if err == nil { + // ignoring error because; os.Open will also error if the file cannot be stat'd + mode = info.Mode() + } + } + + src, err := os.Open(srcPath) + if err != nil { + return err + } + defer src.Close() + dest, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY, mode) + if err != nil { + return err + } + defer dest.Close() + _, err = io.Copy(dest, src) + return err +} diff --git a/internal/pkg/agent/cmd/common/env.go b/internal/pkg/agent/cmd/common/env.go new file mode 100644 index 00000000000..953ae0fa86f --- /dev/null +++ b/internal/pkg/agent/cmd/common/env.go @@ -0,0 +1,103 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package common + +import ( + "os" + "strconv" + "strings" + "time" +) + +func EnvWithDefault(def string, keys ...string) string { + for _, key := range keys { + val, ok := os.LookupEnv(key) + if ok { + return val + } + } + return def +} + +func EnvBool(keys ...string) bool { + for _, key := range keys { + val, ok := os.LookupEnv(key) + if ok && isTrue(val) { + return true + } + } + return false +} + +func EnvDurationWithDefault(defVal string, keys ...string) (time.Duration, error) { + valStr := defVal + for _, key := range keys { + val, ok := os.LookupEnv(key) + if ok { + valStr = val + break + } + } + + return time.ParseDuration(valStr) +} + +func EnvIntWithDefault(defVal string, keys ...string) (int, error) { + valStr := defVal + for _, key := range keys { + val, ok := os.LookupEnv(key) + if ok { + valStr = val + break + } + } + + return strconv.Atoi(valStr) +} + +func EnvTimeout(keys ...string) time.Duration { + for _, key := range keys { + val, ok := os.LookupEnv(key) + if ok { + dur, err := time.ParseDuration(val) + if err == nil { + return dur + } + } + } + return 0 +} + +func EnvMap(key string) map[string]string { + m := make(map[string]string) + prefix := key + "=" + for _, env := range os.Environ() { + if !strings.HasPrefix(env, prefix) { + continue + } + + envVal := strings.TrimPrefix(env, prefix) + + keyValue := strings.SplitN(envVal, "=", 2) + if len(keyValue) != 2 { + continue + } + + m[keyValue[0]] = keyValue[1] + } + + return m +} + +func isTrue(val string) bool { + trueVals := []string{"1", "true", "yes", "y"} + val = strings.ToLower(val) + for _, v := range trueVals { + if val == v { + return true + } + } + return false +} diff --git a/internal/pkg/agent/cmd/common/env_test.go b/internal/pkg/agent/cmd/common/env_test.go new file mode 100644 index 00000000000..3378cba0bda --- /dev/null +++ b/internal/pkg/agent/cmd/common/env_test.go @@ -0,0 +1,47 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package common + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestEnvWithDefault(t *testing.T) { + def := "default" + key1 := "ENV_WITH_DEFAULT_1" + key2 := "ENV_WITH_DEFAULT_2" + + res := EnvWithDefault(def, key1, key2) + + require.Equal(t, def, res) + + t.Setenv(key1, "key1") + + t.Setenv(key2, "key2") + + res2 := EnvWithDefault(def, key1, key2) + require.Equal(t, "key1", res2) +} + +func TestEnvBool(t *testing.T) { + key := "TEST_ENV_BOOL" + + t.Setenv(key, "true") + + res := EnvBool(key) + require.True(t, res) +} + +func TestEnvTimeout(t *testing.T) { + key := "TEST_ENV_TIMEOUT" + + t.Setenv(key, "10s") + + res := EnvTimeout(key) + require.Equal(t, time.Second*10, res) +} diff --git a/internal/pkg/agent/cmd/run_unix.go b/internal/pkg/agent/cmd/common/log_unix.go similarity index 88% rename from internal/pkg/agent/cmd/run_unix.go rename to internal/pkg/agent/cmd/common/log_unix.go index 0fe26c67972..1cc64695268 100644 --- a/internal/pkg/agent/cmd/run_unix.go +++ b/internal/pkg/agent/cmd/common/log_unix.go @@ -4,8 +4,8 @@ //go:build !windows -package cmd +package common // logExternal logs the error to an external log. On non-windows systems this is a no-op. -func logExternal(msg string) { +func LogExternal(msg string) { } diff --git a/internal/pkg/agent/cmd/run_windows.go b/internal/pkg/agent/cmd/common/log_windows.go similarity index 93% rename from internal/pkg/agent/cmd/run_windows.go rename to internal/pkg/agent/cmd/common/log_windows.go index 4f11ccea255..4d6e180b8fd 100644 --- a/internal/pkg/agent/cmd/run_windows.go +++ b/internal/pkg/agent/cmd/common/log_windows.go @@ -4,7 +4,7 @@ //go:build windows -package cmd +package common import ( "golang.org/x/sys/windows/svc/eventlog" @@ -15,7 +15,7 @@ import ( // logExternal logs the error to an external log. On Windows this is // the Application EventLog. This is a best effort logger and no // errors are returned. -func logExternal(msg string) { +func LogExternal(msg string) { eLog, err2 := eventlog.Open(paths.ServiceName()) if err2 != nil { return diff --git a/internal/pkg/agent/cmd/common/logs.go b/internal/pkg/agent/cmd/common/logs.go new file mode 100644 index 00000000000..8eaf3b49162 --- /dev/null +++ b/internal/pkg/agent/cmd/common/logs.go @@ -0,0 +1,41 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package common + +import ( + "fmt" + "strings" + + "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent-libs/logp/configure" + "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" + "github.com/elastic/elastic-agent/internal/pkg/release" + "github.com/elastic/elastic-agent/pkg/core/logger" +) + +func ConfiguredLogger(cfg *configuration.Configuration, name string) (*logger.Logger, error) { + cfg.Settings.LoggingConfig.Beat = name + cfg.Settings.LoggingConfig.Level = logp.DebugLevel + internal, err := logger.MakeInternalFileOutput(cfg.Settings.LoggingConfig) + if err != nil { + return nil, err + } + + libC, err := logger.ToCommonConfig(cfg.Settings.LoggingConfig) + if err != nil { + return nil, err + } + + if err := configure.LoggingWithOutputs("", libC, internal); err != nil { + return nil, fmt.Errorf("error initializing logging: %w", err) + } + return logp.NewLogger(""), nil +} + +func TroubleshootMessage() string { + v := strings.Split(release.Version(), ".") + version := strings.Join(v[:2], ".") + return fmt.Sprintf("For help, please see our troubleshooting guide at https://www.elastic.co/guide/en/fleet/%s/fleet-troubleshooting.html", version) +} diff --git a/internal/pkg/agent/cmd/common/setup_config.go b/internal/pkg/agent/cmd/common/setup_config.go new file mode 100644 index 00000000000..8dd05d0aa2d --- /dev/null +++ b/internal/pkg/agent/cmd/common/setup_config.go @@ -0,0 +1,75 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package common + +import "time" + +// setup configuration + +type SetupConfig struct { + Fleet FleetConfig `config:"fleet"` + FleetServer FleetServerConfig `config:"fleet_server"` + Kibana KibanaConfig `config:"kibana"` +} + +type FleetConfig struct { + CA string `config:"ca"` + Enroll bool `config:"enroll"` + EnrollmentToken string `config:"enrollment_token"` + ID string `config:"id"` + ReplaceToken string `config:"replace_token"` + Force bool `config:"force"` + Insecure bool `config:"insecure"` + TokenName string `config:"token_name"` + TokenPolicyName string `config:"token_policy_name"` + URL string `config:"url"` + Headers map[string]string `config:"headers"` + DaemonTimeout time.Duration `config:"daemon_timeout"` + EnrollTimeout time.Duration `config:"enroll_timeout"` + Cert string `config:"cert"` + CertKey string `config:"cert_key"` +} + +type FleetServerConfig struct { + Cert string `config:"cert"` + CertKey string `config:"cert_key"` + PassphrasePath string `config:"key_passphrase_path"` + ClientAuth string `config:"client_authentication"` + Elasticsearch ElasticsearchConfig `config:"elasticsearch"` + Enable bool `config:"enable"` + Host string `config:"host"` + InsecureHTTP bool `config:"insecure_http"` + PolicyID string `config:"policy_id"` + Port string `config:"port"` + Headers map[string]string `config:"headers"` + Timeout time.Duration `config:"timeout"` +} + +type ElasticsearchConfig struct { + CA string `config:"ca"` + CATrustedFingerprint string `config:"ca_trusted_fingerprint"` + Host string `config:"host"` + ServiceToken string `config:"service_token"` + ServiceTokenPath string `config:"service_token_path"` + Insecure bool `config:"insecure"` + Cert string `config:"cert"` + CertKey string `config:"cert_key"` +} + +type KibanaConfig struct { + Fleet KibanaFleetConfig `config:"fleet"` + RetrySleepDuration time.Duration `config:"retry_sleep_duration"` + RetryMaxCount int `config:"retry_max_count"` + Headers map[string]string `config:"headers"` +} + +type KibanaFleetConfig struct { + CA string `config:"ca"` + Host string `config:"host"` + Username string `config:"username"` + Password string `config:"password"` + ServiceToken string `config:"service_token"` + ServiceTokenPath string `config:"service_token_path"` +} diff --git a/internal/pkg/agent/cmd/component.go b/internal/pkg/agent/cmd/component/component.go similarity index 88% rename from internal/pkg/agent/cmd/component.go rename to internal/pkg/agent/cmd/component/component.go index 0764ffd87b0..46125b69380 100644 --- a/internal/pkg/agent/cmd/component.go +++ b/internal/pkg/agent/cmd/component/component.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package component import ( "github.com/spf13/cobra" @@ -10,7 +10,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/cli" ) -func newComponentCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { +func NewComponentCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "component ", Short: "Tools to work on components", diff --git a/internal/pkg/agent/cmd/component_spec.go b/internal/pkg/agent/cmd/component/component_spec.go similarity index 98% rename from internal/pkg/agent/cmd/component_spec.go rename to internal/pkg/agent/cmd/component/component_spec.go index f033819d4b0..71111781d2e 100644 --- a/internal/pkg/agent/cmd/component_spec.go +++ b/internal/pkg/agent/cmd/component/component_spec.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package component import ( "fmt" diff --git a/internal/pkg/agent/cmd/container.go b/internal/pkg/agent/cmd/container/container.go similarity index 80% rename from internal/pkg/agent/cmd/container.go rename to internal/pkg/agent/cmd/container/container.go index f890b3187ea..28b96b3ba4e 100644 --- a/internal/pkg/agent/cmd/container.go +++ b/internal/pkg/agent/cmd/container/container.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package container import ( "bytes" @@ -11,7 +11,6 @@ import ( "encoding/json" "fmt" "io" - "io/fs" "net/url" "os" "os/exec" @@ -34,6 +33,8 @@ import ( "github.com/elastic/elastic-agent-libs/transport/tlscommon" "github.com/elastic/elastic-agent/internal/pkg/agent/application/enroll" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/agentrun" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/agent/storage" @@ -46,17 +47,15 @@ import ( "github.com/elastic/elastic-agent/pkg/component" "github.com/elastic/elastic-agent/pkg/core/logger" "github.com/elastic/elastic-agent/pkg/core/process" - "github.com/elastic/elastic-agent/pkg/utils" "github.com/elastic/elastic-agent/version" ) const ( requestRetrySleepEnv = "KIBANA_REQUEST_RETRY_SLEEP" maxRequestRetriesEnv = "KIBANA_REQUEST_RETRY_COUNT" - defaultRequestRetrySleep = "1s" // sleep 1 sec between retries for HTTP requests - defaultMaxRequestRetries = "30" // maximum number of retries for HTTP requests - agentBaseDirectory = "/usr/share/elastic-agent" // directory that holds all elastic-agent related files - defaultStateDirectory = agentBaseDirectory + "/state" // directory that will hold the state data + defaultRequestRetrySleep = "1s" // sleep 1 sec between retries for HTTP requests + defaultMaxRequestRetries = "30" // maximum number of + fleetInitTimeoutName = "FLEET_SERVER_INIT_TIMEOUT" logsPathPerms = 0775 ) @@ -65,7 +64,7 @@ const ( // a container to reference a token by name, without having to know what the generated UUID is for that name. var tokenNameStrip = regexp.MustCompile(`\s\([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\)$`) -func newContainerCommand(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewContainerCommand(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := cobra.Command{ Hidden: true, // not exposed over help; used by container entrypoint only Use: "container", @@ -167,7 +166,7 @@ occurs on every start of the container set FLEET_FORCE to 1. } func logError(streams *cli.IOStreams, err error) { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) } func logInfo(streams *cli.IOStreams, a ...interface{}) { @@ -175,7 +174,7 @@ func logInfo(streams *cli.IOStreams, a ...interface{}) { } func logContainerCmd(streams *cli.IOStreams) error { - logsPath := envWithDefault("", "LOGS_PATH") + logsPath := common.EnvWithDefault("", "LOGS_PATH") if logsPath != "" { // log this entire command to a file as well as to the passed streams if err := os.MkdirAll(logsPath, logsPathPerms); err != nil { @@ -195,11 +194,11 @@ func logContainerCmd(streams *cli.IOStreams) error { func containerCmd(streams *cli.IOStreams) error { // set paths early so all action below use the defined paths - if err := setPaths("", "", "", "", true); err != nil { + if err := common.SetPaths("", "", "", "", true); err != nil { return err } - elasticCloud := envBool("ELASTIC_AGENT_CLOUD") + elasticCloud := common.EnvBool("ELASTIC_AGENT_CLOUD") // if not in cloud mode, always run the agent runAgent := !elasticCloud // create access configuration from ENV and config files @@ -279,10 +278,10 @@ func containerCmd(streams *cli.IOStreams) error { return err } -func runContainerCmd(streams *cli.IOStreams, cfg setupConfig) error { +func runContainerCmd(streams *cli.IOStreams, cfg common.SetupConfig) error { var err error - initTimeout := envTimeout(fleetInitTimeoutName) + initTimeout := common.EnvTimeout(fleetInitTimeoutName) if cfg.FleetServer.Enable { err = ensureServiceToken(streams, &cfg) @@ -342,7 +341,7 @@ func runContainerCmd(streams *cli.IOStreams, cfg setupConfig) error { } } - return run(containerCfgOverrides, false, initTimeout, isContainer) + return agentrun.Run(containerCfgOverrides, false, initTimeout, isContainer) } // TokenResp is used to decode a response for generating a service token @@ -355,7 +354,7 @@ type TokenResp struct { // // If no token is specified it will try to use the value from service_token_path // If no filepath is specified it will use the elasticsearch username/password to request a new token from Kibana -func ensureServiceToken(streams *cli.IOStreams, cfg *setupConfig) error { +func ensureServiceToken(streams *cli.IOStreams, cfg *common.SetupConfig) error { // There's already a service token if cfg.Kibana.Fleet.ServiceToken != "" || cfg.FleetServer.Elasticsearch.ServiceToken != "" { return nil @@ -410,7 +409,7 @@ func ensureServiceToken(streams *cli.IOStreams, cfg *setupConfig) error { return nil } -func buildEnrollArgs(cfg setupConfig, token string, policyID string) ([]string, error) { +func buildEnrollArgs(cfg common.SetupConfig, token string, policyID string) ([]string, error) { args := []string{ "enroll", "-f", "-c", paths.ConfigFile(), @@ -426,7 +425,7 @@ func buildEnrollArgs(cfg setupConfig, token string, policyID string) ([]string, if !paths.IsVersionHome() { args = append(args, "--path.home.unversioned") } - if tags := envWithDefault("", "ELASTIC_AGENT_TAGS"); tags != "" { + if tags := common.EnvWithDefault("", "ELASTIC_AGENT_TAGS"); tags != "" { args = append(args, "--tag", tags) } if cfg.FleetServer.Enable { @@ -535,7 +534,7 @@ func buildEnrollArgs(cfg setupConfig, token string, policyID string) ([]string, return args, nil } -func buildFleetServerConnStr(cfg fleetServerConfig) (string, error) { +func buildFleetServerConnStr(cfg common.FleetServerConfig) (string, error) { u, err := url.Parse(cfg.Elasticsearch.Host) if err != nil { return "", err @@ -547,7 +546,7 @@ func buildFleetServerConnStr(cfg fleetServerConfig) (string, error) { return fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, path), nil } -func kibanaFetchPolicy(cfg setupConfig, client *kibana.Client, streams *cli.IOStreams) (*kibanaPolicy, error) { +func kibanaFetchPolicy(cfg common.SetupConfig, client *kibana.Client, streams *cli.IOStreams) (*kibanaPolicy, error) { var policies kibanaPolicies err := performGET(cfg, client, "/api/fleet/agent_policies", &policies, streams.Err, "Kibana fetch policy") if err != nil { @@ -556,7 +555,7 @@ func kibanaFetchPolicy(cfg setupConfig, client *kibana.Client, streams *cli.IOSt return findPolicy(cfg, policies.Items) } -func kibanaFetchToken(cfg setupConfig, client *kibana.Client, policy *kibanaPolicy, streams *cli.IOStreams, tokenName string) (string, error) { +func kibanaFetchToken(cfg common.SetupConfig, client *kibana.Client, policy *kibanaPolicy, streams *cli.IOStreams, tokenName string) (string, error) { var keys kibanaAPIKeys err := performGET(cfg, client, "/api/fleet/enrollment_api_keys", &keys, streams.Err, "Kibana fetch token") if err != nil { @@ -574,7 +573,7 @@ func kibanaFetchToken(cfg setupConfig, client *kibana.Client, policy *kibanaPoli return keyDetail.Item.APIKey, nil } -func kibanaClient(cfg kibanaConfig, headers map[string]string) (*kibana.Client, error) { +func kibanaClient(cfg common.KibanaConfig, headers map[string]string) (*kibana.Client, error) { var tls *tlscommon.Config if cfg.Fleet.CA != "" { tls = &tlscommon.Config{ @@ -596,7 +595,7 @@ func kibanaClient(cfg kibanaConfig, headers map[string]string) (*kibana.Client, }, 0, "Elastic-Agent", version.GetDefaultVersion(), version.Commit(), version.BuildTime().String()) } -func findPolicy(cfg setupConfig, policies []kibanaPolicy) (*kibanaPolicy, error) { +func findPolicy(cfg common.SetupConfig, policies []kibanaPolicy) (*kibanaPolicy, error) { policyID := "" policyName := cfg.Fleet.TokenPolicyName if cfg.FleetServer.Enable { @@ -634,72 +633,7 @@ func findKey(keys []kibanaAPIKey, policy *kibanaPolicy, tokenName string) (*kiba return nil, fmt.Errorf(`unable to find enrollment token named "%s" in policy "%s"`, tokenName, policy.Name) } -func envWithDefault(def string, keys ...string) string { - for _, key := range keys { - val, ok := os.LookupEnv(key) - if ok { - return val - } - } - return def -} - -func envBool(keys ...string) bool { - for _, key := range keys { - val, ok := os.LookupEnv(key) - if ok && isTrue(val) { - return true - } - } - return false -} - -func envTimeout(keys ...string) time.Duration { - for _, key := range keys { - val, ok := os.LookupEnv(key) - if ok { - dur, err := time.ParseDuration(val) - if err == nil { - return dur - } - } - } - return 0 -} - -func envMap(key string) map[string]string { - m := make(map[string]string) - prefix := key + "=" - for _, env := range os.Environ() { - if !strings.HasPrefix(env, prefix) { - continue - } - - envVal := strings.TrimPrefix(env, prefix) - - keyValue := strings.SplitN(envVal, "=", 2) - if len(keyValue) != 2 { - continue - } - - m[keyValue[0]] = keyValue[1] - } - - return m -} - -func isTrue(val string) bool { - trueVals := []string{"1", "true", "yes", "y"} - val = strings.ToLower(val) - for _, v := range trueVals { - if val == v { - return true - } - } - return false -} - -func performGET(cfg setupConfig, client *kibana.Client, path string, response interface{}, writer io.Writer, msg string) error { +func performGET(cfg common.SetupConfig, client *kibana.Client, path string, response interface{}, writer io.Writer, msg string) error { var lastErr error for i := 0; i < cfg.Kibana.RetryMaxCount; i++ { code, result, err := client.Request("GET", path, nil, nil, nil) @@ -786,14 +720,14 @@ func runLegacyAPMServer(streams *cli.IOStreams) (*process.Info, error) { } func containerCfgOverrides(cfg *configuration.Configuration) { - logsPath := envWithDefault("", "LOGS_PATH") + logsPath := common.EnvWithDefault("", "LOGS_PATH") if logsPath == "" { // when no LOGS_PATH defined the container should log to stderr cfg.Settings.LoggingConfig.ToStderr = true cfg.Settings.LoggingConfig.ToFiles = false } - eventsToStderrEnv := envWithDefault("false", "EVENTS_TO_STDERR") + eventsToStderrEnv := common.EnvWithDefault("false", "EVENTS_TO_STDERR") eventsToStderr, err := strconv.ParseBool(eventsToStderrEnv) if err != nil { logp.Warn("cannot parse EVENS_TO_STDERR='%s' as boolean, logging events to file'", eventsToStderrEnv) @@ -806,147 +740,6 @@ func containerCfgOverrides(cfg *configuration.Configuration) { configuration.OverrideDefaultContainerGRPCPort(cfg.Settings.GRPC) } -func setPaths(statePath, configPath, logsPath, socketPath string, writePaths bool) error { - statePath = envWithDefault(statePath, "STATE_PATH") - if statePath == "" { - statePath = defaultStateDirectory - } - - topPath := filepath.Join(statePath, "data") - configPath = envWithDefault(configPath, "CONFIG_PATH") - if configPath == "" { - configPath = statePath - } - if _, err := os.Stat(configPath); errors.Is(err, fs.ErrNotExist) { - if err := os.MkdirAll(configPath, 0755); err != nil { - return fmt.Errorf("cannot create folders for config path '%s': %w", configPath, err) - } - } - - if socketPath == "" { - socketPath = utils.SocketURLWithFallback(statePath, topPath) - } - // ensure that the directory and sub-directory data exists - if err := os.MkdirAll(topPath, 0755); err != nil { - return fmt.Errorf("preparing STATE_PATH(%s) failed: %w", statePath, err) - } - // ensure that the elastic-agent.yml exists in the state directory or if given in the config directory - baseConfig := filepath.Join(configPath, paths.DefaultConfigName) - if _, err := os.Stat(baseConfig); os.IsNotExist(err) { - if err := copyFile(baseConfig, paths.ConfigFile(), 0); err != nil { - return err - } - } - - originalInstall := paths.Install() - paths.SetTop(topPath) - paths.SetConfig(configPath) - paths.SetControlSocket(socketPath) - // when custom top path is provided the home directory is not versioned - paths.SetVersionHome(false) - // install path stays on container default mount (otherwise a bind mounted directory could have noexec set) - paths.SetInstall(originalInstall) - // set LOGS_PATH is given - logsPath = envWithDefault(logsPath, "LOGS_PATH") - if logsPath != "" { - paths.SetLogs(logsPath) - // ensure that the logs directory exists - if err := os.MkdirAll(filepath.Join(logsPath), logsPathPerms); err != nil { - return fmt.Errorf("preparing LOGS_PATH(%s) failed: %w", logsPath, err) - } - } - - // ensure that the internal logger directory exists - loggerPath := filepath.Join(paths.Home(), logger.DefaultLogDirectory) - if err := os.MkdirAll(loggerPath, logsPathPerms); err != nil { - return fmt.Errorf("preparing internal log path(%s) failed: %w", loggerPath, err) - } - - // persist the paths so other commands in the container will use the correct paths - if writePaths { - if err := writeContainerPaths(statePath, configPath, logsPath, socketPath); err != nil { - return err - } - } - return nil -} - -type containerPaths struct { - StatePath string `config:"state_path" yaml:"state_path"` - ConfigPath string `config:"config_path" yaml:"config_path,omitempty"` - LogsPath string `config:"logs_path" yaml:"logs_path,omitempty"` - SocketPath string `config:"socket_path" yaml:"socket_path,omitempty"` -} - -func writeContainerPaths(statePath, configPath, logsPath, socketPath string) error { - pathFile := filepath.Join(statePath, "container-paths.yml") - fp, err := os.Create(pathFile) - if err != nil { - return fmt.Errorf("failed creating %s: %w", pathFile, err) - } - b, err := yaml.Marshal(containerPaths{ - StatePath: statePath, - ConfigPath: configPath, - LogsPath: logsPath, - SocketPath: socketPath, - }) - if err != nil { - return fmt.Errorf("failed to marshal for %s: %w", pathFile, err) - } - _, err = fp.Write(b) - if err != nil { - return fmt.Errorf("failed to write %s: %w", pathFile, err) - } - return nil -} - -func tryContainerLoadPaths() error { - statePath := envWithDefault("", "STATE_PATH") - if statePath == "" { - statePath = defaultStateDirectory - } - pathFile := filepath.Join(statePath, "container-paths.yml") - _, err := os.Stat(pathFile) - if os.IsNotExist(err) { - // no container-paths.yml file exists, so nothing to do - return nil - } - cfg, err := config.LoadFile(pathFile) - if err != nil { - return fmt.Errorf("failed to load %s: %w", pathFile, err) - } - var paths containerPaths - err = cfg.UnpackTo(&paths) - if err != nil { - return fmt.Errorf("failed to unpack %s: %w", pathFile, err) - } - return setPaths(paths.StatePath, paths.ConfigPath, paths.LogsPath, paths.SocketPath, false) -} - -func copyFile(destPath string, srcPath string, mode os.FileMode) error { - // if mode is unset; set to the same as the source file - if mode == 0 { - info, err := os.Stat(srcPath) - if err == nil { - // ignoring error because; os.Open will also error if the file cannot be stat'd - mode = info.Mode() - } - } - - src, err := os.Open(srcPath) - if err != nil { - return err - } - defer src.Close() - dest, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY, mode) - if err != nil { - return err - } - defer dest.Close() - _, err = io.Copy(dest, src) - return err -} - type kibanaPolicy struct { ID string `json:"id"` Name string `json:"name"` @@ -975,32 +768,6 @@ type kibanaAPIKeyDetail struct { Item kibanaAPIKey `json:"item"` } -func envDurationWithDefault(defVal string, keys ...string) (time.Duration, error) { - valStr := defVal - for _, key := range keys { - val, ok := os.LookupEnv(key) - if ok { - valStr = val - break - } - } - - return time.ParseDuration(valStr) -} - -func envIntWithDefault(defVal string, keys ...string) (int, error) { - valStr := defVal - for _, key := range keys { - val, ok := os.LookupEnv(key) - if ok { - valStr = val - break - } - } - - return strconv.Atoi(valStr) -} - // isContainer changes the platform details to be a container. // // Runtime specifications can provide unique configurations when running in a container, this ensures that @@ -1028,7 +795,7 @@ func (a *agentInfo) AgentID() string { } // shouldFleetEnroll returns true if the elastic-agent should enroll to fleet. -func shouldFleetEnroll(setupCfg setupConfig) (bool, error) { +func shouldFleetEnroll(setupCfg common.SetupConfig) (bool, error) { if !setupCfg.Fleet.Enroll { // Enrollment is explicitly disabled in the setup configuration. return false, nil @@ -1203,3 +970,72 @@ func ackFleet(ctx context.Context, client fleetclient.Sender, agentID string) er } }, &backoff.ConstantBackOff{Interval: retryInterval}) } + +func defaultAccessConfig() (common.SetupConfig, error) { + retrySleepDuration, err := common.EnvDurationWithDefault(defaultRequestRetrySleep, requestRetrySleepEnv) + if err != nil { + return common.SetupConfig{}, err + } + + retryMaxCount, err := common.EnvIntWithDefault(defaultMaxRequestRetries, maxRequestRetriesEnv) + if err != nil { + return common.SetupConfig{}, err + } + + cfg := common.SetupConfig{ + Fleet: common.FleetConfig{ + CA: common.EnvWithDefault("", "FLEET_CA", "KIBANA_CA", "ELASTICSEARCH_CA"), + Enroll: common.EnvBool("FLEET_ENROLL", "FLEET_SERVER_ENABLE"), + EnrollmentToken: common.EnvWithDefault("", "FLEET_ENROLLMENT_TOKEN"), + ID: common.EnvWithDefault("", "ELASTIC_AGENT_ID"), + ReplaceToken: common.EnvWithDefault("", "FLEET_REPLACE_TOKEN"), + Force: common.EnvBool("FLEET_FORCE"), + Insecure: common.EnvBool("FLEET_INSECURE"), + TokenName: common.EnvWithDefault("Default", "FLEET_TOKEN_NAME"), + TokenPolicyName: common.EnvWithDefault("", "FLEET_TOKEN_POLICY_NAME"), + URL: common.EnvWithDefault("", "FLEET_URL"), + Headers: common.EnvMap("FLEET_HEADER"), + DaemonTimeout: common.EnvTimeout("FLEET_DAEMON_TIMEOUT"), + EnrollTimeout: common.EnvTimeout("FLEET_ENROLL_TIMEOUT"), + Cert: common.EnvWithDefault("", "ELASTIC_AGENT_CERT"), + CertKey: common.EnvWithDefault("", "ELASTIC_AGENT_CERT_KEY"), + }, + FleetServer: common.FleetServerConfig{ + Cert: common.EnvWithDefault("", "FLEET_SERVER_CERT"), + CertKey: common.EnvWithDefault("", "FLEET_SERVER_CERT_KEY"), + PassphrasePath: common.EnvWithDefault("", "FLEET_SERVER_CERT_KEY_PASSPHRASE"), + ClientAuth: common.EnvWithDefault("none", "FLEET_SERVER_CLIENT_AUTH"), + Elasticsearch: common.ElasticsearchConfig{ + Host: common.EnvWithDefault("http://elasticsearch:9200", "FLEET_SERVER_ELASTICSEARCH_HOST", "ELASTICSEARCH_HOST"), + ServiceToken: common.EnvWithDefault("", "FLEET_SERVER_SERVICE_TOKEN"), + ServiceTokenPath: common.EnvWithDefault("", "FLEET_SERVER_SERVICE_TOKEN_PATH"), + CA: common.EnvWithDefault("", "FLEET_SERVER_ELASTICSEARCH_CA", "ELASTICSEARCH_CA"), + CATrustedFingerprint: common.EnvWithDefault("", "FLEET_SERVER_ELASTICSEARCH_CA_TRUSTED_FINGERPRINT"), + Insecure: common.EnvBool("FLEET_SERVER_ELASTICSEARCH_INSECURE"), + Cert: common.EnvWithDefault("", "FLEET_SERVER_ES_CERT"), + CertKey: common.EnvWithDefault("", "FLEET_SERVER_ES_CERT_KEY"), + }, + Enable: common.EnvBool("FLEET_SERVER_ENABLE"), + Host: common.EnvWithDefault("", "FLEET_SERVER_HOST"), + InsecureHTTP: common.EnvBool("FLEET_SERVER_INSECURE_HTTP"), + PolicyID: common.EnvWithDefault("", "FLEET_SERVER_POLICY_ID", "FLEET_SERVER_POLICY"), + Port: common.EnvWithDefault("", "FLEET_SERVER_PORT"), + Headers: common.EnvMap("FLEET_HEADER"), + Timeout: common.EnvTimeout("FLEET_SERVER_TIMEOUT"), + }, + Kibana: common.KibanaConfig{ + Fleet: common.KibanaFleetConfig{ + Host: common.EnvWithDefault("http://kibana:5601", "KIBANA_FLEET_HOST", "KIBANA_HOST"), + Username: common.EnvWithDefault("elastic", "KIBANA_FLEET_USERNAME", "KIBANA_USERNAME", "ELASTICSEARCH_USERNAME"), + Password: common.EnvWithDefault("changeme", "KIBANA_FLEET_PASSWORD", "KIBANA_PASSWORD", "ELASTICSEARCH_PASSWORD"), + ServiceToken: common.EnvWithDefault("", "KIBANA_FLEET_SERVICE_TOKEN", "FLEET_SERVER_SERVICE_TOKEN"), + ServiceTokenPath: common.EnvWithDefault("", "KIBANA_FLEET_SERVICE_TOKEN_PATH", "FLEET_SERVER_SERVICE_TOKEN_PATH"), + CA: common.EnvWithDefault("", "KIBANA_FLEET_CA", "KIBANA_CA", "ELASTICSEARCH_CA"), + }, + RetrySleepDuration: retrySleepDuration, + RetryMaxCount: retryMaxCount, + Headers: common.EnvMap("FLEET_KIBANA_HEADER"), + }, + } + return cfg, nil +} diff --git a/internal/pkg/agent/cmd/container_init_linux.go b/internal/pkg/agent/cmd/container/container_init_linux.go similarity index 99% rename from internal/pkg/agent/cmd/container_init_linux.go rename to internal/pkg/agent/cmd/container/container_init_linux.go index 3469f0716db..dafb4aada1d 100644 --- a/internal/pkg/agent/cmd/container_init_linux.go +++ b/internal/pkg/agent/cmd/container/container_init_linux.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package container // The initialization steps in this file are specifically for the elastic-agent running inside a container. // They aim to adjust the ownership of agent-related paths to match the elastic-agent process's uid @@ -57,7 +57,7 @@ func logWarning(streams *cli.IOStreams, err error) { // - chown all agent-related paths // // Note that to avoid disrupting effects, any error is logged as a warning, but not returned. -func initContainer(streams *cli.IOStreams) { +func InitContainer(streams *cli.IOStreams) { isRoot, err := utils.HasRoot() if err != nil { logWarning(streams, err) diff --git a/internal/pkg/agent/cmd/container_init_other.go b/internal/pkg/agent/cmd/container/container_init_other.go similarity index 85% rename from internal/pkg/agent/cmd/container_init_other.go rename to internal/pkg/agent/cmd/container/container_init_other.go index 76a569a3ba1..1c2f42508fc 100644 --- a/internal/pkg/agent/cmd/container_init_other.go +++ b/internal/pkg/agent/cmd/container/container_init_other.go @@ -4,10 +4,10 @@ //go:build !linux -package cmd +package container import ( "github.com/elastic/elastic-agent/internal/pkg/cli" ) -func initContainer(_ *cli.IOStreams) {} +func InitContainer(_ *cli.IOStreams) {} diff --git a/internal/pkg/agent/cmd/container_init_test.go b/internal/pkg/agent/cmd/container/container_init_test.go similarity index 99% rename from internal/pkg/agent/cmd/container_init_test.go rename to internal/pkg/agent/cmd/container/container_init_test.go index 2efbb2172a4..6ca46854d0f 100644 --- a/internal/pkg/agent/cmd/container_init_test.go +++ b/internal/pkg/agent/cmd/container/container_init_test.go @@ -4,7 +4,7 @@ //go:build linux -package cmd +package container import ( "errors" diff --git a/internal/pkg/agent/cmd/container_test.go b/internal/pkg/agent/cmd/container/container_test.go similarity index 86% rename from internal/pkg/agent/cmd/container_test.go rename to internal/pkg/agent/cmd/container/container_test.go index fd60a8fcc8f..5f340be2cc6 100644 --- a/internal/pkg/agent/cmd/container_test.go +++ b/internal/pkg/agent/cmd/container/container_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package container import ( "context" @@ -15,7 +15,6 @@ import ( "os" "strings" "testing" - "time" "gopkg.in/yaml.v2" @@ -23,6 +22,7 @@ import ( "github.com/stretchr/testify/require" "github.com/elastic/elastic-agent-libs/kibana" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" "github.com/elastic/elastic-agent/internal/pkg/agent/storage" "github.com/elastic/elastic-agent/internal/pkg/cli" @@ -35,49 +35,14 @@ import ( mockFleetClient "github.com/elastic/elastic-agent/testing/mocks/internal_/pkg/fleetapi/client" ) -func TestEnvWithDefault(t *testing.T) { - def := "default" - key1 := "ENV_WITH_DEFAULT_1" - key2 := "ENV_WITH_DEFAULT_2" - - res := envWithDefault(def, key1, key2) - - require.Equal(t, def, res) - - t.Setenv(key1, "key1") - - t.Setenv(key2, "key2") - - res2 := envWithDefault(def, key1, key2) - require.Equal(t, "key1", res2) -} - -func TestEnvBool(t *testing.T) { - key := "TEST_ENV_BOOL" - - t.Setenv(key, "true") - - res := envBool(key) - require.True(t, res) -} - -func TestEnvTimeout(t *testing.T) { - key := "TEST_ENV_TIMEOUT" - - t.Setenv(key, "10s") - - res := envTimeout(key) - require.Equal(t, time.Second*10, res) -} - func TestContainerTestPaths(t *testing.T) { cases := map[string]struct { config string - expected containerPaths + expected common.ContainerPaths }{ "only_state_path": { config: `state_path: /foo/bar/state`, - expected: containerPaths{ + expected: common.ContainerPaths{ StatePath: "/foo/bar/state", ConfigPath: "", LogsPath: "", @@ -85,7 +50,7 @@ func TestContainerTestPaths(t *testing.T) { }, "only_config_path": { config: `config_path: /foo/bar/config`, - expected: containerPaths{ + expected: common.ContainerPaths{ StatePath: "", ConfigPath: "/foo/bar/config", LogsPath: "", @@ -93,7 +58,7 @@ func TestContainerTestPaths(t *testing.T) { }, "only_logs_path": { config: `logs_path: /foo/bar/logs`, - expected: containerPaths{ + expected: common.ContainerPaths{ StatePath: "", ConfigPath: "", LogsPath: "/foo/bar/logs", @@ -106,7 +71,7 @@ func TestContainerTestPaths(t *testing.T) { cfg, err := config.NewConfigFrom(c.config) require.NoError(t, err) - var paths containerPaths + var paths common.ContainerPaths err = cfg.UnpackTo(&paths) require.NoError(t, err) @@ -117,15 +82,15 @@ func TestContainerTestPaths(t *testing.T) { func TestBuildEnrollArgs(t *testing.T) { cases := map[string]struct { - cfg setupConfig + cfg common.SetupConfig expect []string err error }{ "service token passes": { - cfg: setupConfig{ - FleetServer: fleetServerConfig{ + cfg: common.SetupConfig{ + FleetServer: common.FleetServerConfig{ Enable: true, - Elasticsearch: elasticsearchConfig{ + Elasticsearch: common.ElasticsearchConfig{ Host: "http://localhost:9200", ServiceToken: "token-val", }, @@ -135,10 +100,10 @@ func TestBuildEnrollArgs(t *testing.T) { err: nil, }, "service token path passes": { - cfg: setupConfig{ - FleetServer: fleetServerConfig{ + cfg: common.SetupConfig{ + FleetServer: common.FleetServerConfig{ Enable: true, - Elasticsearch: elasticsearchConfig{ + Elasticsearch: common.ElasticsearchConfig{ Host: "http://localhost:9200", ServiceTokenPath: "/path/to/token", }, @@ -148,10 +113,10 @@ func TestBuildEnrollArgs(t *testing.T) { err: nil, }, "service token path preferred": { - cfg: setupConfig{ - FleetServer: fleetServerConfig{ + cfg: common.SetupConfig{ + FleetServer: common.FleetServerConfig{ Enable: true, - Elasticsearch: elasticsearchConfig{ + Elasticsearch: common.ElasticsearchConfig{ Host: "http://localhost:9200", ServiceTokenPath: "/path/to/token", ServiceToken: "token-val", @@ -162,15 +127,15 @@ func TestBuildEnrollArgs(t *testing.T) { err: nil, }, "mTLS flags": { - cfg: setupConfig{ - Fleet: fleetConfig{ + cfg: common.SetupConfig{ + Fleet: common.FleetConfig{ Cert: "/path/to/agent.crt", CertKey: "/path/to/agent.key", }, - FleetServer: fleetServerConfig{ + FleetServer: common.FleetServerConfig{ Enable: true, ClientAuth: "optional", - Elasticsearch: elasticsearchConfig{ + Elasticsearch: common.ElasticsearchConfig{ Cert: "/path/to/es.crt", CertKey: "/path/to/es.key", }, @@ -252,7 +217,7 @@ func TestKibanaFetchToken(t *testing.T) { HTTP: &http.Client{}, }, } - ak, err := kibanaFetchToken(setupConfig{Kibana: kibanaConfig{RetryMaxCount: 1}}, client, &policy, cli.NewIOStreams(), "tokenName") + ak, err := kibanaFetchToken(common.SetupConfig{Kibana: common.KibanaConfig{RetryMaxCount: 1}}, client, &policy, cli.NewIOStreams(), "tokenName") require.NoError(t, err) require.Equal(t, "apiKey", ak) }) @@ -275,7 +240,7 @@ func TestShouldEnroll(t *testing.T) { fleetNetworkErr := errors.New("fleet network error") for name, tc := range map[string]struct { - cfg setupConfig + cfg common.SetupConfig statFn func(path string) (os.FileInfo, error) encryptedDiskStoreFn func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage fleetClientFn func(t *testing.T) client.Sender @@ -284,21 +249,21 @@ func TestShouldEnroll(t *testing.T) { expectedErr error }{ "should not enroll if fleet enroll is disabled": { - cfg: setupConfig{Fleet: fleetConfig{Enroll: false}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: false}}, expectedShouldEnroll: false, }, "should enroll if fleet force is true": { - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, Force: true}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, Force: true}}, expectedShouldEnroll: true, }, "should enroll if config file does not exist": { statFn: func(path string) (os.FileInfo, error) { return nil, os.ErrNotExist }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, Force: true}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, Force: true}}, expectedShouldEnroll: true, }, "should enroll on agent id but no existing id": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", ID: "diff-agent-id"}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", ID: "diff-agent-id"}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -315,7 +280,7 @@ func TestShouldEnroll(t *testing.T) { }, "should enroll on agent id but diff agent id": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", ID: "diff-agent-id"}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", ID: "diff-agent-id"}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -333,7 +298,7 @@ func TestShouldEnroll(t *testing.T) { }, "should enroll on fleet url change": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1"}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1"}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -351,7 +316,7 @@ func TestShouldEnroll(t *testing.T) { }, "should enroll on fleet token change": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentTokenOther}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentTokenOther}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -370,7 +335,7 @@ func TestShouldEnroll(t *testing.T) { }, "should enroll on replace token change": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken, ReplaceToken: replaceTokenOther}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken, ReplaceToken: replaceTokenOther}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -390,7 +355,7 @@ func TestShouldEnroll(t *testing.T) { }, "should enroll on unauthorized api": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -423,7 +388,7 @@ func TestShouldEnroll(t *testing.T) { }, "should not enroll on no changes": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -459,7 +424,7 @@ func TestShouldEnroll(t *testing.T) { }, "should not enroll on no changes with agent ID and replace token": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", ID: "custom-id", EnrollmentToken: enrollmentToken, ReplaceToken: replaceToken}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", ID: "custom-id", EnrollmentToken: enrollmentToken, ReplaceToken: replaceToken}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -497,7 +462,7 @@ func TestShouldEnroll(t *testing.T) { }, "should fail on fleet network errors": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -522,7 +487,7 @@ func TestShouldEnroll(t *testing.T) { }, "should not update the enrollment token hash if it does not exist in setup configuration": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", EnrollmentToken: ""}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", EnrollmentToken: ""}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -549,7 +514,7 @@ func TestShouldEnroll(t *testing.T) { }, "should not update the replace token hash if it does not exist in setup configuration": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", EnrollmentToken: "", ReplaceToken: ""}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", EnrollmentToken: "", ReplaceToken: ""}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -576,7 +541,7 @@ func TestShouldEnroll(t *testing.T) { }, "should not enroll on no changes and update the stored enrollment token hash": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: @@ -615,7 +580,7 @@ func TestShouldEnroll(t *testing.T) { }, "should not enroll on no changes and update the stored enrollment and replace token hash": { statFn: func(path string) (os.FileInfo, error) { return nil, nil }, - cfg: setupConfig{Fleet: fleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken, ReplaceToken: replaceToken}}, + cfg: common.SetupConfig{Fleet: common.FleetConfig{Enroll: true, URL: "host1", EnrollmentToken: enrollmentToken, ReplaceToken: replaceToken}}, encryptedDiskStoreFn: func(t *testing.T, savedConfig *configuration.Configuration) storage.Storage { m := mockStorage.NewStorage(t) m.On("Load").Return(io.NopCloser(strings.NewReader(`fleet: diff --git a/internal/pkg/agent/cmd/diagnostics.go b/internal/pkg/agent/cmd/diagnostics/diagnostics.go similarity index 94% rename from internal/pkg/agent/cmd/diagnostics.go rename to internal/pkg/agent/cmd/diagnostics/diagnostics.go index ef13a4704a2..a60e4aef9d7 100644 --- a/internal/pkg/agent/cmd/diagnostics.go +++ b/internal/pkg/agent/cmd/diagnostics/diagnostics.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package diagnostics import ( "context" @@ -17,18 +17,19 @@ import ( "github.com/spf13/cobra" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/internal/pkg/diagnostics" ) -func newDiagnosticsCommand(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewDiagnosticsCommand(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "diagnostics", Short: "Gather diagnostics information from the Elastic Agent and write it to a zip archive", Long: "This command gathers diagnostics information from the Elastic Agent and writes it to a zip archive.", Run: func(c *cobra.Command, args []string) { if err := diagnosticCmd(streams, c); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(1) } }, @@ -54,7 +55,7 @@ func diagnosticCmd(streams *cli.IOStreams, cmd *cobra.Command) error { return fmt.Errorf("cannot get 'exclude-events' flag: %w", err) } - ctx := handleSignal(context.Background()) + ctx := common.HandleSignal(context.Background()) // 1st create the file to store the diagnostics, if it fails, anything else // is pointless. diff --git a/internal/pkg/agent/cmd/diagnostics_test.go b/internal/pkg/agent/cmd/diagnostics/diagnostics_test.go similarity index 98% rename from internal/pkg/agent/cmd/diagnostics_test.go rename to internal/pkg/agent/cmd/diagnostics/diagnostics_test.go index 95d3e15c6e7..64ab9b7a7d9 100644 --- a/internal/pkg/agent/cmd/diagnostics_test.go +++ b/internal/pkg/agent/cmd/diagnostics/diagnostics_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package diagnostics import ( "os" diff --git a/internal/pkg/agent/cmd/include.go b/internal/pkg/agent/cmd/include.go index ab2ce160682..15de301b3f9 100644 --- a/internal/pkg/agent/cmd/include.go +++ b/internal/pkg/agent/cmd/include.go @@ -6,6 +6,7 @@ package cmd import ( // include the composable providers + _ "github.com/elastic/elastic-agent/internal/pkg/agent/agentservice" _ "github.com/elastic/elastic-agent/internal/pkg/composable/providers/agent" _ "github.com/elastic/elastic-agent/internal/pkg/composable/providers/docker" _ "github.com/elastic/elastic-agent/internal/pkg/composable/providers/env" diff --git a/internal/pkg/agent/cmd/inspect.go b/internal/pkg/agent/cmd/inspect/inspect.go similarity index 97% rename from internal/pkg/agent/cmd/inspect.go rename to internal/pkg/agent/cmd/inspect/inspect.go index b71096626f1..0bebd4c4a50 100644 --- a/internal/pkg/agent/cmd/inspect.go +++ b/internal/pkg/agent/cmd/inspect/inspect.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package inspect import ( "context" @@ -20,6 +20,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/info" "github.com/elastic/elastic-agent/internal/pkg/agent/application/monitoring" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/agent/transpiler" @@ -34,7 +35,7 @@ import ( "github.com/elastic/elastic-agent/pkg/utils" ) -func newInspectCommandWithArgs(s []string, streams *cli.IOStreams) *cobra.Command { +func NewInspectCommandWithArgs(s []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "inspect", Short: "Show current configuration of the Elastic Agent", @@ -58,7 +59,7 @@ wait that amount of time before using the variables for the configuration. ctx, cancel := context.WithCancel(context.Background()) service.HandleSignals(func() {}, cancel) if err := inspectConfig(ctx, paths.ConfigFile(), opts, streams); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(1) } }, @@ -111,7 +112,7 @@ variables for the configuration. service.HandleSignals(func() {}, cancel) if err := inspectComponents(ctx, paths.ConfigFile(), opts, streams); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(1) } }, @@ -287,7 +288,7 @@ func inspectComponents(ctx context.Context, cfgPath string, opts inspectComponen return err } - comps, err := getComponentsFromPolicy(ctx, l, cfgPath, opts.variablesWait) + comps, err := GetComponentsFromPolicy(ctx, l, cfgPath, opts.variablesWait) if err != nil { // error already includes the context return err @@ -352,7 +353,7 @@ func inspectComponents(ctx context.Context, cfgPath string, opts inspectComponen return printComponents(allowed, blocked, streams) } -func getComponentsFromPolicy(ctx context.Context, l *logger.Logger, cfgPath string, variablesWait time.Duration, platformModifiers ...component.PlatformModifier) ([]component.Component, error) { +func GetComponentsFromPolicy(ctx context.Context, l *logger.Logger, cfgPath string, variablesWait time.Duration, platformModifiers ...component.PlatformModifier) ([]component.Component, error) { // Load the requirements before trying to load the configuration. These should always load // even if the configuration is wrong. platform, err := component.LoadPlatformDetail(platformModifiers...) diff --git a/internal/pkg/agent/cmd/enroll.go b/internal/pkg/agent/cmd/install/enroll.go similarity index 97% rename from internal/pkg/agent/cmd/enroll.go rename to internal/pkg/agent/cmd/install/enroll.go index e023897a645..2b77ba237d8 100644 --- a/internal/pkg/agent/cmd/enroll.go +++ b/internal/pkg/agent/cmd/install/enroll.go @@ -2,18 +2,16 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package install import ( "context" "fmt" "os" - "os/signal" "path/filepath" "runtime" "strconv" "strings" - "syscall" "time" "github.com/spf13/cobra" @@ -21,6 +19,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/enroll" "github.com/elastic/elastic-agent/internal/pkg/agent/application/info" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/agent/storage" @@ -38,15 +37,15 @@ const ( fromInstallGroupArg = "from-install-group" ) -func newEnrollCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewEnrollCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "enroll", Short: "Enroll the Elastic Agent into Fleet", Long: "This command will enroll the Elastic Agent into Fleet.", Run: func(c *cobra.Command, args []string) { if err := doEnroll(streams, c); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) - logExternal(fmt.Sprintf("%s enroll failed: %s", paths.BinaryName, err)) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) + common.LogExternal(fmt.Sprintf("%s enroll failed: %s", paths.BinaryName, err)) os.Exit(1) } }, @@ -484,7 +483,7 @@ func doEnroll(streams *cli.IOStreams, cmd *cobra.Command) error { key, _ := cmd.Flags().GetString("elastic-agent-cert-key") keyPassphrase, _ := cmd.Flags().GetString("elastic-agent-cert-key-passphrase") - ctx := handleSignal(context.Background()) + ctx := common.HandleSignal(context.Background()) if enrollTimeout > 0 { eCtx, cancel := context.WithTimeout(ctx, enrollTimeout) @@ -572,7 +571,7 @@ func doEnroll(streams *cli.IOStreams, cmd *cobra.Command) error { storeOpts..., ) - c, err := newEnrollCmd( + c, err := NewEnrollCmd( logger, &options, pathConfigFile, @@ -586,26 +585,6 @@ func doEnroll(streams *cli.IOStreams, cmd *cobra.Command) error { return c.Execute(ctx, streams) } -func handleSignal(ctx context.Context) context.Context { - ctx, cfunc := context.WithCancel(ctx) - - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - - go func() { - select { - case <-sigs: - cfunc() - case <-ctx.Done(): - } - - signal.Stop(sigs) - close(sigs) - }() - - return ctx -} - func mapFromEnvList(envList []string) map[string]string { m := make(map[string]string) for _, kv := range envList { diff --git a/internal/pkg/agent/cmd/enroll_cmd.go b/internal/pkg/agent/cmd/install/enroll_cmd.go similarity index 97% rename from internal/pkg/agent/cmd/enroll_cmd.go rename to internal/pkg/agent/cmd/install/enroll_cmd.go index de2c6af5bce..ee3e1521a31 100644 --- a/internal/pkg/agent/cmd/enroll_cmd.go +++ b/internal/pkg/agent/cmd/install/enroll_cmd.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package install import ( "bytes" @@ -21,6 +21,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/enroll" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" "github.com/elastic/elastic-agent/internal/pkg/agent/application/secret" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/agent/perms" "github.com/elastic/elastic-agent/internal/pkg/agent/vault" @@ -46,8 +47,7 @@ const ( ) var ( - enrollDelay = 1 * time.Second // max delay to start enrollment - daemonTimeout = 30 * time.Second // max amount of for communication to running Agent daemon + enrollDelay = 1 * time.Second // max delay to start enrollment ) type saver interface { @@ -69,7 +69,7 @@ type enrollCmd struct { } // newEnrollCmd creates a new enrollment with the given store. -func newEnrollCmd( +func NewEnrollCmd( log *logger.Logger, options *enroll.EnrollOptions, configPath string, @@ -238,7 +238,7 @@ func (c *enrollCmd) fleetServerBootstrap(ctx context.Context, persistentConfig m if c.options.FleetServer.InternalPort == 0 { c.options.FleetServer.InternalPort = defaultFleetServerInternalPort } - _, err := getDaemonState(ctx) + _, err := common.GetDaemonState(ctx) if err != nil { if !c.options.FleetServer.SpawnAgent { // wait longer to try and communicate with the Elastic Agent @@ -471,18 +471,6 @@ func yamlToReader(in interface{}) (io.Reader, error) { return bytes.NewReader(data), nil } -func getDaemonState(ctx context.Context) (*client.AgentState, error) { - ctx, cancel := context.WithTimeout(ctx, daemonTimeout) - defer cancel() - daemon := client.New() - err := daemon.Connect(ctx) - if err != nil { - return nil, err - } - defer daemon.Disconnect() - return daemon.State(ctx) -} - type waitResult struct { enrollmentToken string err error @@ -520,7 +508,7 @@ func waitForFleetServer(ctx context.Context, agentSubproc <-chan *os.ProcessStat timeout)} } - state, err := getDaemonState(innerCtx) + state, err := common.GetDaemonState(innerCtx) if errors.Is(err, context.Canceled) { resChan <- waitResult{err: err} return diff --git a/internal/pkg/agent/cmd/enroll_cmd_nofips_test.go b/internal/pkg/agent/cmd/install/enroll_cmd_nofips_test.go similarity index 98% rename from internal/pkg/agent/cmd/enroll_cmd_nofips_test.go rename to internal/pkg/agent/cmd/install/enroll_cmd_nofips_test.go index 81a6310cb80..227f49c62a5 100644 --- a/internal/pkg/agent/cmd/enroll_cmd_nofips_test.go +++ b/internal/pkg/agent/cmd/install/enroll_cmd_nofips_test.go @@ -4,7 +4,7 @@ //go:build !requirefips -package cmd +package install import ( "context" @@ -91,7 +91,7 @@ func Test_Enroll_mTLS(t *testing.T) { SkipCreateSecret: skipCreateSecret, SkipDaemonRestart: true, } - cmd, err := newEnrollCmd( + cmd, err := NewEnrollCmd( log, &enrollOptions, "", diff --git a/internal/pkg/agent/cmd/enroll_cmd_test.go b/internal/pkg/agent/cmd/install/enroll_cmd_test.go similarity index 96% rename from internal/pkg/agent/cmd/enroll_cmd_test.go rename to internal/pkg/agent/cmd/install/enroll_cmd_test.go index dda79a2b52a..9e67b3f6f1e 100644 --- a/internal/pkg/agent/cmd/enroll_cmd_test.go +++ b/internal/pkg/agent/cmd/install/enroll_cmd_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package install import ( "bytes" @@ -109,7 +109,7 @@ func TestEnroll(t *testing.T) { store := &mockSaver{} failCall := store.On("Save", mock.Anything).Return(errors.New("fail to save")).Once() store.On("Save", mock.Anything).Return(nil).NotBefore(failCall).Once() - cmd, err := newEnrollCmd( + cmd, err := NewEnrollCmd( log, &enroll.EnrollOptions{ URL: url, @@ -182,7 +182,7 @@ func TestEnroll(t *testing.T) { return cfg.Client.Host == host && cfg.AccessAPIKey == "my-access-api-key" })).Return(nil).Once() - cmd, err := newEnrollCmd( + cmd, err := NewEnrollCmd( log, &enroll.EnrollOptions{ URL: url, @@ -251,7 +251,7 @@ func TestEnroll(t *testing.T) { } return cfg.Client.Host == host && cfg.AccessAPIKey == "my-access-api-key" })).Return(nil).Once() - cmd, err := newEnrollCmd( + cmd, err := NewEnrollCmd( log, &enroll.EnrollOptions{ URL: url, @@ -321,7 +321,7 @@ func TestEnroll(t *testing.T) { } return cfg.Client.Host == host && cfg.AccessAPIKey == "my-access-api-key" })).Return(nil).Once() - cmd, err := newEnrollCmd( + cmd, err := NewEnrollCmd( log, &enroll.EnrollOptions{ URL: url, @@ -362,7 +362,7 @@ func TestEnroll(t *testing.T) { }, func(t *testing.T, host string) { url := "http://" + host store := &mockSaver{} - cmd, err := newEnrollCmd( + cmd, err := NewEnrollCmd( log, &enroll.EnrollOptions{ URL: url, @@ -444,7 +444,7 @@ func TestEnroll(t *testing.T) { } return cfg.Client.Host == host && cfg.AccessAPIKey == "my-access-api-key" })).Return(nil).Once() - cmd, err := newEnrollCmd( + cmd, err := NewEnrollCmd( log, &enroll.EnrollOptions{ URL: url, @@ -509,7 +509,7 @@ func TestEnroll(t *testing.T) { }, func(t *testing.T, host string) { url := "http://" + host store := &mockStore{} - cmd, err := newEnrollCmd( + cmd, err := NewEnrollCmd( log, &enroll.EnrollOptions{ URL: url, @@ -550,7 +550,7 @@ func TestValidateArgs(t *testing.T) { streams, _, _, _ := cli.NewTestingIOStreams() t.Run("comma separated tags are parsed", func(t *testing.T) { - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err := cmd.Flags().Set("tag", "windows,production") require.NoError(t, err) err = cmd.Flags().Set("insecure", "true") @@ -573,7 +573,7 @@ func TestValidateArgs(t *testing.T) { }) t.Run("comma separated tags and duplicated tags are cleaned", func(t *testing.T) { - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err := cmd.Flags().Set("tag", "windows, production, windows") require.NoError(t, err) args := buildEnrollmentFlags(cmd, url, enrolmentToken) @@ -591,7 +591,7 @@ func TestValidateArgs(t *testing.T) { }) t.Run("valid tag and empty tag", func(t *testing.T) { - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err := cmd.Flags().Set("tag", "windows, ") require.NoError(t, err) args := buildEnrollmentFlags(cmd, url, enrolmentToken) @@ -607,7 +607,7 @@ func TestValidateArgs(t *testing.T) { }) t.Run("secret paths are passed", func(t *testing.T) { - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err := cmd.Flags().Set("fleet-server-cert-key-passphrase", "/path/to/passphrase") require.NoError(t, err) err = cmd.Flags().Set("fleet-server-service-token-path", "/path/to/token") @@ -620,7 +620,7 @@ func TestValidateArgs(t *testing.T) { }) t.Run("fleet-es client certificates are passed", func(t *testing.T) { - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err := cmd.Flags().Set("fleet-server-es-cert", "/path/to/cert") require.NoError(t, err) err = cmd.Flags().Set("fleet-server-es-cert-key", "/path/to/key") @@ -633,7 +633,7 @@ func TestValidateArgs(t *testing.T) { }) t.Run("elastic-agent client certificates are passed", func(t *testing.T) { - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err := cmd.Flags().Set("elastic-agent-cert", "/path/to/cert") require.NoError(t, err) err = cmd.Flags().Set("elastic-agent-cert-key", "/path/to/key") @@ -646,7 +646,7 @@ func TestValidateArgs(t *testing.T) { }) t.Run("enroll-timeout value is passed", func(t *testing.T) { - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err := cmd.Flags().Set("enroll-timeout", "1m") require.NoError(t, err) args := buildEnrollmentFlags(cmd, url, enrolmentToken) @@ -655,7 +655,7 @@ func TestValidateArgs(t *testing.T) { }) t.Run("negative enroll-timeout value is passed", func(t *testing.T) { - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err := cmd.Flags().Set("enroll-timeout", "-1s") require.NoError(t, err) args := buildEnrollmentFlags(cmd, url, enrolmentToken) @@ -668,7 +668,7 @@ func TestValidateEnrollFlags(t *testing.T) { streams, _, _, _ := cli.NewTestingIOStreams() t.Run("no flags", func(t *testing.T) { - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err := validateEnrollFlags(cmd) assert.NoError(t, err) @@ -678,7 +678,7 @@ func TestValidateEnrollFlags(t *testing.T) { absPath, err := filepath.Abs("/path/to/token") require.NoError(t, err, "could not get absolute absPath") - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err = cmd.Flags().Set("fleet-server-service-token-path", absPath) require.NoError(t, err) err = cmd.Flags().Set("fleet-server-service-token", "token-value") @@ -696,7 +696,7 @@ func TestValidateEnrollFlags(t *testing.T) { absPath, err := filepath.Abs("/path/to/elastic-agent-cert-key") require.NoError(t, err, "could not get absolute absPath") - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err = cmd.Flags().Set("elastic-agent-cert-key", absPath) require.NoError(t, err, "could not set flag 'elastic-agent-cert-key'") @@ -709,7 +709,7 @@ func TestValidateEnrollFlags(t *testing.T) { absPath, err := filepath.Abs("/path/to/elastic-agent-cert-key-passphrase") require.NoError(t, err, "could not get absolute absPath") - cmd := newEnrollCommandWithArgs([]string{}, streams) + cmd := NewEnrollCommandWithArgs([]string{}, streams) err = cmd.Flags().Set("elastic-agent-cert-key-passphrase", absPath) require.NoError(t, err, "could not set flag 'elastic-agent-cert-key-passphrase'") diff --git a/internal/pkg/agent/cmd/enroll_match_fileowner_unix.go b/internal/pkg/agent/cmd/install/enroll_match_fileowner_unix.go similarity index 98% rename from internal/pkg/agent/cmd/enroll_match_fileowner_unix.go rename to internal/pkg/agent/cmd/install/enroll_match_fileowner_unix.go index acc5996c244..dfe352a8789 100644 --- a/internal/pkg/agent/cmd/enroll_match_fileowner_unix.go +++ b/internal/pkg/agent/cmd/install/enroll_match_fileowner_unix.go @@ -4,7 +4,7 @@ //go:build !windows -package cmd +package install import ( "fmt" diff --git a/internal/pkg/agent/cmd/enroll_match_fileowner_unix_test.go b/internal/pkg/agent/cmd/install/enroll_match_fileowner_unix_test.go similarity index 97% rename from internal/pkg/agent/cmd/enroll_match_fileowner_unix_test.go rename to internal/pkg/agent/cmd/install/enroll_match_fileowner_unix_test.go index 9ad3b22db8c..9b95186f42f 100644 --- a/internal/pkg/agent/cmd/enroll_match_fileowner_unix_test.go +++ b/internal/pkg/agent/cmd/install/enroll_match_fileowner_unix_test.go @@ -4,7 +4,7 @@ //go:build !windows -package cmd +package install import ( "os" diff --git a/internal/pkg/agent/cmd/enroll_match_fileowner_windows.go b/internal/pkg/agent/cmd/install/enroll_match_fileowner_windows.go similarity index 95% rename from internal/pkg/agent/cmd/enroll_match_fileowner_windows.go rename to internal/pkg/agent/cmd/install/enroll_match_fileowner_windows.go index 1205617e27b..6e869fceec5 100644 --- a/internal/pkg/agent/cmd/enroll_match_fileowner_windows.go +++ b/internal/pkg/agent/cmd/install/enroll_match_fileowner_windows.go @@ -4,7 +4,7 @@ //go:build windows -package cmd +package install func isOwnerExec(path string) (bool, error) { // No-op for Windows: always allow diff --git a/internal/pkg/agent/cmd/enroll_match_fileowner_windows_test.go b/internal/pkg/agent/cmd/install/enroll_match_fileowner_windows_test.go similarity index 97% rename from internal/pkg/agent/cmd/enroll_match_fileowner_windows_test.go rename to internal/pkg/agent/cmd/install/enroll_match_fileowner_windows_test.go index cfc1abcb6bb..d2080b47368 100644 --- a/internal/pkg/agent/cmd/enroll_match_fileowner_windows_test.go +++ b/internal/pkg/agent/cmd/install/enroll_match_fileowner_windows_test.go @@ -4,7 +4,7 @@ //go:build windows -package cmd +package install import ( "os" diff --git a/internal/pkg/agent/cmd/enroll_unix.go b/internal/pkg/agent/cmd/install/enroll_unix.go similarity index 98% rename from internal/pkg/agent/cmd/enroll_unix.go rename to internal/pkg/agent/cmd/install/enroll_unix.go index ff404aa2916..0483e28ce8a 100644 --- a/internal/pkg/agent/cmd/enroll_unix.go +++ b/internal/pkg/agent/cmd/install/enroll_unix.go @@ -4,7 +4,7 @@ //go:build !windows -package cmd +package install import ( "fmt" diff --git a/internal/pkg/agent/cmd/enroll_windows.go b/internal/pkg/agent/cmd/install/enroll_windows.go similarity index 98% rename from internal/pkg/agent/cmd/enroll_windows.go rename to internal/pkg/agent/cmd/install/enroll_windows.go index 6ab1827529d..b4ce0754423 100644 --- a/internal/pkg/agent/cmd/enroll_windows.go +++ b/internal/pkg/agent/cmd/install/enroll_windows.go @@ -4,7 +4,7 @@ //go:build windows -package cmd +package install import ( "fmt" diff --git a/internal/pkg/agent/cmd/install.go b/internal/pkg/agent/cmd/install/install.go similarity index 97% rename from internal/pkg/agent/cmd/install.go rename to internal/pkg/agent/cmd/install/install.go index 9e48532c9ae..0e9e855315c 100644 --- a/internal/pkg/agent/cmd/install.go +++ b/internal/pkg/agent/cmd/install/install.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package install import ( "errors" @@ -19,6 +19,7 @@ import ( "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent/internal/pkg/agent/application/filelock" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/install" "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/pkg/core/logger" @@ -38,7 +39,7 @@ const ( flagInstallCustomPass = "password" ) -func newInstallCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewInstallCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "install", Short: "Install Elastic Agent permanently on this system", @@ -49,8 +50,8 @@ would like the Agent to operate. `, Run: func(c *cobra.Command, _ []string) { if err := installCmd(streams, c); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) - logExternal(fmt.Sprintf("%s install failed: %s", paths.BinaryName, err)) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) + common.LogExternal(fmt.Sprintf("%s install failed: %s", paths.BinaryName, err)) os.Exit(1) } }, diff --git a/internal/pkg/agent/cmd/install_enroll.go b/internal/pkg/agent/cmd/install/install_enroll.go similarity index 98% rename from internal/pkg/agent/cmd/install_enroll.go rename to internal/pkg/agent/cmd/install/install_enroll.go index da72ba6dc19..61cf070fad2 100644 --- a/internal/pkg/agent/cmd/install_enroll.go +++ b/internal/pkg/agent/cmd/install/install_enroll.go @@ -4,7 +4,7 @@ //go:build !windows -package cmd +package install import ( "fmt" diff --git a/internal/pkg/agent/cmd/install_enroll_windows.go b/internal/pkg/agent/cmd/install/install_enroll_windows.go similarity index 97% rename from internal/pkg/agent/cmd/install_enroll_windows.go rename to internal/pkg/agent/cmd/install/install_enroll_windows.go index 5d180a4a526..5b61f17be34 100644 --- a/internal/pkg/agent/cmd/install_enroll_windows.go +++ b/internal/pkg/agent/cmd/install/install_enroll_windows.go @@ -4,7 +4,7 @@ //go:build windows -package cmd +package install import ( "fmt" diff --git a/internal/pkg/agent/cmd/install_test.go b/internal/pkg/agent/cmd/install/install_test.go similarity index 97% rename from internal/pkg/agent/cmd/install_test.go rename to internal/pkg/agent/cmd/install/install_test.go index f483c71c020..86a37b058be 100644 --- a/internal/pkg/agent/cmd/install_test.go +++ b/internal/pkg/agent/cmd/install/install_test.go @@ -4,7 +4,7 @@ //go:build !windows -package cmd +package install import ( "bytes" @@ -57,7 +57,7 @@ func TestInvalidBasePath(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { streams := cli.NewIOStreams() - cmd := newInstallCommandWithArgs([]string{}, streams) + cmd := NewInstallCommandWithArgs([]string{}, streams) err := cmd.Flags().Set(flagInstallBasePath, test.basePath) require.NoError(t, err) diff --git a/internal/pkg/agent/cmd/install_windows_test.go b/internal/pkg/agent/cmd/install/install_windows_test.go similarity index 95% rename from internal/pkg/agent/cmd/install_windows_test.go rename to internal/pkg/agent/cmd/install/install_windows_test.go index 030ef7f954e..02866a6ab5b 100644 --- a/internal/pkg/agent/cmd/install_windows_test.go +++ b/internal/pkg/agent/cmd/install/install_windows_test.go @@ -4,7 +4,7 @@ //go:build windows -package cmd +package install import ( "testing" @@ -51,7 +51,7 @@ func TestInvalidBasePath(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { streams := cli.NewIOStreams() - cmd := newInstallCommandWithArgs([]string{}, streams) + cmd := NewInstallCommandWithArgs([]string{}, streams) err := cmd.Flags().Set(flagInstallBasePath, test.basePath) require.NoError(t, err) diff --git a/internal/pkg/agent/cmd/logs.go b/internal/pkg/agent/cmd/logs/logs.go similarity index 98% rename from internal/pkg/agent/cmd/logs.go rename to internal/pkg/agent/cmd/logs/logs.go index 76978057de5..b4bcdb463a5 100644 --- a/internal/pkg/agent/cmd/logs.go +++ b/internal/pkg/agent/cmd/logs/logs.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package logs import ( "bufio" @@ -23,6 +23,7 @@ import ( "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/pkg/core/logger" ) @@ -56,111 +57,7 @@ type logEntry struct { LogLevel string `json:"log.level"` } -// createComponentFilter creates a new log entry filter that -// lets print only the log lines that contain the given component ID. -func createComponentFilter(id string) filterFunc { - return func(entry []byte) bool { - var e logEntry - err := json.Unmarshal(entry, &e) - if err != nil { - return false - } - return e.Component.ID == id - } -} - -func addColorModifier(entry []byte) []byte { - var e logEntry - err := json.Unmarshal(entry, &e) - if err != nil { - return entry - } - switch strings.ToLower(e.LogLevel) { - - case logp.InfoLevel.String(): - return []byte(color.CyanString(string(entry))) - case logp.WarnLevel.String(): - return []byte(color.YellowString(string(entry))) - case logp.ErrorLevel.String(): - return []byte(color.RedString(string(entry))) - case logp.CriticalLevel.String(): - return []byte(color.HiRedString(string(entry))) - default: - return entry - } -} - -// stackWriter collects written byte slices and then pops them in -// the reversed (LIFO) order. -// Supports filtering and modification of each written byte slice. -type stackWriter struct { - lines [][]byte - filter filterFunc - modifier modifierFunc -} - -// Write implements `io.Writer` -func (s *stackWriter) Write(line []byte) (int, error) { - if s.filter != nil && !s.filter(line) { - return 0, errLineFiltered - } - // we must allocate and copy to preserve the state, - // `line` is normally a slice on the reading buffer which - // gets overwritten - l := make([]byte, len(line)) - copy(l, line) - - if s.modifier != nil { - l = s.modifier(l) - } - - s.lines = append(s.lines, l) - return len(l), nil -} - -// PopAll pops every line from the stack and writes into `w` in LIFO order. -func (s stackWriter) PopAll(w io.Writer) error { - for i := len(s.lines) - 1; i >= 0; i-- { - _, err := w.Write(s.lines[i]) - if err != nil { - return fmt.Errorf("failed to print the log line to the writer: %w", err) - } - _, err = w.Write([]byte{'\n'}) - if err != nil { - return fmt.Errorf("failed to print the log line to the writer: %w", err) - } - } - - return nil -} - -// newWrappedWriter create a writer proxy that filters out log lines according to the given `filter` -func newWrappedWriter(ctx context.Context, w io.Writer, filter filterFunc, modifier modifierFunc) io.Writer { - pr, pw := io.Pipe() - scanner := bufio.NewScanner(pr) - go func() { - for scanner.Scan() { - select { - case <-ctx.Done(): - return - default: - line := scanner.Bytes() - if filter != nil && !filter(line) { - continue - } - if modifier != nil { - line = modifier(line) - } - _, _ = w.Write(line) - _, _ = w.Write([]byte{'\n'}) - } - } - }() - - return pw -} - -func newLogsCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewLogsCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { logsDir := filepath.Join(paths.Home(), logger.DefaultLogDirectory) eventLogsDir := filepath.Join(logsDir, "events") @@ -170,7 +67,7 @@ func newLogsCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { Long: "This command allows to output, watch and filter Elastic Agent logs.", Run: func(c *cobra.Command, _ []string) { if err := logsCmd(streams, c, logsDir, eventLogsDir); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(1) } }, @@ -427,6 +324,110 @@ func printLogFile(filename string, maxLines int, w *stackWriter, buf []byte) (li return linesWritten, nil } +// createComponentFilter creates a new log entry filter that +// lets print only the log lines that contain the given component ID. +func createComponentFilter(id string) filterFunc { + return func(entry []byte) bool { + var e logEntry + err := json.Unmarshal(entry, &e) + if err != nil { + return false + } + return e.Component.ID == id + } +} + +func addColorModifier(entry []byte) []byte { + var e logEntry + err := json.Unmarshal(entry, &e) + if err != nil { + return entry + } + switch strings.ToLower(e.LogLevel) { + + case logp.InfoLevel.String(): + return []byte(color.CyanString(string(entry))) + case logp.WarnLevel.String(): + return []byte(color.YellowString(string(entry))) + case logp.ErrorLevel.String(): + return []byte(color.RedString(string(entry))) + case logp.CriticalLevel.String(): + return []byte(color.HiRedString(string(entry))) + default: + return entry + } +} + +// stackWriter collects written byte slices and then pops them in +// the reversed (LIFO) order. +// Supports filtering and modification of each written byte slice. +type stackWriter struct { + lines [][]byte + filter filterFunc + modifier modifierFunc +} + +// Write implements `io.Writer` +func (s *stackWriter) Write(line []byte) (int, error) { + if s.filter != nil && !s.filter(line) { + return 0, errLineFiltered + } + // we must allocate and copy to preserve the state, + // `line` is normally a slice on the reading buffer which + // gets overwritten + l := make([]byte, len(line)) + copy(l, line) + + if s.modifier != nil { + l = s.modifier(l) + } + + s.lines = append(s.lines, l) + return len(l), nil +} + +// PopAll pops every line from the stack and writes into `w` in LIFO order. +func (s stackWriter) PopAll(w io.Writer) error { + for i := len(s.lines) - 1; i >= 0; i-- { + _, err := w.Write(s.lines[i]) + if err != nil { + return fmt.Errorf("failed to print the log line to the writer: %w", err) + } + _, err = w.Write([]byte{'\n'}) + if err != nil { + return fmt.Errorf("failed to print the log line to the writer: %w", err) + } + } + + return nil +} + +// newWrappedWriter create a writer proxy that filters out log lines according to the given `filter` +func newWrappedWriter(ctx context.Context, w io.Writer, filter filterFunc, modifier modifierFunc) io.Writer { + pr, pw := io.Pipe() + scanner := bufio.NewScanner(pr) + go func() { + for scanner.Scan() { + select { + case <-ctx.Done(): + return + default: + line := scanner.Bytes() + if filter != nil && !filter(line) { + continue + } + if modifier != nil { + line = modifier(line) + } + _, _ = w.Write(line) + _, _ = w.Write([]byte{'\n'}) + } + } + }() + + return pw +} + // getLogFilenames returns absolute paths to all log files in `dir` sorted in the log rotation order. func getLogFilenames(dir string) ([]string, error) { entries, err := os.ReadDir(dir) diff --git a/internal/pkg/agent/cmd/logs_test.go b/internal/pkg/agent/cmd/logs/logs_test.go similarity index 99% rename from internal/pkg/agent/cmd/logs_test.go rename to internal/pkg/agent/cmd/logs/logs_test.go index b81e6ccfaa4..2a05ba8ef90 100644 --- a/internal/pkg/agent/cmd/logs_test.go +++ b/internal/pkg/agent/cmd/logs/logs_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package logs import ( "bufio" @@ -702,7 +702,7 @@ func TestCobraCmd(t *testing.T) { expectedLines := 10 testingStreams, _, out, _ := cli.NewTestingIOStreams() - cmd := newLogsCommandWithArgs(nil, testingStreams) + cmd := NewLogsCommandWithArgs(nil, testingStreams) logsDir := t.TempDir() eventLogsDir := filepath.Join(logsDir, "events") diff --git a/internal/pkg/agent/cmd/command_components.go b/internal/pkg/agent/cmd/otel/command_components.go similarity index 88% rename from internal/pkg/agent/cmd/command_components.go rename to internal/pkg/agent/cmd/otel/command_components.go index 6fe2b5e720b..8420a398da8 100644 --- a/internal/pkg/agent/cmd/command_components.go +++ b/internal/pkg/agent/cmd/otel/command_components.go @@ -2,11 +2,12 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package otel import ( "github.com/spf13/cobra" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/internal/pkg/otel" ) @@ -23,9 +24,9 @@ func newComponentsCommandWithArgs(_ []string, _ *cli.IOStreams) *cobra.Command { }, } - setupOtelFlags(cmd.Flags()) + SetupOtelFlags(cmd.Flags()) cmd.SetHelpFunc(func(c *cobra.Command, s []string) { - hideInheritedFlags(c) + common.HideInheritedFlags(c) c.Root().HelpFunc()(c, s) }) diff --git a/internal/pkg/agent/cmd/command_components_fips_test.go b/internal/pkg/agent/cmd/otel/command_components_fips_test.go similarity index 99% rename from internal/pkg/agent/cmd/command_components_fips_test.go rename to internal/pkg/agent/cmd/otel/command_components_fips_test.go index 83bb51712a0..e3c97d27ba3 100644 --- a/internal/pkg/agent/cmd/command_components_fips_test.go +++ b/internal/pkg/agent/cmd/otel/command_components_fips_test.go @@ -4,7 +4,7 @@ //go:build requirefips -package cmd +package otel import ( "bytes" diff --git a/internal/pkg/agent/cmd/command_components_nofips_test.go b/internal/pkg/agent/cmd/otel/command_components_nofips_test.go similarity index 99% rename from internal/pkg/agent/cmd/command_components_nofips_test.go rename to internal/pkg/agent/cmd/otel/command_components_nofips_test.go index 901e9a44469..8b3c2ce1762 100644 --- a/internal/pkg/agent/cmd/command_components_nofips_test.go +++ b/internal/pkg/agent/cmd/otel/command_components_nofips_test.go @@ -4,7 +4,7 @@ //go:build !requirefips -package cmd +package otel import ( "bytes" diff --git a/internal/pkg/agent/cmd/otel.go b/internal/pkg/agent/cmd/otel/otel.go similarity index 93% rename from internal/pkg/agent/cmd/otel.go rename to internal/pkg/agent/cmd/otel/otel.go index c6b6b7dfda9..88f7249d6e6 100644 --- a/internal/pkg/agent/cmd/otel.go +++ b/internal/pkg/agent/cmd/otel/otel.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package otel import ( "context" @@ -13,12 +13,12 @@ import ( "go.uber.org/zap/zapcore" "github.com/spf13/cobra" - "github.com/spf13/pflag" "go.opentelemetry.io/collector/otelcol" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent-libs/service" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/internal/pkg/otel" "github.com/elastic/elastic-agent/internal/pkg/otel/agentprovider" @@ -27,7 +27,7 @@ import ( "github.com/elastic/elastic-agent/pkg/core/logger" ) -func newOtelCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { +func NewOtelCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "otel", Short: "Start the Elastic Agent in otel mode", @@ -52,30 +52,24 @@ func newOtelCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Comman }, PreRun: func(c *cobra.Command, args []string) { // hide inherited flags not to bloat help with flags not related to otel - hideInheritedFlags(c) + common.HideInheritedFlags(c) }, SilenceUsage: true, SilenceErrors: true, } cmd.SetHelpFunc(func(c *cobra.Command, s []string) { - hideInheritedFlags(c) + common.HideInheritedFlags(c) c.Root().HelpFunc()(c, s) }) - setupOtelFlags(cmd.Flags()) + SetupOtelFlags(cmd.Flags()) cmd.AddCommand(newValidateCommandWithArgs(args, streams)) cmd.AddCommand(newComponentsCommandWithArgs(args, streams)) return cmd } -func hideInheritedFlags(c *cobra.Command) { - c.InheritedFlags().VisitAll(func(f *pflag.Flag) { - f.Hidden = true - }) -} - func RunCollector(cmdCtx context.Context, configFiles []string, supervised bool, supervisedLoggingLevel string) error { settings, err := prepareCollectorSettings(configFiles, supervised, supervisedLoggingLevel) if err != nil { @@ -164,7 +158,7 @@ func prepareEnv() error { // The filestorage extension will handle directory creation since create_directory: true is set by default. // If the user hasn’t specified the env:STATE_PATH in filestorage config, they may have opted for a custom path, and the extension will create the directory accordingly. // In this case, setting env:STATE_PATH will have no effect. - if err := os.Setenv("STATE_PATH", defaultStateDirectory); err != nil { + if err := os.Setenv("STATE_PATH", common.DefaultStateDirectory); err != nil { return err } } diff --git a/internal/pkg/agent/cmd/otel_flags.go b/internal/pkg/agent/cmd/otel/otel_flags.go similarity index 98% rename from internal/pkg/agent/cmd/otel_flags.go rename to internal/pkg/agent/cmd/otel/otel_flags.go index e5818eee19a..5606ad9fced 100644 --- a/internal/pkg/agent/cmd/otel_flags.go +++ b/internal/pkg/agent/cmd/otel/otel_flags.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package otel import ( "flag" @@ -22,7 +22,7 @@ const ( otelSetFlagName = "set" ) -func setupOtelFlags(flags *pflag.FlagSet) { +func SetupOtelFlags(flags *pflag.FlagSet) { flags.StringArray(otelConfigFlagName, []string{}, "Locations to the config file(s), note that only a"+ " single location can be set per flag entry e.g. `--config=file:/path/to/first --config=file:path/to/second`.") diff --git a/internal/pkg/agent/cmd/otel_flags_test.go b/internal/pkg/agent/cmd/otel/otel_flags_test.go similarity index 95% rename from internal/pkg/agent/cmd/otel_flags_test.go rename to internal/pkg/agent/cmd/otel/otel_flags_test.go index 24fe30e2afd..b2876f1b321 100644 --- a/internal/pkg/agent/cmd/otel_flags_test.go +++ b/internal/pkg/agent/cmd/otel/otel_flags_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package otel import ( "testing" @@ -16,7 +16,7 @@ import ( func TestOtelFlagsSetup(t *testing.T) { fs := new(pflag.FlagSet) - setupOtelFlags(fs) + SetupOtelFlags(fs) expectedFlags := []string{ otelConfigFlagName, @@ -30,7 +30,7 @@ func TestOtelFlagsSetup(t *testing.T) { } func TestGetConfigFiles(t *testing.T) { - cmd := newOtelCommandWithArgs(nil, nil) + cmd := NewOtelCommandWithArgs(nil, nil) configFile := "sample.yaml" require.NoError(t, cmd.Flag(otelConfigFlagName).Value.Set(configFile)) @@ -46,7 +46,7 @@ func TestGetConfigFiles(t *testing.T) { } func TestGetConfigFilesWithDefault(t *testing.T) { - cmd := newOtelCommandWithArgs(nil, nil) + cmd := NewOtelCommandWithArgs(nil, nil) setVal := "set=val" sets, err := getSets([]string{setVal}) @@ -60,7 +60,7 @@ func TestGetConfigFilesWithDefault(t *testing.T) { } func TestGetConfigErrorWhenNoConfig(t *testing.T) { - cmd := newOtelCommandWithArgs(nil, nil) + cmd := NewOtelCommandWithArgs(nil, nil) _, err := getConfigFiles(cmd, false) require.Error(t, err) diff --git a/internal/pkg/agent/cmd/otel_test.go b/internal/pkg/agent/cmd/otel/otel_test.go similarity index 99% rename from internal/pkg/agent/cmd/otel_test.go rename to internal/pkg/agent/cmd/otel/otel_test.go index 073fd46d58b..b70eaed5e9f 100644 --- a/internal/pkg/agent/cmd/otel_test.go +++ b/internal/pkg/agent/cmd/otel/otel_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package otel import ( "os" diff --git a/internal/pkg/agent/cmd/validate.go b/internal/pkg/agent/cmd/otel/validate.go similarity index 90% rename from internal/pkg/agent/cmd/validate.go rename to internal/pkg/agent/cmd/otel/validate.go index 2853e4c9cdf..9b1cf90c01f 100644 --- a/internal/pkg/agent/cmd/validate.go +++ b/internal/pkg/agent/cmd/otel/validate.go @@ -2,13 +2,14 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package otel import ( "context" "github.com/spf13/cobra" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/internal/pkg/otel" ) @@ -29,9 +30,9 @@ func newValidateCommandWithArgs(_ []string, _ *cli.IOStreams) *cobra.Command { }, } - setupOtelFlags(cmd.Flags()) + SetupOtelFlags(cmd.Flags()) cmd.SetHelpFunc(func(c *cobra.Command, s []string) { - hideInheritedFlags(c) + common.HideInheritedFlags(c) c.Root().HelpFunc()(c, s) }) diff --git a/internal/pkg/agent/cmd/validate_test.go b/internal/pkg/agent/cmd/otel/validate_test.go similarity index 98% rename from internal/pkg/agent/cmd/validate_test.go rename to internal/pkg/agent/cmd/otel/validate_test.go index ee5066c3c18..3acbf2523c3 100644 --- a/internal/pkg/agent/cmd/validate_test.go +++ b/internal/pkg/agent/cmd/otel/validate_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package otel import ( "context" diff --git a/internal/pkg/agent/cmd/reexec.go b/internal/pkg/agent/cmd/reexec/reexec.go similarity index 83% rename from internal/pkg/agent/cmd/reexec.go rename to internal/pkg/agent/cmd/reexec/reexec.go index e061a29a226..3fff3806e2b 100644 --- a/internal/pkg/agent/cmd/reexec.go +++ b/internal/pkg/agent/cmd/reexec/reexec.go @@ -4,7 +4,7 @@ //go:build !windows -package cmd +package reexec import ( "github.com/spf13/cobra" @@ -12,6 +12,6 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/cli" ) -func newReExecWindowsCommand(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewReExecWindowsCommand(_ []string, streams *cli.IOStreams) *cobra.Command { return nil } diff --git a/internal/pkg/agent/cmd/reexec_windows.go b/internal/pkg/agent/cmd/reexec/reexec_windows.go similarity index 93% rename from internal/pkg/agent/cmd/reexec_windows.go rename to internal/pkg/agent/cmd/reexec/reexec_windows.go index 9b9a727cf9f..339f1136ce5 100644 --- a/internal/pkg/agent/cmd/reexec_windows.go +++ b/internal/pkg/agent/cmd/reexec/reexec_windows.go @@ -4,7 +4,7 @@ //go:build windows -package cmd +package reexec import ( "fmt" @@ -19,6 +19,7 @@ import ( "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/cli" ) @@ -26,7 +27,7 @@ const ( reexecName = "elastic-agent-reexec" ) -func newReExecWindowsCommand(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewReExecWindowsCommand(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Hidden: true, Use: "reexec_windows ", @@ -34,10 +35,10 @@ func newReExecWindowsCommand(_ []string, streams *cli.IOStreams) *cobra.Command Long: "This waits for the windows service to stop then restarts it to allow self-upgrading.", Args: cobra.ExactArgs(2), Run: func(c *cobra.Command, args []string) { - cfg := getConfig(streams) - log, err := configuredLogger(cfg, reexecName) + cfg := common.GetConfig(streams) + log, err := common.ConfiguredLogger(cfg, reexecName) if err != nil { - fmt.Fprintf(streams.Err, "Error configuring logger: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error configuring logger: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(3) } diff --git a/internal/pkg/agent/cmd/setup_config.go b/internal/pkg/agent/cmd/setup_config.go deleted file mode 100644 index d7347efb924..00000000000 --- a/internal/pkg/agent/cmd/setup_config.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License 2.0; -// you may not use this file except in compliance with the Elastic License 2.0. - -package cmd - -import "time" - -// setup configuration - -type setupConfig struct { - Fleet fleetConfig `config:"fleet"` - FleetServer fleetServerConfig `config:"fleet_server"` - Kibana kibanaConfig `config:"kibana"` -} - -type fleetConfig struct { - CA string `config:"ca"` - Enroll bool `config:"enroll"` - EnrollmentToken string `config:"enrollment_token"` - ID string `config:"id"` - ReplaceToken string `config:"replace_token"` - Force bool `config:"force"` - Insecure bool `config:"insecure"` - TokenName string `config:"token_name"` - TokenPolicyName string `config:"token_policy_name"` - URL string `config:"url"` - Headers map[string]string `config:"headers"` - DaemonTimeout time.Duration `config:"daemon_timeout"` - EnrollTimeout time.Duration `config:"enroll_timeout"` - Cert string `config:"cert"` - CertKey string `config:"cert_key"` -} - -type fleetServerConfig struct { - Cert string `config:"cert"` - CertKey string `config:"cert_key"` - PassphrasePath string `config:"key_passphrase_path"` - ClientAuth string `config:"client_authentication"` - Elasticsearch elasticsearchConfig `config:"elasticsearch"` - Enable bool `config:"enable"` - Host string `config:"host"` - InsecureHTTP bool `config:"insecure_http"` - PolicyID string `config:"policy_id"` - Port string `config:"port"` - Headers map[string]string `config:"headers"` - Timeout time.Duration `config:"timeout"` -} - -type elasticsearchConfig struct { - CA string `config:"ca"` - CATrustedFingerprint string `config:"ca_trusted_fingerprint"` - Host string `config:"host"` - ServiceToken string `config:"service_token"` - ServiceTokenPath string `config:"service_token_path"` - Insecure bool `config:"insecure"` - Cert string `config:"cert"` - CertKey string `config:"cert_key"` -} - -type kibanaConfig struct { - Fleet kibanaFleetConfig `config:"fleet"` - RetrySleepDuration time.Duration `config:"retry_sleep_duration"` - RetryMaxCount int `config:"retry_max_count"` - Headers map[string]string `config:"headers"` -} - -type kibanaFleetConfig struct { - CA string `config:"ca"` - Host string `config:"host"` - Username string `config:"username"` - Password string `config:"password"` - ServiceToken string `config:"service_token"` - ServiceTokenPath string `config:"service_token_path"` -} - -func defaultAccessConfig() (setupConfig, error) { - retrySleepDuration, err := envDurationWithDefault(defaultRequestRetrySleep, requestRetrySleepEnv) - if err != nil { - return setupConfig{}, err - } - - retryMaxCount, err := envIntWithDefault(defaultMaxRequestRetries, maxRequestRetriesEnv) - if err != nil { - return setupConfig{}, err - } - - cfg := setupConfig{ - Fleet: fleetConfig{ - CA: envWithDefault("", "FLEET_CA", "KIBANA_CA", "ELASTICSEARCH_CA"), - Enroll: envBool("FLEET_ENROLL", "FLEET_SERVER_ENABLE"), - EnrollmentToken: envWithDefault("", "FLEET_ENROLLMENT_TOKEN"), - ID: envWithDefault("", "ELASTIC_AGENT_ID"), - ReplaceToken: envWithDefault("", "FLEET_REPLACE_TOKEN"), - Force: envBool("FLEET_FORCE"), - Insecure: envBool("FLEET_INSECURE"), - TokenName: envWithDefault("Default", "FLEET_TOKEN_NAME"), - TokenPolicyName: envWithDefault("", "FLEET_TOKEN_POLICY_NAME"), - URL: envWithDefault("", "FLEET_URL"), - Headers: envMap("FLEET_HEADER"), - DaemonTimeout: envTimeout("FLEET_DAEMON_TIMEOUT"), - EnrollTimeout: envTimeout("FLEET_ENROLL_TIMEOUT"), - Cert: envWithDefault("", "ELASTIC_AGENT_CERT"), - CertKey: envWithDefault("", "ELASTIC_AGENT_CERT_KEY"), - }, - FleetServer: fleetServerConfig{ - Cert: envWithDefault("", "FLEET_SERVER_CERT"), - CertKey: envWithDefault("", "FLEET_SERVER_CERT_KEY"), - PassphrasePath: envWithDefault("", "FLEET_SERVER_CERT_KEY_PASSPHRASE"), - ClientAuth: envWithDefault("none", "FLEET_SERVER_CLIENT_AUTH"), - Elasticsearch: elasticsearchConfig{ - Host: envWithDefault("http://elasticsearch:9200", "FLEET_SERVER_ELASTICSEARCH_HOST", "ELASTICSEARCH_HOST"), - ServiceToken: envWithDefault("", "FLEET_SERVER_SERVICE_TOKEN"), - ServiceTokenPath: envWithDefault("", "FLEET_SERVER_SERVICE_TOKEN_PATH"), - CA: envWithDefault("", "FLEET_SERVER_ELASTICSEARCH_CA", "ELASTICSEARCH_CA"), - CATrustedFingerprint: envWithDefault("", "FLEET_SERVER_ELASTICSEARCH_CA_TRUSTED_FINGERPRINT"), - Insecure: envBool("FLEET_SERVER_ELASTICSEARCH_INSECURE"), - Cert: envWithDefault("", "FLEET_SERVER_ES_CERT"), - CertKey: envWithDefault("", "FLEET_SERVER_ES_CERT_KEY"), - }, - Enable: envBool("FLEET_SERVER_ENABLE"), - Host: envWithDefault("", "FLEET_SERVER_HOST"), - InsecureHTTP: envBool("FLEET_SERVER_INSECURE_HTTP"), - PolicyID: envWithDefault("", "FLEET_SERVER_POLICY_ID", "FLEET_SERVER_POLICY"), - Port: envWithDefault("", "FLEET_SERVER_PORT"), - Headers: envMap("FLEET_HEADER"), - Timeout: envTimeout("FLEET_SERVER_TIMEOUT"), - }, - Kibana: kibanaConfig{ - Fleet: kibanaFleetConfig{ - Host: envWithDefault("http://kibana:5601", "KIBANA_FLEET_HOST", "KIBANA_HOST"), - Username: envWithDefault("elastic", "KIBANA_FLEET_USERNAME", "KIBANA_USERNAME", "ELASTICSEARCH_USERNAME"), - Password: envWithDefault("changeme", "KIBANA_FLEET_PASSWORD", "KIBANA_PASSWORD", "ELASTICSEARCH_PASSWORD"), - ServiceToken: envWithDefault("", "KIBANA_FLEET_SERVICE_TOKEN", "FLEET_SERVER_SERVICE_TOKEN"), - ServiceTokenPath: envWithDefault("", "KIBANA_FLEET_SERVICE_TOKEN_PATH", "FLEET_SERVER_SERVICE_TOKEN_PATH"), - CA: envWithDefault("", "KIBANA_FLEET_CA", "KIBANA_CA", "ELASTICSEARCH_CA"), - }, - RetrySleepDuration: retrySleepDuration, - RetryMaxCount: retryMaxCount, - Headers: envMap("FLEET_KIBANA_HEADER"), - }, - } - return cfg, nil -} diff --git a/internal/pkg/agent/cmd/status.go b/internal/pkg/agent/cmd/status/status.go similarity index 96% rename from internal/pkg/agent/cmd/status.go rename to internal/pkg/agent/cmd/status/status.go index bdffc8e3420..73027ee3df0 100644 --- a/internal/pkg/agent/cmd/status.go +++ b/internal/pkg/agent/cmd/status/status.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package status import ( "context" @@ -14,6 +14,7 @@ import ( "time" "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/pkg/control" "github.com/elastic/elastic-agent/pkg/control/v2/client" "github.com/elastic/elastic-agent/pkg/control/v2/cproto" @@ -37,14 +38,14 @@ var statusOutputs = map[string]outputter{ "yaml": yamlOutput, } -func newStatusCommand(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewStatusCommand(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "status", Short: "Show the current status of the running Elastic Agent daemon", Long: `This command shows the current status of the running Elastic Agent daemon.`, Run: func(c *cobra.Command, args []string) { if err := statusCmd(streams, c, args); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(1) } }, @@ -62,11 +63,11 @@ func statusCmd(streams *cli.IOStreams, cmd *cobra.Command, args []string) error return fmt.Errorf("unsupported output: %s", output) } - ctx := handleSignal(context.Background()) + ctx := common.HandleSignal(context.Background()) innerCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - state, err := getDaemonState(innerCtx) + state, err := common.GetDaemonState(innerCtx) if errors.Is(err, context.DeadlineExceeded) { return errors.New("timed out after 30 seconds trying to connect to Elastic Agent daemon") } else if errors.Is(err, context.Canceled) { diff --git a/internal/pkg/agent/cmd/status_test.go b/internal/pkg/agent/cmd/status/status_test.go similarity index 99% rename from internal/pkg/agent/cmd/status_test.go rename to internal/pkg/agent/cmd/status/status_test.go index b6a43f7d5a5..9bbaa2de62d 100644 --- a/internal/pkg/agent/cmd/status_test.go +++ b/internal/pkg/agent/cmd/status/status_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package status import ( "bytes" diff --git a/internal/pkg/agent/cmd/privileged.go b/internal/pkg/agent/cmd/switch/privileged.go similarity index 90% rename from internal/pkg/agent/cmd/privileged.go rename to internal/pkg/agent/cmd/switch/privileged.go index 2d462ec16d9..4c1a2b7b48a 100644 --- a/internal/pkg/agent/cmd/privileged.go +++ b/internal/pkg/agent/cmd/switch/privileged.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package switchcmd import ( "context" @@ -13,13 +13,14 @@ import ( "github.com/spf13/cobra" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/install" "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/pkg/control/v2/client/wait" "github.com/elastic/elastic-agent/pkg/utils" ) -func newPrivilegedCommandWithArgs(s []string, streams *cli.IOStreams) *cobra.Command { +func NewPrivilegedCommandWithArgs(s []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "privileged", Short: "Switch installed Elastic Agent to run as privileged", @@ -34,7 +35,7 @@ privileged it will still perform all the same work, including stopping and start Args: cobra.ExactArgs(0), Run: func(c *cobra.Command, args []string) { if err := privilegedCmd(streams, c); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(1) } }, @@ -78,8 +79,8 @@ func privilegedCmd(streams *cli.IOStreams, cmd *cobra.Command) (err error) { // wait for the service if daemonTimeout >= 0 { pt.Describe("Waiting for running service") - ctx := handleSignal(context.Background()) // allowed to be cancelled - err = wait.ForAgent(ctx, daemonTimeout) + ctx := common.HandleSignal(context.Background()) // allowed to be cancelled + err = wait.ForAgent(ctx, common.DaemonTimeout) if err != nil { if errors.Is(err, context.Canceled) { pt.Describe("Cancelled waiting for running service") diff --git a/internal/pkg/agent/cmd/unprivileged.go b/internal/pkg/agent/cmd/switch/unprivileged.go similarity index 86% rename from internal/pkg/agent/cmd/unprivileged.go rename to internal/pkg/agent/cmd/switch/unprivileged.go index a6d7ea70f06..a00f9b99635 100644 --- a/internal/pkg/agent/cmd/unprivileged.go +++ b/internal/pkg/agent/cmd/switch/unprivileged.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package switchcmd import ( "context" @@ -13,15 +13,25 @@ import ( "github.com/spf13/cobra" + "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/inspect" "github.com/elastic/elastic-agent/internal/pkg/agent/install" "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/pkg/component" "github.com/elastic/elastic-agent/pkg/control/v2/client/wait" + "github.com/elastic/elastic-agent/pkg/core/logger" "github.com/elastic/elastic-agent/pkg/utils" ) -func newUnprivilegedCommandWithArgs(s []string, streams *cli.IOStreams) *cobra.Command { +const ( + flagInstallCustomUser = "user" + flagInstallCustomGroup = "group" + flagInstallCustomPass = "password" +) + +func NewUnprivilegedCommandWithArgs(s []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "unprivileged", Short: "Switch installed Elastic Agent to run as unprivileged", @@ -36,7 +46,7 @@ unprivileged it will still perform all the same work, including stopping and sta Args: cobra.ExactArgs(0), Run: func(c *cobra.Command, args []string) { if err := unprivilegedCmd(streams, c); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(1) } }, @@ -104,8 +114,8 @@ func unprivilegedCmd(streams *cli.IOStreams, cmd *cobra.Command) (err error) { // wait for the service if daemonTimeout >= 0 { pt.Describe("Waiting for running service") - ctx := handleSignal(context.Background()) // allowed to be cancelled - err = wait.ForAgent(ctx, daemonTimeout) + ctx := common.HandleSignal(context.Background()) // allowed to be cancelled + err = wait.ForAgent(ctx, common.DaemonTimeout) if err != nil { if errors.Is(err, context.Canceled) { pt.Describe("Cancelled waiting for running service") @@ -128,7 +138,7 @@ func ensureNoServiceComponentIssues() error { } // this forces the component calculation to always compute with no root // this allows any runtime preventions to error for a component when it has a no root support - comps, err := getComponentsFromPolicy(ctx, l, paths.ConfigFile(), 0, forceNonRoot) + comps, err := inspect.GetComponentsFromPolicy(ctx, l, paths.ConfigFile(), 0, forceNonRoot) if err != nil { return fmt.Errorf("failed to create component model from policy: %w", err) } @@ -157,3 +167,7 @@ func forceNonRoot(detail component.PlatformDetail) component.PlatformDetail { detail.User.Root = false return detail } + +func newErrorLogger() (*logger.Logger, error) { + return logger.NewWithLogpLevel("", logp.ErrorLevel, false) +} diff --git a/internal/pkg/agent/cmd/uninstall.go b/internal/pkg/agent/cmd/uninstall/uninstall.go similarity index 92% rename from internal/pkg/agent/cmd/uninstall.go rename to internal/pkg/agent/cmd/uninstall/uninstall.go index a83df2e6055..e4636ec0ae7 100644 --- a/internal/pkg/agent/cmd/uninstall.go +++ b/internal/pkg/agent/cmd/uninstall/uninstall.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package uninstall import ( "fmt" @@ -12,13 +12,14 @@ import ( "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/install" "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/pkg/core/logger" "github.com/elastic/elastic-agent/pkg/utils" ) -func newUninstallCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewUninstallCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "uninstall", Short: "Uninstall Elastic Agent from this system", @@ -28,8 +29,8 @@ Unless -f is used this command will ask confirmation before performing removal. `, Run: func(c *cobra.Command, _ []string) { if err := uninstallCmd(streams, c); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) - logExternal(fmt.Sprintf("%s uninstall failed: %s", paths.BinaryName, err)) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) + common.LogExternal(fmt.Sprintf("%s uninstall failed: %s", paths.BinaryName, err)) os.Exit(1) } }, diff --git a/internal/pkg/agent/cmd/upgrade.go b/internal/pkg/agent/cmd/upgrade/upgrade.go similarity index 96% rename from internal/pkg/agent/cmd/upgrade.go rename to internal/pkg/agent/cmd/upgrade/upgrade.go index c944133e288..659c77894ae 100644 --- a/internal/pkg/agent/cmd/upgrade.go +++ b/internal/pkg/agent/cmd/upgrade/upgrade.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package upgrade import ( "context" @@ -21,6 +21,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade" "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/artifact/download" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/cli" ) @@ -42,7 +43,7 @@ var ( skipVerifyNotRootError = errors.New(fmt.Sprintf("user needs to be root to use \"%s\" flag when upgrading standalone agents", flagSkipVerify)) ) -func newUpgradeCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewUpgradeCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "upgrade ", Short: "Upgrade the currently installed Elastic Agent to the specified version", @@ -51,7 +52,7 @@ func newUpgradeCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Comman Run: func(c *cobra.Command, args []string) { c.SetContext(context.Background()) if err := upgradeCmd(streams, c, args); err != nil { - fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(1) } }, diff --git a/internal/pkg/agent/cmd/upgrade_test.go b/internal/pkg/agent/cmd/upgrade/upgrade_test.go similarity index 95% rename from internal/pkg/agent/cmd/upgrade_test.go rename to internal/pkg/agent/cmd/upgrade/upgrade_test.go index f9b98e5ec89..77e6e9d0b28 100644 --- a/internal/pkg/agent/cmd/upgrade_test.go +++ b/internal/pkg/agent/cmd/upgrade/upgrade_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package upgrade import ( "context" @@ -48,7 +48,7 @@ func TestUpgradeCmd(t *testing.T) { args := []string{"--skip-verify", "8.13.0"} streams := cli.NewIOStreams() - cmd := newUpgradeCommandWithArgs(args, streams) + cmd := NewUpgradeCommandWithArgs(args, streams) cmd.SetContext(context.Background()) commandInput := &upgradeInput{ @@ -91,7 +91,7 @@ func TestUpgradeCmd(t *testing.T) { args := []string{"8.13.0"} // Version argument streams := cli.NewIOStreams() - cmd := newUpgradeCommandWithArgs(args, streams) + cmd := NewUpgradeCommandWithArgs(args, streams) err := cmd.Flags().Set(flagForce, "true") if err != nil { log.Fatal(err) @@ -121,7 +121,7 @@ func TestUpgradeCmd(t *testing.T) { args := []string{"8.13.0"} // Version argument streams := cli.NewIOStreams() - cmd := newUpgradeCommandWithArgs(args, streams) + cmd := NewUpgradeCommandWithArgs(args, streams) cmd.SetContext(context.Background()) commandInput := &upgradeInput{ @@ -147,7 +147,7 @@ func TestUpgradeCmd(t *testing.T) { args := []string{"8.13.0"} // Version argument streams := cli.NewIOStreams() - cmd := newUpgradeCommandWithArgs(args, streams) + cmd := NewUpgradeCommandWithArgs(args, streams) cmd.SetContext(context.Background()) err := cmd.Flags().Set(flagForce, "true") if err != nil { @@ -173,7 +173,7 @@ func TestUpgradeCmd(t *testing.T) { args := []string{"8.13.0"} // Version argument streams := cli.NewIOStreams() - cmd := newUpgradeCommandWithArgs(args, streams) + cmd := NewUpgradeCommandWithArgs(args, streams) cmd.SetContext(context.Background()) err := cmd.Flags().Set(flagForce, "true") if err != nil { @@ -204,7 +204,7 @@ func TestUpgradeCmd(t *testing.T) { args := []string{"8.13.0"} // Version argument streams := cli.NewIOStreams() - cmd := newUpgradeCommandWithArgs(args, streams) + cmd := NewUpgradeCommandWithArgs(args, streams) cmd.SetContext(context.Background()) err := cmd.Flags().Set(flagForce, "true") if err != nil { @@ -236,7 +236,7 @@ func TestUpgradeCmd(t *testing.T) { args := []string{"8.13.0"} // Version argument streams := cli.NewIOStreams() - cmd := newUpgradeCommandWithArgs(args, streams) + cmd := NewUpgradeCommandWithArgs(args, streams) cmd.SetContext(context.Background()) err := cmd.Flags().Set(flagForce, "true") if err != nil { diff --git a/internal/pkg/agent/cmd/watch.go b/internal/pkg/agent/cmd/watch/watch.go similarity index 85% rename from internal/pkg/agent/cmd/watch.go rename to internal/pkg/agent/cmd/watch/watch.go index f203c1814f7..b693e492918 100644 --- a/internal/pkg/agent/cmd/watch.go +++ b/internal/pkg/agent/cmd/watch/watch.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package watch import ( "context" @@ -16,17 +16,16 @@ import ( "github.com/spf13/cobra" "github.com/elastic/elastic-agent-libs/logp" - "github.com/elastic/elastic-agent-libs/logp/configure" "github.com/elastic/elastic-agent/pkg/control/v2/client" "github.com/elastic/elastic-agent/internal/pkg/agent/application/filelock" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade" "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/cli" - "github.com/elastic/elastic-agent/internal/pkg/config" "github.com/elastic/elastic-agent/internal/pkg/release" "github.com/elastic/elastic-agent/pkg/core/logger" "github.com/elastic/elastic-agent/version" @@ -37,16 +36,16 @@ const ( watcherLockFile = "watcher.lock" ) -func newWatchCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { +func NewWatchCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "watch", Short: "Watch the Elastic Agent for failures and initiate rollback", Long: `This command watches Elastic Agent for failures and initiates rollback if necessary.`, Run: func(_ *cobra.Command, _ []string) { - cfg := getConfig(streams) - log, err := configuredLogger(cfg, watcherName) + cfg := common.GetConfig(streams) + log, err := common.ConfiguredLogger(cfg, watcherName) if err != nil { - fmt.Fprintf(streams.Err, "Error configuring logger: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Error configuring logger: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(3) } @@ -55,7 +54,7 @@ func newWatchCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command if err := watchCmd(log, paths.Top(), cfg.Settings.Upgrade.Watcher, new(upgradeAgentWatcher), new(upgradeInstallationModifier)); err != nil { log.Errorw("Watch command failed", "error.message", err) - fmt.Fprintf(streams.Err, "Watch command failed: %v\n%s\n", err, troubleshootMessage()) + fmt.Fprintf(streams.Err, "Watch command failed: %v\n%s\n", err, common.TroubleshootMessage()) os.Exit(4) } }, @@ -234,44 +233,6 @@ func gracePeriod(marker *upgrade.UpdateMarker, gracePeriodDuration time.Duration return false, gracePeriodDuration } -func configuredLogger(cfg *configuration.Configuration, name string) (*logger.Logger, error) { - cfg.Settings.LoggingConfig.Beat = name - cfg.Settings.LoggingConfig.Level = logp.DebugLevel - internal, err := logger.MakeInternalFileOutput(cfg.Settings.LoggingConfig) - if err != nil { - return nil, err - } - - libC, err := logger.ToCommonConfig(cfg.Settings.LoggingConfig) - if err != nil { - return nil, err - } - - if err := configure.LoggingWithOutputs("", libC, internal); err != nil { - return nil, fmt.Errorf("error initializing logging: %w", err) - } - return logp.NewLogger(""), nil -} - -func getConfig(streams *cli.IOStreams) *configuration.Configuration { - defaultCfg := configuration.DefaultConfiguration() - - pathConfigFile := paths.ConfigFile() - rawConfig, err := config.LoadFile(pathConfigFile) - if err != nil { - fmt.Fprintf(streams.Err, "could not read configuration file %s", pathConfigFile) - return defaultCfg - } - - cfg, err := configuration.NewFromConfig(rawConfig) - if err != nil { - fmt.Fprintf(streams.Err, "could not parse configuration file %s", pathConfigFile) - return defaultCfg - } - - return cfg -} - func initUpgradeDetails(marker *upgrade.UpdateMarker, saveMarker func(*upgrade.UpdateMarker, bool) error, log *logp.Logger) *details.Details { upgradeDetails := details.NewDetails(version.GetAgentPackageVersion(), details.StateWatching, marker.GetActionID()) upgradeDetails.RegisterObserver(func(details *details.Details) { diff --git a/internal/pkg/agent/cmd/watch_impl.go b/internal/pkg/agent/cmd/watch/watch_impl.go similarity index 98% rename from internal/pkg/agent/cmd/watch_impl.go rename to internal/pkg/agent/cmd/watch/watch_impl.go index 92e3118435c..4cfb4da3873 100644 --- a/internal/pkg/agent/cmd/watch_impl.go +++ b/internal/pkg/agent/cmd/watch/watch_impl.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package watch import ( "context" diff --git a/internal/pkg/agent/cmd/watch_test.go b/internal/pkg/agent/cmd/watch/watch_test.go similarity index 99% rename from internal/pkg/agent/cmd/watch_test.go rename to internal/pkg/agent/cmd/watch/watch_test.go index 9451c476543..19537bf4f0c 100644 --- a/internal/pkg/agent/cmd/watch_test.go +++ b/internal/pkg/agent/cmd/watch/watch_test.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. -package cmd +package watch import ( "fmt" diff --git a/internal/pkg/otel/manager/testing/testing.go b/internal/pkg/otel/manager/testing/testing.go index 6ad11fd8644..8a89664b6ab 100644 --- a/internal/pkg/otel/manager/testing/testing.go +++ b/internal/pkg/otel/manager/testing/testing.go @@ -10,7 +10,7 @@ import ( "os" "time" - "github.com/elastic/elastic-agent/internal/pkg/agent/cmd" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/otel" ) func main() { @@ -28,7 +28,7 @@ func main() { }) } - err := cmd.RunCollector(ctx, nil, true, "debug") + err := otel.RunCollector(ctx, nil, true, "debug") if err == nil || errors.Is(err, context.Canceled) { os.Exit(0) } From ef25c7a5ef7139eda322f16a83f0b5f49f239cb0 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Mon, 1 Sep 2025 14:51:40 +0200 Subject: [PATCH 03/15] linux files --- internal/pkg/agent/cmd/common/container.go | 6 +----- .../agent/cmd/container/container_init_linux.go | 15 ++++++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/internal/pkg/agent/cmd/common/container.go b/internal/pkg/agent/cmd/common/container.go index cc0ef3af7c3..7938b595b0a 100644 --- a/internal/pkg/agent/cmd/common/container.go +++ b/internal/pkg/agent/cmd/common/container.go @@ -21,11 +21,7 @@ import ( ) const ( - requestRetrySleepEnv = "KIBANA_REQUEST_RETRY_SLEEP" - maxRequestRetriesEnv = "KIBANA_REQUEST_RETRY_COUNT" - defaultRequestRetrySleep = "1s" // sleep 1 sec between retries for HTTP requests - defaultMaxRequestRetries = "30" // maximum number of retries for HTTP requests - agentBaseDirectory = "/usr/share/elastic-agent" // directory that holds all elastic-agent related files + agentBaseDirectory = "/usr/share/elastic-agent" // directory that holds all elastic-agent related files logsPathPerms = 0775 ) diff --git a/internal/pkg/agent/cmd/container/container_init_linux.go b/internal/pkg/agent/cmd/container/container_init_linux.go index dafb4aada1d..c3a9324d80e 100644 --- a/internal/pkg/agent/cmd/container/container_init_linux.go +++ b/internal/pkg/agent/cmd/container/container_init_linux.go @@ -22,9 +22,14 @@ import ( "kernel.org/pub/linux/libs/security/libcap/cap" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/cli" ) +const ( + agentBaseDirectory = "/usr/share/elastic-agent" // directory that holds all elastic-agent related files +) + type capProc interface { GetFlag(vec cap.Flag, val cap.Value) (bool, error) SetFlag(vec cap.Flag, enable bool, val ...cap.Value) error @@ -219,11 +224,11 @@ func chownPaths(agentBaseDirectory string) error { agentBaseDirectory: {}, } - pathsToChown.addPath(envWithDefault("", "LOGS_PATH")) - pathsToChown.addPath(envWithDefault("", "STATE_PATH")) - pathsToChown.addPath(envWithDefault("", "CONFIG_PATH")) - pathsToChown.addPath(envWithDefault("", "DATA_PATH")) - pathsToChown.addPath(envWithDefault("", "HOME_PATH")) + pathsToChown.addPath(common.EnvWithDefault("", "LOGS_PATH")) + pathsToChown.addPath(common.EnvWithDefault("", "STATE_PATH")) + pathsToChown.addPath(common.EnvWithDefault("", "CONFIG_PATH")) + pathsToChown.addPath(common.EnvWithDefault("", "DATA_PATH")) + pathsToChown.addPath(common.EnvWithDefault("", "HOME_PATH")) return pathsToChown.chown(uid, gid) } From 93b7e61ee3b32d6fc4a263a113d0ef883abba130 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Mon, 1 Sep 2025 15:14:21 +0200 Subject: [PATCH 04/15] uid,gid related linter --- internal/pkg/agent/cmd/container/container_init_linux.go | 2 +- internal/pkg/agent/cmd/install/install_enroll.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/pkg/agent/cmd/container/container_init_linux.go b/internal/pkg/agent/cmd/container/container_init_linux.go index c3a9324d80e..3e6971c5b1a 100644 --- a/internal/pkg/agent/cmd/container/container_init_linux.go +++ b/internal/pkg/agent/cmd/container/container_init_linux.go @@ -277,7 +277,7 @@ func (u distinctPaths) chown(uid int, gid int) error { return nil } - if sysInfo.Gid == uint32(gid) && sysInfo.Uid == uint32(uid) { + if sysInfo.Gid == uint32(gid) && sysInfo.Uid == uint32(uid) { //nolint:gosec // G115 always under 32-bit // already owned return nil } diff --git a/internal/pkg/agent/cmd/install/install_enroll.go b/internal/pkg/agent/cmd/install/install_enroll.go index 61cf070fad2..8e483fae5b9 100644 --- a/internal/pkg/agent/cmd/install/install_enroll.go +++ b/internal/pkg/agent/cmd/install/install_enroll.go @@ -32,8 +32,8 @@ func enrollCmdExtras(cmd *exec.Cmd, ownership utils.FileOwner) error { } cmd.SysProcAttr = &syscall.SysProcAttr{ Credential: &syscall.Credential{ - Uid: uint32(ownership.UID), - Gid: uint32(ownership.GID), + Uid: uint32(ownership.UID), //nolint:gosec // G115 always under 32-bit + Gid: uint32(ownership.GID), //nolint:gosec // G115 always under 32-bit }, } return nil From ccc6a6645e6d8966ee0da33a28dad95247a70167 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Mon, 1 Sep 2025 15:37:04 +0200 Subject: [PATCH 05/15] interactive --- internal/pkg/agent/agentservice/agentservice_windows.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/pkg/agent/agentservice/agentservice_windows.go b/internal/pkg/agent/agentservice/agentservice_windows.go index 87e9d19867d..0fb76c88776 100644 --- a/internal/pkg/agent/agentservice/agentservice_windows.go +++ b/internal/pkg/agent/agentservice/agentservice_windows.go @@ -60,15 +60,15 @@ func processWindowsControlEvents(stopCallback func()) { defer close(serviceInstance.executeFinished) //nolint:staticcheck // keep using the deprecated method in order to maintain the existing behavior - isWindowsService, err := svc.IsWindowsService() + isInteractive, err := svc.IsAnInteractiveSession() if err != nil { logp.Err("IsAnInteractiveSession: %v", err) return } - logp.Debug("service", "Windows is interactive: %v", isWindowsService) + logp.Debug("service", "Windows is interactive: %v", isInteractive) run := svc.Run - if isWindowsService { + if isInteractive { run = debug.Run } From 1ef74cd3b8c1076e94e7226046a1296e52d9055c Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Mon, 1 Sep 2025 15:56:38 +0200 Subject: [PATCH 06/15] headers --- internal/pkg/agent/agentservice/agentservice.go | 4 ++++ internal/pkg/agent/agentservice/agentservice_unix.go | 4 ++++ internal/pkg/agent/agentservice/agentservice_windows.go | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/internal/pkg/agent/agentservice/agentservice.go b/internal/pkg/agent/agentservice/agentservice.go index 84ff2ba3cd0..2b1cc3eb8c8 100644 --- a/internal/pkg/agent/agentservice/agentservice.go +++ b/internal/pkg/agent/agentservice/agentservice.go @@ -1,3 +1,7 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + package agentservice var ( diff --git a/internal/pkg/agent/agentservice/agentservice_unix.go b/internal/pkg/agent/agentservice/agentservice_unix.go index 600ddc9b376..99f9245887e 100644 --- a/internal/pkg/agent/agentservice/agentservice_unix.go +++ b/internal/pkg/agent/agentservice/agentservice_unix.go @@ -1,3 +1,7 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + //go:build !windows package agentservice diff --git a/internal/pkg/agent/agentservice/agentservice_windows.go b/internal/pkg/agent/agentservice/agentservice_windows.go index 0fb76c88776..bb4990e44d9 100644 --- a/internal/pkg/agent/agentservice/agentservice_windows.go +++ b/internal/pkg/agent/agentservice/agentservice_windows.go @@ -1,3 +1,7 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + //go:build windows package agentservice From 0cc8cb3d7f97bb50bae9b2a42fa5e0d7103c1a6a Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Mon, 1 Sep 2025 16:45:10 +0200 Subject: [PATCH 07/15] notice --- NOTICE-fips.txt | 120 ++++++++++++++++++++++++------------------------ NOTICE.txt | 120 ++++++++++++++++++++++++------------------------ 2 files changed, 120 insertions(+), 120 deletions(-) diff --git a/NOTICE-fips.txt b/NOTICE-fips.txt index 03c93623313..24f7c55e050 100644 --- a/NOTICE-fips.txt +++ b/NOTICE-fips.txt @@ -3850,6 +3850,66 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Dependency : github.com/ghodss/yaml +Version: v1.0.0 +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/ghodss/yaml@v1.0.0/LICENSE: + +The MIT License (MIT) + +Copyright (c) 2014 Sam Ghods + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + -------------------------------------------------------------------------------- Dependency : github.com/go-viper/mapstructure/v2 Version: v2.3.0 @@ -46004,66 +46064,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------- -Dependency : github.com/ghodss/yaml -Version: v1.0.0 -Licence type (autodetected): MIT --------------------------------------------------------------------------------- - -Contents of probable licence file $GOMODCACHE/github.com/ghodss/yaml@v1.0.0/LICENSE: - -The MIT License (MIT) - -Copyright (c) 2014 Sam Ghods - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -Copyright (c) 2012 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -------------------------------------------------------------------------------- Dependency : github.com/go-asn1-ber/asn1-ber Version: v1.5.5 diff --git a/NOTICE.txt b/NOTICE.txt index 934d74aab8d..24794a77247 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -3850,6 +3850,66 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Dependency : github.com/ghodss/yaml +Version: v1.0.0 +Licence type (autodetected): MIT +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/ghodss/yaml@v1.0.0/LICENSE: + +The MIT License (MIT) + +Copyright (c) 2014 Sam Ghods + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + -------------------------------------------------------------------------------- Dependency : github.com/go-viper/mapstructure/v2 Version: v2.3.0 @@ -48226,66 +48286,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------- -Dependency : github.com/ghodss/yaml -Version: v1.0.0 -Licence type (autodetected): MIT --------------------------------------------------------------------------------- - -Contents of probable licence file $GOMODCACHE/github.com/ghodss/yaml@v1.0.0/LICENSE: - -The MIT License (MIT) - -Copyright (c) 2014 Sam Ghods - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -Copyright (c) 2012 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -------------------------------------------------------------------------------- Dependency : github.com/go-asn1-ber/asn1-ber Version: v1.5.5 From e9e04ac3530202620459dc7930ee57c897307b29 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Mon, 1 Sep 2025 18:02:11 +0200 Subject: [PATCH 08/15] move testdata --- .../testdata/diagnostics/endpoint-security/logs/endpoint-1.log | 0 .../testdata/diagnostics/endpoint-security/logs/endpoint-2.log | 0 internal/pkg/agent/cmd/otel/command_components_fips_test.go | 2 +- .../cmd/{ => otel}/testdata/otel/components-output-fips.yml | 0 .../agent/cmd/{ => otel}/testdata/otel/components-output.yml | 0 .../pkg/agent/cmd/{ => otel}/testdata/otel/elastic-agent.yml | 0 internal/pkg/agent/cmd/{ => otel}/testdata/otel/otel.yml | 0 .../pkg/agent/cmd/{ => status}/testdata/status/full_degraded | 0 .../pkg/agent/cmd/{ => status}/testdata/status/full_healthy | 0 .../pkg/agent/cmd/{ => status}/testdata/status/human_degraded | 0 .../pkg/agent/cmd/{ => status}/testdata/status/human_healthy | 0 11 files changed, 1 insertion(+), 1 deletion(-) rename internal/pkg/agent/cmd/{ => diagnostics}/testdata/diagnostics/endpoint-security/logs/endpoint-1.log (100%) rename internal/pkg/agent/cmd/{ => diagnostics}/testdata/diagnostics/endpoint-security/logs/endpoint-2.log (100%) rename internal/pkg/agent/cmd/{ => otel}/testdata/otel/components-output-fips.yml (100%) rename internal/pkg/agent/cmd/{ => otel}/testdata/otel/components-output.yml (100%) rename internal/pkg/agent/cmd/{ => otel}/testdata/otel/elastic-agent.yml (100%) rename internal/pkg/agent/cmd/{ => otel}/testdata/otel/otel.yml (100%) rename internal/pkg/agent/cmd/{ => status}/testdata/status/full_degraded (100%) rename internal/pkg/agent/cmd/{ => status}/testdata/status/full_healthy (100%) rename internal/pkg/agent/cmd/{ => status}/testdata/status/human_degraded (100%) rename internal/pkg/agent/cmd/{ => status}/testdata/status/human_healthy (100%) diff --git a/internal/pkg/agent/cmd/testdata/diagnostics/endpoint-security/logs/endpoint-1.log b/internal/pkg/agent/cmd/diagnostics/testdata/diagnostics/endpoint-security/logs/endpoint-1.log similarity index 100% rename from internal/pkg/agent/cmd/testdata/diagnostics/endpoint-security/logs/endpoint-1.log rename to internal/pkg/agent/cmd/diagnostics/testdata/diagnostics/endpoint-security/logs/endpoint-1.log diff --git a/internal/pkg/agent/cmd/testdata/diagnostics/endpoint-security/logs/endpoint-2.log b/internal/pkg/agent/cmd/diagnostics/testdata/diagnostics/endpoint-security/logs/endpoint-2.log similarity index 100% rename from internal/pkg/agent/cmd/testdata/diagnostics/endpoint-security/logs/endpoint-2.log rename to internal/pkg/agent/cmd/diagnostics/testdata/diagnostics/endpoint-security/logs/endpoint-2.log diff --git a/internal/pkg/agent/cmd/otel/command_components_fips_test.go b/internal/pkg/agent/cmd/otel/command_components_fips_test.go index e3c97d27ba3..0e197ee7792 100644 --- a/internal/pkg/agent/cmd/otel/command_components_fips_test.go +++ b/internal/pkg/agent/cmd/otel/command_components_fips_test.go @@ -42,7 +42,7 @@ func TestComponentsCommand(t *testing.T) { cmd := &cobra.Command{} cmd.SetArgs([]string{"components"}) - expectedOutput, err := os.ReadFile(filepath.Join("testdata", "otel/components-output-fips.yml")) + expectedOutput, err := os.ReadFile(filepath.Join("../", "testdata", "otel/components-output-fips.yml")) require.NoError(t, err) expectedComponents := &componentsOutput{} err = yaml.Unmarshal(expectedOutput, expectedComponents) diff --git a/internal/pkg/agent/cmd/testdata/otel/components-output-fips.yml b/internal/pkg/agent/cmd/otel/testdata/otel/components-output-fips.yml similarity index 100% rename from internal/pkg/agent/cmd/testdata/otel/components-output-fips.yml rename to internal/pkg/agent/cmd/otel/testdata/otel/components-output-fips.yml diff --git a/internal/pkg/agent/cmd/testdata/otel/components-output.yml b/internal/pkg/agent/cmd/otel/testdata/otel/components-output.yml similarity index 100% rename from internal/pkg/agent/cmd/testdata/otel/components-output.yml rename to internal/pkg/agent/cmd/otel/testdata/otel/components-output.yml diff --git a/internal/pkg/agent/cmd/testdata/otel/elastic-agent.yml b/internal/pkg/agent/cmd/otel/testdata/otel/elastic-agent.yml similarity index 100% rename from internal/pkg/agent/cmd/testdata/otel/elastic-agent.yml rename to internal/pkg/agent/cmd/otel/testdata/otel/elastic-agent.yml diff --git a/internal/pkg/agent/cmd/testdata/otel/otel.yml b/internal/pkg/agent/cmd/otel/testdata/otel/otel.yml similarity index 100% rename from internal/pkg/agent/cmd/testdata/otel/otel.yml rename to internal/pkg/agent/cmd/otel/testdata/otel/otel.yml diff --git a/internal/pkg/agent/cmd/testdata/status/full_degraded b/internal/pkg/agent/cmd/status/testdata/status/full_degraded similarity index 100% rename from internal/pkg/agent/cmd/testdata/status/full_degraded rename to internal/pkg/agent/cmd/status/testdata/status/full_degraded diff --git a/internal/pkg/agent/cmd/testdata/status/full_healthy b/internal/pkg/agent/cmd/status/testdata/status/full_healthy similarity index 100% rename from internal/pkg/agent/cmd/testdata/status/full_healthy rename to internal/pkg/agent/cmd/status/testdata/status/full_healthy diff --git a/internal/pkg/agent/cmd/testdata/status/human_degraded b/internal/pkg/agent/cmd/status/testdata/status/human_degraded similarity index 100% rename from internal/pkg/agent/cmd/testdata/status/human_degraded rename to internal/pkg/agent/cmd/status/testdata/status/human_degraded diff --git a/internal/pkg/agent/cmd/testdata/status/human_healthy b/internal/pkg/agent/cmd/status/testdata/status/human_healthy similarity index 100% rename from internal/pkg/agent/cmd/testdata/status/human_healthy rename to internal/pkg/agent/cmd/status/testdata/status/human_healthy From e1fc526f53c8ae77e1e85ddb3940aa949b82c583 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Tue, 2 Sep 2025 09:47:42 +0200 Subject: [PATCH 09/15] ess tests compilation errors --- internal/pkg/agent/cmd/otel/command_components_fips_test.go | 2 +- testing/integration/ess/re-enroll_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/pkg/agent/cmd/otel/command_components_fips_test.go b/internal/pkg/agent/cmd/otel/command_components_fips_test.go index 0e197ee7792..e3c97d27ba3 100644 --- a/internal/pkg/agent/cmd/otel/command_components_fips_test.go +++ b/internal/pkg/agent/cmd/otel/command_components_fips_test.go @@ -42,7 +42,7 @@ func TestComponentsCommand(t *testing.T) { cmd := &cobra.Command{} cmd.SetArgs([]string{"components"}) - expectedOutput, err := os.ReadFile(filepath.Join("../", "testdata", "otel/components-output-fips.yml")) + expectedOutput, err := os.ReadFile(filepath.Join("testdata", "otel/components-output-fips.yml")) require.NoError(t, err) expectedComponents := &componentsOutput{} err = yaml.Unmarshal(expectedOutput, expectedComponents) diff --git a/testing/integration/ess/re-enroll_test.go b/testing/integration/ess/re-enroll_test.go index 55d7658b36f..1cb65c60992 100644 --- a/testing/integration/ess/re-enroll_test.go +++ b/testing/integration/ess/re-enroll_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/require" "github.com/elastic/elastic-agent-libs/kibana" - "github.com/elastic/elastic-agent/internal/pkg/agent/cmd" + "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/install" atesting "github.com/elastic/elastic-agent/pkg/testing" "github.com/elastic/elastic-agent/pkg/testing/define" "github.com/elastic/elastic-agent/pkg/testing/tools" @@ -55,7 +55,7 @@ func TestReEnrollUnprivileged(t *testing.T) { out, err := fixture.Exec(ctx, enrollArgs) require.Error(t, err) - require.Contains(t, string(out), cmd.UserOwnerMismatchError.Error()) + require.Contains(t, string(out), install.UserOwnerMismatchError.Error()) assert.Eventuallyf(t, func() bool { err := fixture.IsHealthy(t.Context()) From ec8f50895ed4f39071e0fbc90d14f11a568ed2dd Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Wed, 3 Sep 2025 12:10:23 +0200 Subject: [PATCH 10/15] fixed termination and logging --- .../agent/agentservice/agentservice_unix.go | 4 ++ .../agentservice/agentservice_windows.go | 49 ++++++++++++++----- internal/pkg/agent/cmd/agentrun/run.go | 5 +- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/internal/pkg/agent/agentservice/agentservice_unix.go b/internal/pkg/agent/agentservice/agentservice_unix.go index 99f9245887e..bf87e168a85 100644 --- a/internal/pkg/agent/agentservice/agentservice_unix.go +++ b/internal/pkg/agent/agentservice/agentservice_unix.go @@ -5,3 +5,7 @@ //go:build !windows package agentservice + +func NotifyTermination() {} + +func WaitExecutionDone() {} diff --git a/internal/pkg/agent/agentservice/agentservice_windows.go b/internal/pkg/agent/agentservice/agentservice_windows.go index bb4990e44d9..eba1f4de439 100644 --- a/internal/pkg/agent/agentservice/agentservice_windows.go +++ b/internal/pkg/agent/agentservice/agentservice_windows.go @@ -51,7 +51,13 @@ func init() { func stopBeat() { if StopChanBeat != nil { + // making sure select catches it by sending + select { + case StopChanBeat <- true: + default: + } close(StopChanBeat) + } } @@ -66,10 +72,10 @@ func processWindowsControlEvents(stopCallback func()) { //nolint:staticcheck // keep using the deprecated method in order to maintain the existing behavior isInteractive, err := svc.IsAnInteractiveSession() if err != nil { - logp.Err("IsAnInteractiveSession: %v", err) + logp.L().Errorf("IsAnInteractiveSession: %v", err) return } - logp.Debug("service", "Windows is interactive: %v", isInteractive) + logp.L().Debug("service", "Windows is interactive: %v", isInteractive) run := svc.Run if isInteractive { @@ -98,11 +104,11 @@ func processWindowsControlEvents(stopCallback func()) { If the program will be run as a console application for debugging purposes, structure it such that service-specific code is not called when this error is returned." */ - logp.Info("Attempted to register Windows service handlers, but this is not a service. No action necessary") + logp.L().Info("Attempted to register Windows service handlers, but this is not a service. No action necessary") return } - logp.Err("Windows service setup failed: %+v", err) + logp.L().Errorf("Windows service setup failed: %+v", err) } // Execute runs the beat service with the arguments and manages changes that @@ -112,8 +118,7 @@ func (m *beatService) Execute(args []string, r <-chan svc.ChangeRequest, changes changes <- svc.Status{State: svc.StartPending} changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} - log := logp.NewLogger("service_windows") - log.Info("reported Running to Service manager") + logp.L().Info("reported Running to Service manager") combinedChan := make(chan svc.ChangeRequest) go func() { for { @@ -140,34 +145,56 @@ loop: // The svc.Cmd tye does not implement the Stringer interface and its // underlying type is an integer, therefore it's needed to manually log them. case svc.Stop: - log.Info("received state change 'svc.Stop' from windows service manager") + logp.L().Info("received state change 'svc.Stop' from windows service manager") break loop case svc.Shutdown: - log.Info("received state change 'svc.Shutdown' from windows service manager") + logp.L().Info("received state change 'svc.Shutdown' from windows service manager") break loop default: - log.Errorf("Unexpected control request: $%d. Ignored.", c) + logp.L().Errorf("Unexpected control request: $%d. Ignored.", c) } } trySendState(svc.StopPending, changes) defer trySendState(svc.Stopped, changes) - log.Info("changed windows service state to svc.StopPending, invoking stopCallback") + logp.L().Info("changed windows service state to svc.StopPending, invoking stopCallback") m.stopCallback() // Block until notifyWindowsServiceStopped below is called. This is required // as the windows/svc package will transition the service to STOPPED state // once this function returns. <-m.done - log.Debug("windows service state changed to svc.Stopped") + logp.L().Debug("windows service state changed to svc.Stopped") return ssec, errno } +func (m *beatService) stop() { + close(m.done) +} + +func NotifyTermination() { + serviceInstance.stop() +} + func trySendState(s svc.State, changes chan<- svc.Status) { select { case changes <- svc.Status{State: s}: case <-time.After(500 * time.Millisecond): // should never happen, but don't make this blocking } } + +// WaitExecutionDone returns only after stop was reported to service manager. +// If response is not retrieved within 500 millisecond wait is aborted. +func WaitExecutionDone() { + if isWinService, err := svc.IsWindowsService(); err != nil || !isWinService { + // not a service, don't wait + return + } + + select { + case <-serviceInstance.executeFinished: + case <-time.After(500 * time.Millisecond): + } +} diff --git a/internal/pkg/agent/cmd/agentrun/run.go b/internal/pkg/agent/cmd/agentrun/run.go index c9f8eb5ad0c..0294219ed3c 100644 --- a/internal/pkg/agent/cmd/agentrun/run.go +++ b/internal/pkg/agent/cmd/agentrun/run.go @@ -14,6 +14,7 @@ import ( "github.com/elastic/elastic-agent-libs/service" + "github.com/elastic/elastic-agent/internal/pkg/agent/agentservice" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" "github.com/elastic/elastic-agent/internal/pkg/agent/cmd/common" "github.com/elastic/elastic-agent/internal/pkg/cli" @@ -81,8 +82,8 @@ func runService(testingMode bool, fleetInitTimeout time.Duration) error { // After this is run, the service is considered by the OS to be stopped. // This must be the first deferred cleanup task (last to execute). defer func() { - service.NotifyTermination() - service.WaitExecutionDone() + agentservice.NotifyTermination() + agentservice.WaitExecutionDone() }() service.BeforeRun() From a93cab0af3136c14e01aabb562da27de5bcee07f Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Wed, 3 Sep 2025 15:46:43 +0200 Subject: [PATCH 11/15] try init test --- testing/integration/ess/init_test.go | 109 +++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 testing/integration/ess/init_test.go diff --git a/testing/integration/ess/init_test.go b/testing/integration/ess/init_test.go new file mode 100644 index 00000000000..a1cba22da92 --- /dev/null +++ b/testing/integration/ess/init_test.go @@ -0,0 +1,109 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +//go:build integration + +package ess + +import ( + "context" + "os/exec" + "regexp" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/elastic/elastic-agent/pkg/core/process" + atesting "github.com/elastic/elastic-agent/pkg/testing" + "github.com/elastic/elastic-agent/pkg/testing/define" + "github.com/elastic/elastic-agent/pkg/testing/tools/testcontext" + "github.com/elastic/elastic-agent/testing/installtest" + "github.com/elastic/elastic-agent/testing/integration" +) + +func TestInitOrderNotDegraded(t *testing.T) { + define.Require(t, define.Requirements{ + Group: integration.Default, + // We require sudo for this test to run + // `elastic-agent install`. + Sudo: true, + + // It's not safe to run this test locally as it + // installs Elastic Agent. + Local: false, + OS: []define.OS{ + { + Type: define.Windows, + }, + }, + }) + + // Get path to Elastic Agent executable + fixture, err := define.NewFixtureFromLocalBuild(t, define.Version()) + require.NoError(t, err) + + ctx, cancel := testcontext.WithDeadline(t, context.Background(), time.Now().Add(10*time.Minute)) + defer cancel() + + // Prepare the Elastic Agent so the binary is extracted and ready to use. + err = fixture.Prepare(ctx) + require.NoError(t, err) + + // Run `elastic-agent install`. We use `--force` to prevent interactive + // execution. + opts := &atesting.InstallOpts{Force: true, Privileged: true} + out, err := fixture.Install(ctx, opts) + if err != nil { + t.Logf("install output: %s", out) + require.NoError(t, err) + } + + // Check that Agent was installed in default base path in unprivileged mode + require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, &installtest.CheckOpts{Privileged: true})) + + var withEnv process.CmdOption = func(c *exec.Cmd) error { + c.Env = append(c.Env, `GODEBUG="inittrace=1"`) + return nil + } + + output := strings.Builder{} + var redirectOutput process.CmdOption = func(c *exec.Cmd) error { + c.Stderr = &output + return nil + } + + // Switch to privileged mode + out, err = fixture.Exec(ctx, []string{"version"}, withEnv, redirectOutput) + if err != nil { + t.Logf("version output: %s", out) + require.NoError(t, err) + } + + relativeExec, pointInTimeMs := getAgentServiceStats(output.String()) + require.Less(t, pointInTimeMs, 200, "init took more than 200 ms") + require.Less(t, relativeExec, 70, "init moved past 70%") +} + +func getAgentServiceStats(output string) (int, int) { + var totalLines, agentServiceIdx int + var pointInTimeMs int + + for line := range strings.Lines(output) { + if strings.HasPrefix(line, "init github.com/elastic/elastic-agent/internal/pkg/agent/agentservice") { + re := regexp.MustCompile(`@(\d+)\s*ms`) + match := re.FindStringSubmatch(line) + if len(match) > 1 { + pointInTimeMs, _ = strconv.Atoi(match[1]) + } + agentServiceIdx = totalLines + } + totalLines++ + } + + relativeExec := (100 * agentServiceIdx) / totalLines + return relativeExec, pointInTimeMs +} From e3c23633cdc91df47b75c4f7ae173c2d617f959e Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Wed, 3 Sep 2025 17:32:27 +0200 Subject: [PATCH 12/15] edgecases --- testing/integration/ess/init_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testing/integration/ess/init_test.go b/testing/integration/ess/init_test.go index a1cba22da92..9385afc54b9 100644 --- a/testing/integration/ess/init_test.go +++ b/testing/integration/ess/init_test.go @@ -84,6 +84,7 @@ func TestInitOrderNotDegraded(t *testing.T) { } relativeExec, pointInTimeMs := getAgentServiceStats(output.String()) + require.NotEqual(t, relativeExec, 0, "agent service not initialized") require.Less(t, pointInTimeMs, 200, "init took more than 200 ms") require.Less(t, relativeExec, 70, "init moved past 70%") } @@ -93,6 +94,11 @@ func getAgentServiceStats(output string) (int, int) { var pointInTimeMs int for line := range strings.Lines(output) { + if !strings.HasPrefix(line, "init ") { + // we only count initializations + continue + } + if strings.HasPrefix(line, "init github.com/elastic/elastic-agent/internal/pkg/agent/agentservice") { re := regexp.MustCompile(`@(\d+)\s*ms`) match := re.FindStringSubmatch(line) From 86d50410f1fda1b5f3dc29325005a040e7f41379 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Thu, 4 Sep 2025 09:13:41 +0200 Subject: [PATCH 13/15] output already captured --- testing/integration/ess/init_test.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/testing/integration/ess/init_test.go b/testing/integration/ess/init_test.go index 9385afc54b9..497ad2e163d 100644 --- a/testing/integration/ess/init_test.go +++ b/testing/integration/ess/init_test.go @@ -70,20 +70,14 @@ func TestInitOrderNotDegraded(t *testing.T) { return nil } - output := strings.Builder{} - var redirectOutput process.CmdOption = func(c *exec.Cmd) error { - c.Stderr = &output - return nil - } - // Switch to privileged mode - out, err = fixture.Exec(ctx, []string{"version"}, withEnv, redirectOutput) + out, err = fixture.Exec(ctx, []string{"version"}, withEnv) if err != nil { t.Logf("version output: %s", out) require.NoError(t, err) } - relativeExec, pointInTimeMs := getAgentServiceStats(output.String()) + relativeExec, pointInTimeMs := getAgentServiceStats(string(out)) require.NotEqual(t, relativeExec, 0, "agent service not initialized") require.Less(t, pointInTimeMs, 200, "init took more than 200 ms") require.Less(t, relativeExec, 70, "init moved past 70%") From b858dd37ab30d43fa71daf06aa9f001e79b1c524 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Thu, 4 Sep 2025 11:29:31 +0200 Subject: [PATCH 14/15] output already captured --- testing/integration/ess/init_test.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/testing/integration/ess/init_test.go b/testing/integration/ess/init_test.go index 497ad2e163d..4e269af2f3f 100644 --- a/testing/integration/ess/init_test.go +++ b/testing/integration/ess/init_test.go @@ -8,7 +8,6 @@ package ess import ( "context" - "os/exec" "regexp" "strconv" "strings" @@ -65,14 +64,10 @@ func TestInitOrderNotDegraded(t *testing.T) { // Check that Agent was installed in default base path in unprivileged mode require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, &installtest.CheckOpts{Privileged: true})) - var withEnv process.CmdOption = func(c *exec.Cmd) error { - c.Env = append(c.Env, `GODEBUG="inittrace=1"`) - return nil - } - // Switch to privileged mode - out, err = fixture.Exec(ctx, []string{"version"}, withEnv) - if err != nil { + out, err = fixture.Exec(ctx, []string{"version"}, process.WithEnv(`GODEBUG=inittrace=1`)) + if err != nil && !strings.Contains(string(out), "Binary: ") { + // error of not communicating with not running agent is ok t.Logf("version output: %s", out) require.NoError(t, err) } From af577148e9b3472e62b61e519f051bf10d0b5f13 Mon Sep 17 00:00:00 2001 From: Michal Pristas Date: Thu, 4 Sep 2025 11:34:36 +0200 Subject: [PATCH 15/15] output already captured --- testing/integration/ess/init_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/testing/integration/ess/init_test.go b/testing/integration/ess/init_test.go index 4e269af2f3f..0036216a270 100644 --- a/testing/integration/ess/init_test.go +++ b/testing/integration/ess/init_test.go @@ -8,6 +8,7 @@ package ess import ( "context" + "os/exec" "regexp" "strconv" "strings" @@ -64,8 +65,12 @@ func TestInitOrderNotDegraded(t *testing.T) { // Check that Agent was installed in default base path in unprivileged mode require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, &installtest.CheckOpts{Privileged: true})) + var withEnv process.CmdOption = func(c *exec.Cmd) error { + c.Env = append(c.Env, `GODEBUG=inittrace=1`) + return nil + } // Switch to privileged mode - out, err = fixture.Exec(ctx, []string{"version"}, process.WithEnv(`GODEBUG=inittrace=1`)) + out, err = fixture.Exec(ctx, []string{"version"}, withEnv) if err != nil && !strings.Contains(string(out), "Binary: ") { // error of not communicating with not running agent is ok t.Logf("version output: %s", out)