diff --git a/README.md b/README.md index f48fbb45152..7bf785def31 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,63 @@ [![Build status](https://badge.buildkite.com/1d35bb40427cc6833979645b61ea214fc4b686a2ffe3a68bdf.svg)](https://buildkite.com/elastic/elastic-agent) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=elastic_elastic-agent&metric=coverage)](https://sonarcloud.io/summary/new_code?id=elastic_elastic-agent) -## Architecture / internal docs +## Architecture and Internals - [Agent architecture](docs/architecture.md) - [Component spec files](docs/component-specs.md) - [Policy configuration](docs/agent-policy.md) +## Official Documentation + +See https://www.elastic.co/guide/en/fleet/current/index.html. + +The source files for the offical Elastic Agent documentation are currently stored +in the [ingest-docs](https://github.com/elastic/ingest-docs/tree/main/docs/en/ingest-management) repository. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). -## Developer docs +## Developing -The source files for the general Elastic Agent documentation are currently stored -in the [ingest-docs](https://github.com/elastic/ingest-docs/tree/main/docs/en/ingest-management) repository. -The following docs are only focused on getting developers started building code for Elastic Agent. +The following are exclusively focused on getting developers started building code for Elastic Agent. + +### Development Installations + +> :warning: Development installations are not officially supported and are intended for Elastic Agent developers. + +If you are an Elastic employee, you already have an Information Security managed Elastic Agent installed on your machine for endpoint protection. +This prevents you from installing the Elastic Agent a second time for development without using a VM or Docker container. To eliminate this point +of friction, Elastic Agent has a development mode that permits installing the Elastic Agent on your machine a second time: + +```sh +# All other arguments to the install command are still supported when --develop is specified. +sudo ./elastic-agent install --develop +# The run command also supports the --develop option to allow running without installing when there is another agent on the machine. +./elastic-agent run -e --develop +``` + +Using the `--develop` option will install the agent in an isolated `Agent-Development` agent directory in the chosen base path. +Development agents enrolled in Fleet will have the `Development` tag added automatically. Using the default base path on MacOS you will see: + +```sh +sudo ls /Library/Elastic/ +Agent +Agent-Development +``` + +The `elastic-agent` command in the shell is replaced with `elastic-development-agent` to interact with the development agent: + +```sh +# For a privileged agent +sudo elastic-development-agent status +# For an unprivileged agent +sudo -u elastic-agent-user elastic-development-agent status +``` + +The primary restriction of `--develop` installations is that you cannot run Elastic Defend a second time on the same machine. Attempting to +install Defend twice will fail with resource conflicts. All other integrations should be usable provided conflicting configurations are +changed ahead of time. For example two agents cannot bind to the same `agent.monitoring.http.port` to expose their monitoring servers. ### Test Framework diff --git a/internal/pkg/agent/application/paths/common.go b/internal/pkg/agent/application/paths/common.go index 1f784e0a50c..8d5ac704323 100644 --- a/internal/pkg/agent/application/paths/common.go +++ b/internal/pkg/agent/application/paths/common.go @@ -321,11 +321,6 @@ func BinaryPath(baseDir, agentName string) string { return filepath.Join(binaryDir(baseDir), agentName) } -// InstallPath returns the top level directory Agent will be installed into. -func InstallPath(basePath string) string { - return filepath.Join(basePath, "Elastic", "Agent") -} - // TopBinaryPath returns the path to the Elastic Agent binary that is inside the Top directory. // // This always points to the symlink that points to the latest Elastic Agent version. diff --git a/internal/pkg/agent/application/paths/common_namespace.go b/internal/pkg/agent/application/paths/common_namespace.go new file mode 100644 index 00000000000..8b2651a30be --- /dev/null +++ b/internal/pkg/agent/application/paths/common_namespace.go @@ -0,0 +1,130 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// This file encapsulates the common paths that need to account for installation namepsaces. +// Installation namespaces allow multiple agents on the same machine. +package paths + +import ( + "fmt" + "path/filepath" + "strings" +) + +const ( + // installDirNamespaceFmt is the format of the directory agent will be installed to within the base path when using an installation namepsace. + // It is $BasePath/Agent-$namespace. + installDir = "Agent" + installDirNamespaceSep = "-" + installDirNamespacePrefix = installDir + installDirNamespaceSep + installDirNamespaceFmt = installDirNamespacePrefix + "%s" + + // DevelopmentNamespace defines the "well known" development namespace. + DevelopmentNamespace = "Development" + + // Service display names. Must be different from the ServiceName() on Windows. + serviceDisplayName = "Elastic Agent" + serviceDisplayNameNamespaceFmt = "Elastic Agent - %s" +) + +// installNamespace is the name of the agent's current installation namepsace. +var installNamespace string + +// SetInstallNamespace sets whether the agent is currently in or is being installed in an installation namespace. +// Removes leading and trailing whitespace +func SetInstallNamespace(namespace string) { + installNamespace = strings.TrimSpace(namespace) +} + +// InstallNamespace returns the name of the current installation namespace. Returns the empty string +// for the default namespace. For installed agents, the namespace is parsed from the installation +// directory name, since a unique directory name is required to avoid collisions between installed +// agents in the same base path. Before installation, the installation namespace must be configured +// using SetInstallNamespace(). +func InstallNamespace() string { + if installNamespace != "" { + return installNamespace + } + + if RunningInstalled() { + // Parse the namespace from the directory once to ensure deterministic behavior from startup. + namespace := parseNamespaceFromDir(filepath.Base(Top())) + installNamespace = namespace + } + + return "" +} + +func parseNamespaceFromDir(dir string) string { + parts := strings.SplitAfterN(dir, "-", 2) + if len(parts) <= 1 { + return "" + } else if parts[0] != installDirNamespacePrefix { + return "" + } + + return parts[1] +} + +// InInstallNamespace returns true if the agent is being installed in an installation namespace. +func InInstallNamespace() bool { + return InstallNamespace() != "" +} + +// InstallDirNameForNamespace returns the installation directory name for a given namespace. +// The installation directory name with a namespace is $BasePath/InstallDirNameForNamespace(). +func InstallDirNameForNamespace(namespace string) string { + if namespace == "" { + return installDir + } + + return fmt.Sprintf(installDirNamespaceFmt, namespace) +} + +// InstallPath returns the top level directory Agent will be installed into, accounting for any namespace. +func InstallPath(basePath string) string { + namespace := InstallNamespace() + return filepath.Join(basePath, "Elastic", InstallDirNameForNamespace(namespace)) +} + +// ServiceName returns the service name accounting for any namespace. +func ServiceName() string { + namespace := InstallNamespace() + if namespace == "" { + return serviceName + } + + return fmt.Sprintf(serviceNameNamespaceFmt, namespace) +} + +// ServiceDisplayName returns the service display name accounting for any namespace. +func ServiceDisplayName() string { + namespace := InstallNamespace() + if namespace == "" { + return serviceDisplayName + } + + return fmt.Sprintf(serviceDisplayNameNamespaceFmt, namespace) +} + +// ShellWrapperPath returns the shell wrapper path accounting for any namespace. +// The provided namespace is always lowercased for consistency. +func ShellWrapperPath() string { + namespace := InstallNamespace() + if namespace == "" { + return shellWrapperPath + } + + return ShellWrapperPathForNamespace(namespace) +} + +// ControlSocketRunSymlink returns the shell wrapper path accounting for any namespace. +// Does not auto detect the namespace because it is used outside of agent itself in the testing framework. +func ControlSocketRunSymlink(namespace string) string { + if namespace == "" { + return controlSocketRunSymlink + } + + return controlSocketRunSymlinkForNamespace(namespace) +} diff --git a/internal/pkg/agent/application/paths/common_namespace_test.go b/internal/pkg/agent/application/paths/common_namespace_test.go new file mode 100644 index 00000000000..6be3ffa72b8 --- /dev/null +++ b/internal/pkg/agent/application/paths/common_namespace_test.go @@ -0,0 +1,76 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package paths + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInstallNamespace(t *testing.T) { + namespace := "testing" + basePath := filepath.Join("base", "path") + + // Add whitespace to ensure it gets removed. + SetInstallNamespace(" " + namespace + "\t ") + + assert.Equal(t, namespace, InstallNamespace()) + assert.True(t, InInstallNamespace()) + assert.Equal(t, filepath.Join(basePath, "Elastic", fmt.Sprintf(installDirNamespaceFmt, namespace)), InstallPath(basePath)) + assert.Equal(t, fmt.Sprintf(serviceNameNamespaceFmt, namespace), ServiceName()) + assert.Equal(t, fmt.Sprintf(serviceDisplayNameNamespaceFmt, namespace), ServiceDisplayName()) + assert.Equal(t, ShellWrapperPathForNamespace(namespace), ShellWrapperPath()) + assert.Equal(t, controlSocketRunSymlinkForNamespace(namespace), ControlSocketRunSymlink(namespace)) +} + +func TestInstallNoNamespace(t *testing.T) { + namespace := "" + basePath := filepath.Join("base", "path") + SetInstallNamespace(namespace) + + assert.Equal(t, namespace, InstallNamespace()) + assert.False(t, InInstallNamespace()) + assert.Equal(t, filepath.Join(basePath, "Elastic", installDir), InstallPath(basePath)) + assert.Equal(t, serviceName, ServiceName()) + assert.Equal(t, serviceDisplayName, ServiceDisplayName()) + assert.Equal(t, shellWrapperPath, ShellWrapperPath()) + assert.Equal(t, controlSocketRunSymlink, ControlSocketRunSymlink(namespace)) +} + +func TestParseNamespaceFromDirName(t *testing.T) { + testcases := []struct { + name string + dir string + namespace string + }{ + {name: "empty", dir: "", namespace: ""}, + {name: "none", dir: "Agent", namespace: ""}, + {name: "develop", dir: "Agent-Development", namespace: "Development"}, + {name: "dashes", dir: "Agent-With-Dashes", namespace: "With-Dashes"}, + {name: "special", dir: "Agent-@!$%^&*()-_+=", namespace: "@!$%^&*()-_+="}, + {name: "format", dir: "Agent-%s%d%v%t", namespace: "%s%d%v%t"}, + {name: "spaces", dir: "Agent- Development \t", namespace: " Development \t"}, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + assert.Equalf(t, tc.namespace, parseNamespaceFromDir(tc.dir), "parsing %s", tc.dir) + + // Special case: if the directory is empty the install dir is the default "Agent" not "Agent-" + wantDir := tc.dir + if wantDir == "" { + wantDir = installDir + } + assert.Equal(t, wantDir, InstallDirNameForNamespace(tc.namespace)) + }) + } +} + +func TestParseNamespaceFromDirNameWithoutAgentPrefix(t *testing.T) { + assert.Equal(t, "", parseNamespaceFromDir("Beats-Development")) +} diff --git a/internal/pkg/agent/application/paths/paths_darwin.go b/internal/pkg/agent/application/paths/paths_darwin.go index 6630176660e..44ed4ba3fa1 100644 --- a/internal/pkg/agent/application/paths/paths_darwin.go +++ b/internal/pkg/agent/application/paths/paths_darwin.go @@ -14,19 +14,22 @@ const ( // for installing Elastic Agent's files. DefaultBasePath = "/Library" - // ControlSocketRunSymlink is the path to the symlink that should be + // controlSocketRunSymlink is the path to the symlink that should be // created to the control socket when Elastic Agent is running with root. - ControlSocketRunSymlink = "/var/run/elastic-agent.sock" + controlSocketRunSymlink = "/var/run/elastic-agent.sock" + controlSocketRunSymlinkNamespaceFmt = "/var/run/elastic-agent-%s.sock" - // ServiceName is the service name when installed. - ServiceName = "co.elastic.elastic-agent" + // serviceName is the service name when installed. + serviceName = "co.elastic.elastic-agent" + serviceNameNamespaceFmt = "co.elastic.elastic-agent-%s" - // ShellWrapperPath is the path to the installed shell wrapper. - ShellWrapperPath = "/usr/local/bin/elastic-agent" + // shellWrapperPath is the path to the installed shell wrapper. + shellWrapperPath = "/usr/local/bin/elastic-agent" + shellWrapperPathNamespaceFmt = "/usr/local/bin/elastic-%s-agent" // ShellWrapper is the wrapper that is installed. The %s must // be substituted with the appropriate top path. - ShellWrapper = `#!/bin/sh + ShellWrapperFmt = `#!/bin/sh exec %s/elastic-agent $@ ` ) diff --git a/internal/pkg/agent/application/paths/paths_linux.go b/internal/pkg/agent/application/paths/paths_linux.go index f5450142752..5f46602f833 100644 --- a/internal/pkg/agent/application/paths/paths_linux.go +++ b/internal/pkg/agent/application/paths/paths_linux.go @@ -14,21 +14,24 @@ const ( // for installing Elastic Agent's files. DefaultBasePath = "/opt" - // ServiceName is the service name when installed. - ServiceName = "elastic-agent" + // serviceName is the service name when installed. + serviceName = "elastic-agent" + serviceNameNamespaceFmt = "elastic-agent-%s" - // ShellWrapperPath is the path to the installed shell wrapper. - ShellWrapperPath = "/usr/bin/elastic-agent" + // shellWrapperPath is the path to the installed shell wrapper. + shellWrapperPath = "/usr/bin/elastic-agent" + shellWrapperPathNamespaceFmt = "/usr/bin/elastic-%s-agent" // ShellWrapper is the wrapper that is installed. The %s must // be substituted with the appropriate top path. - ShellWrapper = `#!/bin/sh + ShellWrapperFmt = `#!/bin/sh exec %s/elastic-agent $@ ` - // ControlSocketRunSymlink is the path to the symlink that should be + // controlSocketRunSymlink is the path to the symlink that should be // created to the control socket when Elastic Agent is running with root. - ControlSocketRunSymlink = "/run/elastic-agent.sock" + controlSocketRunSymlink = "/run/elastic-agent.sock" + controlSocketRunSymlinkNamespaceFmt = "/run/elastic-agent-%s.sock" ) // ArePathsEqual determines whether paths are equal taking case sensitivity of os into account. diff --git a/internal/pkg/agent/application/paths/paths_unix.go b/internal/pkg/agent/application/paths/paths_unix.go index 99d8cbe1ad5..164b7575b42 100644 --- a/internal/pkg/agent/application/paths/paths_unix.go +++ b/internal/pkg/agent/application/paths/paths_unix.go @@ -7,10 +7,27 @@ package paths import ( + "fmt" "path/filepath" "runtime" + "strings" ) +const () + +// shellWrapperPathForNamespace is a helper to work around not being able to use fmt.Sprintf +// unconditionally since shellWrapperPathNamespaceFmt is empty on Windows. The provided namespace is +// always lowercased for consistency. +func ShellWrapperPathForNamespace(namespace string) string { + return fmt.Sprintf(shellWrapperPathNamespaceFmt, strings.ToLower(namespace)) +} + +// controlSocketRunSymlinkForNamespace is a helper to work around not being able to use fmt.Sprintf +// unconditionally since controlSocketRunSymlinkNamespaceFmt is empty on Windows. +func controlSocketRunSymlinkForNamespace(namespace string) string { + return fmt.Sprintf(controlSocketRunSymlinkNamespaceFmt, namespace) +} + func initialControlSocketPath(topPath string) string { return ControlSocketFromPath(runtime.GOOS, topPath) } diff --git a/internal/pkg/agent/application/paths/paths_windows.go b/internal/pkg/agent/application/paths/paths_windows.go index 5c8f7008d6d..4734a34dd9f 100644 --- a/internal/pkg/agent/application/paths/paths_windows.go +++ b/internal/pkg/agent/application/paths/paths_windows.go @@ -20,19 +20,32 @@ const ( // for installing Elastic Agent's files. DefaultBasePath = `C:\Program Files` - // ControlSocketRunSymlink is not created on Windows. - ControlSocketRunSymlink = "" + // controlSocketRunSymlink is not created on Windows. + controlSocketRunSymlink = "" - // ServiceName is the service name when installed. - ServiceName = "Elastic Agent" + // serviceName is the service name when installed. + serviceName = "Elastic Agent" + serviceNameNamespaceFmt = "Elastic Agent - %s" - // ShellWrapperPath is the path to the installed shell wrapper. - ShellWrapperPath = "" // no wrapper on Windows + // shellWrapperPath is the path to the installed shell wrapper. + shellWrapperPath = "" // ShellWrapper is the wrapper that is installed. - ShellWrapper = "" // no wrapper on Windows + ShellWrapperFmt = "" // no wrapper on Windows ) +// ShellWrapperPathForNamespace is a helper to work around not being able to use fmt.Sprintf +// unconditionally since shellWrapperPath is empty on Windows. +func ShellWrapperPathForNamespace(namespace string) string { + return "" +} + +// controlSocketRunSymlinkForNamespace is a helper to work around not being able to use fmt.Sprintf +// unconditionally since controlSocketRunSymlink is empty on Windows. +func controlSocketRunSymlinkForNamespace(namespace string) string { + return "" +} + // ArePathsEqual determines whether paths are equal taking case sensitivity of os into account. func ArePathsEqual(expected, actual string) bool { return strings.EqualFold(expected, actual) diff --git a/internal/pkg/agent/application/reexec/reexec_windows.go b/internal/pkg/agent/application/reexec/reexec_windows.go index cc9eeb4950b..530ab4dcdf4 100644 --- a/internal/pkg/agent/application/reexec/reexec_windows.go +++ b/internal/pkg/agent/application/reexec/reexec_windows.go @@ -46,7 +46,7 @@ func reexec(log *logger.Logger, executable string, argOverrides ...string) error _ = t.Close() }() - args := []string{filepath.Base(executable), "reexec_windows", paths.ServiceName, strconv.Itoa(os.Getpid())} + args := []string{filepath.Base(executable), "reexec_windows", paths.ServiceName(), strconv.Itoa(os.Getpid())} args = append(args, argOverrides...) cmd := exec.Cmd{ Path: executable, diff --git a/internal/pkg/agent/application/upgrade/service_update_linux.go b/internal/pkg/agent/application/upgrade/service_update_linux.go index 618b632f93a..a09eddf4bdf 100644 --- a/internal/pkg/agent/application/upgrade/service_update_linux.go +++ b/internal/pkg/agent/application/upgrade/service_update_linux.go @@ -23,7 +23,7 @@ import ( func EnsureServiceConfigUpToDate() error { switch service.ChosenSystem().String() { case "linux-systemd": - unitFilePath := "/etc/systemd/system/" + paths.ServiceName + ".service" + unitFilePath := "/etc/systemd/system/" + paths.ServiceName() + ".service" updated, err := ensureSystemdServiceConfigUpToDate(unitFilePath) if err != nil { return err diff --git a/internal/pkg/agent/application/upgrade/service_update_linux_test.go b/internal/pkg/agent/application/upgrade/service_update_linux_test.go index fa7913a5e93..eb2563f71de 100644 --- a/internal/pkg/agent/application/upgrade/service_update_linux_test.go +++ b/internal/pkg/agent/application/upgrade/service_update_linux_test.go @@ -94,7 +94,7 @@ WantedBy=multi-user.target for name, test := range tests { t.Run(name, func(t *testing.T) { - unitFilePath := filepath.Join(t.TempDir(), paths.ServiceName+".service") + unitFilePath := filepath.Join(t.TempDir(), paths.ServiceName()+".service") err := os.WriteFile(unitFilePath, []byte(test.unitFileInitialContents), 0644) require.NoError(t, err) diff --git a/internal/pkg/agent/cmd/enroll_cmd.go b/internal/pkg/agent/cmd/enroll_cmd.go index 4dd721cdef1..edaff9ec3d3 100644 --- a/internal/pkg/agent/cmd/enroll_cmd.go +++ b/internal/pkg/agent/cmd/enroll_cmd.go @@ -557,6 +557,12 @@ func (c *enrollCmd) enroll(ctx context.Context, persistentConfig map[string]inte return errors.New(err, "acquiring metadata failed") } + // Automatically add the namespace as a tag when installed into a namepsace. + // Ensures the development agent is differentiated from others when on the same host. + if namespace := paths.InstallNamespace(); namespace != "" { + c.options.Tags = append(c.options.Tags, namespace) + } + r := &fleetapi.EnrollRequest{ EnrollAPIKey: c.options.EnrollAPIKey, Type: fleetapi.PermanentEnroll, diff --git a/internal/pkg/agent/cmd/install.go b/internal/pkg/agent/cmd/install.go index f6f0aac4122..55f5f77067e 100644 --- a/internal/pkg/agent/cmd/install.go +++ b/internal/pkg/agent/cmd/install.go @@ -25,6 +25,8 @@ import ( const ( flagInstallBasePath = "base-path" flagInstallUnprivileged = "unprivileged" + flagInstallDevelopment = "develop" + flagInstallNamespace = "namespace" ) func newInstallCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { @@ -48,6 +50,13 @@ would like the Agent to operate. cmd.Flags().BoolP("non-interactive", "n", false, "Install Elastic Agent in non-interactive mode which will not prompt on missing parameters but fails instead.") cmd.Flags().String(flagInstallBasePath, paths.DefaultBasePath, "The path where the Elastic Agent will be installed. It must be an absolute path.") cmd.Flags().Bool(flagInstallUnprivileged, false, "Install in unprivileged mode, limiting the access of the Elastic Agent. (beta)") + + cmd.Flags().String(flagInstallNamespace, "", "Install into an isolated namespace. Allows multiple Elastic Agents to be installed at once. (experimental)") + _ = cmd.Flags().MarkHidden(flagInstallNamespace) // For internal use only. + + cmd.Flags().Bool(flagInstallDevelopment, false, "Install into a standardized development namespace, may enable development specific options. Allows multiple Elastic Agents to be installed at once. (experimental)") + _ = cmd.Flags().MarkHidden(flagInstallDevelopment) // For internal use only. + addEnrollFlags(cmd) return cmd @@ -79,6 +88,20 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error { fmt.Fprintln(streams.Out, "Unprivileged installation mode enabled; this feature is currently in beta.") } + isDevelopmentMode, _ := cmd.Flags().GetBool(flagInstallDevelopment) + if isDevelopmentMode { + fmt.Fprintln(streams.Out, "Installing into development namespace; this is an experimental and currently unsupported feature.") + // For now, development mode only installs agent in a well known namespace to allow two agents on the same machine. + paths.SetInstallNamespace(paths.DevelopmentNamespace) + } + + namespace, _ := cmd.Flags().GetString(flagInstallNamespace) + if namespace != "" { + fmt.Fprintf(streams.Out, "Installing into namespace '%s'; this is an experimental and currently unsupported feature.\n", namespace) + // Overrides the development namespace if namespace was specified separately. + paths.SetInstallNamespace(namespace) + } + topPath := paths.InstallPath(basePath) status, reason := install.Status(topPath) @@ -221,7 +244,7 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error { err = install.StartService(topPath) if err != nil { progBar.Describe("Start Service failed, exiting...") - fmt.Fprintf(streams.Out, "Installation failed to start Elastic Agent service.\n") + fmt.Fprintf(streams.Out, "Installation failed to start '%s' service.\n", paths.ServiceName()) return fmt.Errorf("error starting service: %w", err) } progBar.Describe("Service Started") diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index f5c1db4d498..bd855c71ad0 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -58,6 +58,7 @@ import ( const ( agentName = "elastic-agent" fleetInitTimeoutName = "FLEET_SERVER_INIT_TIMEOUT" + flagRunDevelopment = "develop" ) type ( @@ -71,9 +72,17 @@ func newRunCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { Short: "Start the Elastic Agent", Long: "This command starts the Elastic Agent.", RunE: func(cmd *cobra.Command, _ []string) error { - // done very early so the encrypted store is never used + 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 { + if disableEncryptedStore || isDevelopmentMode { storage.DisableEncryptionDarwin() } fleetInitTimeout, _ := cmd.Flags().GetDuration("fleet-init-timeout") @@ -105,6 +114,9 @@ func newRunCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { 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 } @@ -314,23 +326,24 @@ func runElasticAgent(ctx context.Context, cancel context.CancelFunc, override cf // this provides backwards compatibility as the control socket was moved with the addition of --unprivileged // option during installation // - // Windows `paths.ControlSocketRunSymlink` is `""` so this is always skipped on Windows. - if isRoot && paths.RunningInstalled() && paths.ControlSocketRunSymlink != "" { + // Windows `paths.ControlSocketRunSymlink()` is `""` so this is always skipped on Windows. + controlSocketRunSymlink := paths.ControlSocketRunSymlink(paths.InstallNamespace()) + if isRoot && paths.RunningInstalled() && controlSocketRunSymlink != "" { socketPath := strings.TrimPrefix(paths.ControlSocket(), "unix://") - socketLog := controlLog.With("path", socketPath).With("link", paths.ControlSocketRunSymlink) + socketLog := controlLog.With("path", socketPath).With("link", controlSocketRunSymlink) // ensure it doesn't exist before creating the symlink - if err := os.Remove(paths.ControlSocketRunSymlink); err != nil && !errors.Is(err, os.ErrNotExist) { - socketLog.Errorf("Failed to remove existing control socket symlink %s: %s", paths.ControlSocketRunSymlink, err) + if err := os.Remove(controlSocketRunSymlink); err != nil && !errors.Is(err, os.ErrNotExist) { + socketLog.Errorf("Failed to remove existing control socket symlink %s: %s", controlSocketRunSymlink, err) } - if err := os.Symlink(socketPath, paths.ControlSocketRunSymlink); err != nil { - socketLog.Errorf("Failed to create control socket symlink %s -> %s: %s", paths.ControlSocketRunSymlink, socketPath, err) + if err := os.Symlink(socketPath, controlSocketRunSymlink); err != nil { + socketLog.Errorf("Failed to create control socket symlink %s -> %s: %s", controlSocketRunSymlink, socketPath, err) } else { - socketLog.Infof("Created control socket symlink %s -> %s; allowing unix://%s connection", paths.ControlSocketRunSymlink, socketPath, paths.ControlSocketRunSymlink) + socketLog.Infof("Created control socket symlink %s -> %s; allowing unix://%s connection", controlSocketRunSymlink, socketPath, controlSocketRunSymlink) } defer func() { // delete the symlink on exit; ignore the error - if err := os.Remove(paths.ControlSocketRunSymlink); err != nil { - socketLog.Errorf("Failed to remove control socket symlink %s: %s", paths.ControlSocketRunSymlink, err) + if err := os.Remove(controlSocketRunSymlink); err != nil { + socketLog.Errorf("Failed to remove control socket symlink %s: %s", controlSocketRunSymlink, err) } }() } diff --git a/internal/pkg/agent/cmd/run_windows.go b/internal/pkg/agent/cmd/run_windows.go index 94505d87d4b..308de4f1a57 100644 --- a/internal/pkg/agent/cmd/run_windows.go +++ b/internal/pkg/agent/cmd/run_windows.go @@ -16,7 +16,7 @@ import ( // the Application EventLog. This is a best effort logger and no // errors are returned. func logExternal(msg string) { - eLog, err2 := eventlog.Open(paths.ServiceName) + eLog, err2 := eventlog.Open(paths.ServiceName()) if err2 != nil { return } diff --git a/internal/pkg/agent/configuration/grpc.go b/internal/pkg/agent/configuration/grpc.go index f768694dc25..f60b7d8de1f 100644 --- a/internal/pkg/agent/configuration/grpc.go +++ b/internal/pkg/agent/configuration/grpc.go @@ -6,6 +6,8 @@ package configuration import ( "fmt" + + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" ) // GRPCConfig is a configuration of GRPC server. @@ -18,11 +20,22 @@ type GRPCConfig struct { // DefaultGRPCConfig creates a default server configuration. func DefaultGRPCConfig() *GRPCConfig { + // When in an installation namespace, bind to port zero to select a random free port to avoid + // collisions with any already installed Elastic Agent. Ideally we'd always bind to port zero, + // but this would be breaking for users that had to manually whitelist the gRPC port in local + // firewall rules. + // + // Note: this uses local TCP by default. A port of -1 switches to unix domain sockets / named + // pipes. Using domain sockets by default is preferable but is currently blocked because the + // gRPC library endpoint security uses does not support Windows named pipes. + defaultPort := uint16(6789) + if paths.InInstallNamespace() { + defaultPort = 0 + } + return &GRPCConfig{ - Address: "localhost", - // [gRPC:8.15] The line below is commented out for 8.14 and should replace the current port default once Endpoint is ready for domain socket gRPC - // Port: -1, // -1 (negative) port value by default enabled "local" rpc utilizing domain sockets and named pipes - Port: 6789, // Set TCP gRPC by default + Address: "localhost", + Port: defaultPort, MaxMsgSize: 1024 * 1024 * 100, // grpc default 4MB is unsufficient for diagnostics CheckinChunkingDisabled: false, // on by default } diff --git a/internal/pkg/agent/install/install.go b/internal/pkg/agent/install/install.go index ea9e4e350dc..f6b7e10fb05 100644 --- a/internal/pkg/agent/install/install.go +++ b/internal/pkg/agent/install/install.go @@ -102,13 +102,13 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p pt.Describe("Successfully copied files") // place shell wrapper, if present on platform - if paths.ShellWrapperPath != "" { - pathDir := filepath.Dir(paths.ShellWrapperPath) + if paths.ShellWrapperPath() != "" { + pathDir := filepath.Dir(paths.ShellWrapperPath()) err = os.MkdirAll(pathDir, 0755) if err != nil { return utils.FileOwner{}, errors.New( err, - fmt.Sprintf("failed to create directory (%s) for shell wrapper (%s)", pathDir, paths.ShellWrapperPath), + fmt.Sprintf("failed to create directory (%s) for shell wrapper (%s)", pathDir, paths.ShellWrapperPath()), errors.M("directory", pathDir)) } // Install symlink for darwin instead of the wrapper script. @@ -117,32 +117,32 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p // This is specifically important for osquery FDA permissions at the moment. if runtime.GOOS == darwin { // Check if previous shell wrapper or symlink exists and remove it so it can be overwritten - if _, err := os.Lstat(paths.ShellWrapperPath); err == nil { - if err := os.Remove(paths.ShellWrapperPath); err != nil { + if _, err := os.Lstat(paths.ShellWrapperPath()); err == nil { + if err := os.Remove(paths.ShellWrapperPath()); err != nil { return utils.FileOwner{}, errors.New( err, - fmt.Sprintf("failed to remove (%s)", paths.ShellWrapperPath), - errors.M("destination", paths.ShellWrapperPath)) + fmt.Sprintf("failed to remove (%s)", paths.ShellWrapperPath()), + errors.M("destination", paths.ShellWrapperPath())) } } - err = os.Symlink(filepath.Join(topPath, paths.BinaryName), paths.ShellWrapperPath) + err = os.Symlink(filepath.Join(topPath, paths.BinaryName), paths.ShellWrapperPath()) if err != nil { return utils.FileOwner{}, errors.New( err, - fmt.Sprintf("failed to create elastic-agent symlink (%s)", paths.ShellWrapperPath), - errors.M("destination", paths.ShellWrapperPath)) + fmt.Sprintf("failed to create elastic-agent symlink (%s)", paths.ShellWrapperPath()), + errors.M("destination", paths.ShellWrapperPath())) } } else { // We use strings.Replace instead of fmt.Sprintf here because, with the // latter, govet throws a false positive error here: "fmt.Sprintf call has // arguments but no formatting directives". - shellWrapper := strings.Replace(paths.ShellWrapper, "%s", topPath, -1) - err = os.WriteFile(paths.ShellWrapperPath, []byte(shellWrapper), 0755) + shellWrapper := strings.Replace(paths.ShellWrapperFmt, "%s", topPath, -1) + err = os.WriteFile(paths.ShellWrapperPath(), []byte(shellWrapper), 0755) if err != nil { return utils.FileOwner{}, errors.New( err, - fmt.Sprintf("failed to write shell wrapper (%s)", paths.ShellWrapperPath), - errors.M("destination", paths.ShellWrapperPath)) + fmt.Sprintf("failed to write shell wrapper (%s)", paths.ShellWrapperPath()), + errors.M("destination", paths.ShellWrapperPath())) } } } @@ -158,10 +158,10 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p if err != nil { return ownership, fmt.Errorf("failed to perform permission changes on path %s: %w", topPath, err) } - if paths.ShellWrapperPath != "" { - err = perms.FixPermissions(paths.ShellWrapperPath, perms.WithOwnership(ownership)) + if paths.ShellWrapperPath() != "" { + err = perms.FixPermissions(paths.ShellWrapperPath(), perms.WithOwnership(ownership)) if err != nil { - return ownership, fmt.Errorf("failed to perform permission changes on path %s: %w", paths.ShellWrapperPath, err) + return ownership, fmt.Errorf("failed to perform permission changes on path %s: %w", paths.ShellWrapperPath(), err) } } @@ -344,7 +344,7 @@ func StartService(topPath string) error { } err = svc.Start() if err != nil { - return fmt.Errorf("failed to start service (%s): %w", paths.ServiceName, err) + return fmt.Errorf("failed to start service (%s): %w", paths.ServiceName(), err) } return nil } @@ -358,11 +358,11 @@ func StopService(topPath string, timeout time.Duration, interval time.Duration) } err = svc.Stop() if err != nil { - return fmt.Errorf("failed to stop service (%s): %w", paths.ServiceName, err) + return fmt.Errorf("failed to stop service (%s): %w", paths.ServiceName(), err) } - err = isStopped(timeout, interval, paths.ServiceName) + err = isStopped(timeout, interval, paths.ServiceName()) if err != nil { - return fmt.Errorf("failed to stop service (%s): %w", paths.ServiceName, err) + return fmt.Errorf("failed to stop service (%s): %w", paths.ServiceName(), err) } return nil } @@ -376,7 +376,7 @@ func RestartService(topPath string) error { } err = svc.Restart() if err != nil { - return fmt.Errorf("failed to restart service (%s): %w", paths.ServiceName, err) + return fmt.Errorf("failed to restart service (%s): %w", paths.ServiceName(), err) } return nil } @@ -402,13 +402,13 @@ func InstallService(topPath string, ownership utils.FileOwner, username string, } err = svc.Install() if err != nil { - return fmt.Errorf("failed to install service (%s): %w", paths.ServiceName, err) + return fmt.Errorf("failed to install service (%s): %w", paths.ServiceName(), err) } err = serviceConfigure(ownership) if err != nil { // ignore error _ = svc.Uninstall() - return fmt.Errorf("failed to configure service (%s): %w", paths.ServiceName, err) + return fmt.Errorf("failed to configure service (%s): %w", paths.ServiceName(), err) } return nil } @@ -421,7 +421,7 @@ func UninstallService(topPath string) error { } err = svc.Uninstall() if err != nil { - return fmt.Errorf("failed to uninstall service (%s): %w", paths.ServiceName, err) + return fmt.Errorf("failed to uninstall service (%s): %w", paths.ServiceName(), err) } return nil } diff --git a/internal/pkg/agent/install/install_windows.go b/internal/pkg/agent/install/install_windows.go index 43715ab3d99..cc53fcf670f 100644 --- a/internal/pkg/agent/install/install_windows.go +++ b/internal/pkg/agent/install/install_windows.go @@ -83,7 +83,7 @@ func withServiceOptions(username string, groupName string) ([]serviceOpt, error) // ReExec is not possible on Windows. func serviceConfigure(ownership utils.FileOwner) error { // Modify registry to allow logging to eventlog as "Elastic Agent". - err := eventlog.InstallAsEventCreate(paths.ServiceName, eventlog.Info|eventlog.Warning|eventlog.Error) + err := eventlog.InstallAsEventCreate(paths.ServiceName(), eventlog.Info|eventlog.Warning|eventlog.Error) if err != nil && !strings.Contains(err.Error(), "registry key already exists") { return fmt.Errorf("unable to create registry key for logging: %w", err) } @@ -103,9 +103,9 @@ func serviceConfigure(ownership utils.FileOwner) error { if err != nil { return fmt.Errorf("failed to get DACL from security descriptor: %w", err) } - err = windows.SetNamedSecurityInfo(paths.ServiceName, windows.SE_SERVICE, windows.DACL_SECURITY_INFORMATION, nil, nil, dacl, nil) + err = windows.SetNamedSecurityInfo(paths.ServiceName(), windows.SE_SERVICE, windows.DACL_SECURITY_INFORMATION, nil, nil, dacl, nil) if err != nil { - return fmt.Errorf("failed to set DACL for service(%s): %w", paths.ServiceName, err) + return fmt.Errorf("failed to set DACL for service(%s): %w", paths.ServiceName(), err) } return nil } diff --git a/internal/pkg/agent/install/svc.go b/internal/pkg/agent/install/svc.go index 3fc599fb80a..eacc39fc0a1 100644 --- a/internal/pkg/agent/install/svc.go +++ b/internal/pkg/agent/install/svc.go @@ -15,9 +15,6 @@ import ( ) const ( - // ServiceDisplayName is the service display name for the service. - ServiceDisplayName = "Elastic Agent" - // ServiceDescription is the description for the service. ServiceDescription = "Elastic Agent is a unified agent to observe, monitor and protect your system." @@ -31,8 +28,8 @@ const ( // ExecutablePath returns the path for the installed Agents executable. func ExecutablePath(topPath string) string { exec := filepath.Join(topPath, paths.BinaryName) - if paths.ShellWrapperPath != "" { - exec = paths.ShellWrapperPath + if paths.ShellWrapperPath() != "" { + exec = paths.ShellWrapperPath() } return exec } @@ -75,8 +72,8 @@ func newService(topPath string, opt ...serviceOpt) (service.Service, error) { } cfg := &service.Config{ - Name: paths.ServiceName, - DisplayName: ServiceDisplayName, + Name: paths.ServiceName(), + DisplayName: paths.ServiceDisplayName(), Description: ServiceDescription, Executable: ExecutablePath(topPath), WorkingDirectory: topPath, @@ -107,8 +104,8 @@ func newService(topPath string, opt ...serviceOpt) (service.Service, error) { // Set the stdout and stderr logs to be inside the installation directory, ensures that the // executing user for the service can write to the directory for the logs. - cfg.Option["StandardOutPath"] = filepath.Join(topPath, fmt.Sprintf("%s.out.log", paths.ServiceName)) - cfg.Option["StandardErrorPath"] = filepath.Join(topPath, fmt.Sprintf("%s.err.log", paths.ServiceName)) + cfg.Option["StandardOutPath"] = filepath.Join(topPath, fmt.Sprintf("%s.out.log", paths.ServiceName())) + cfg.Option["StandardErrorPath"] = filepath.Join(topPath, fmt.Sprintf("%s.err.log", paths.ServiceName())) } return service.New(nil, cfg) diff --git a/internal/pkg/agent/install/switch.go b/internal/pkg/agent/install/switch.go index 8412ce28cb2..9ae6a2a74f7 100644 --- a/internal/pkg/agent/install/switch.go +++ b/internal/pkg/agent/install/switch.go @@ -58,10 +58,10 @@ func SwitchExecutingMode(topPath string, pt *progressbar.ProgressBar, username s if err != nil { return fmt.Errorf("failed to perform permission changes on path %s: %w", topPath, err) } - if paths.ShellWrapperPath != "" { - err = perms.FixPermissions(paths.ShellWrapperPath, perms.WithOwnership(ownership)) + if paths.ShellWrapperPath() != "" { + err = perms.FixPermissions(paths.ShellWrapperPath(), perms.WithOwnership(ownership)) if err != nil { - return fmt.Errorf("failed to perform permission changes on path %s: %w", paths.ShellWrapperPath, err) + return fmt.Errorf("failed to perform permission changes on path %s: %w", paths.ShellWrapperPath(), err) } } diff --git a/internal/pkg/agent/install/uninstall.go b/internal/pkg/agent/install/uninstall.go index ed8902d9ba2..703aef69e4e 100644 --- a/internal/pkg/agent/install/uninstall.go +++ b/internal/pkg/agent/install/uninstall.go @@ -90,13 +90,13 @@ func Uninstall(cfgFile, topPath, uninstallToken string, log *logp.Logger, pt *pr } // remove, if present on platform - if paths.ShellWrapperPath != "" { - err = os.Remove(paths.ShellWrapperPath) + if paths.ShellWrapperPath() != "" { + err = os.Remove(paths.ShellWrapperPath()) if !os.IsNotExist(err) && err != nil { return aerrors.New( err, - fmt.Sprintf("failed to remove shell wrapper (%s)", paths.ShellWrapperPath), - aerrors.M("destination", paths.ShellWrapperPath)) + fmt.Sprintf("failed to remove shell wrapper (%s)", paths.ShellWrapperPath()), + aerrors.M("destination", paths.ShellWrapperPath())) } } diff --git a/magefile.go b/magefile.go index 52298ba6925..f3024d78242 100644 --- a/magefile.go +++ b/magefile.go @@ -108,7 +108,7 @@ func init() { common.RegisterCheckDeps(Update, Check.All) test.RegisterDeps(UnitTest) devtools.BeatLicense = "Elastic License" - devtools.BeatDescription = "Agent manages other beats based on configuration provided." + devtools.BeatDescription = "Elastic Agent - single, unified way to add monitoring for logs, metrics, and other types of data to a host." devtools.Platforms = devtools.Platforms.Filter("!linux/386") devtools.Platforms = devtools.Platforms.Filter("!windows/386") diff --git a/pkg/testing/fixture.go b/pkg/testing/fixture.go index 3472fcdc663..d2e5b2e40f5 100644 --- a/pkg/testing/fixture.go +++ b/pkg/testing/fixture.go @@ -831,10 +831,15 @@ func (f *Fixture) EnsurePrepared(ctx context.Context) error { func (f *Fixture) binaryPath() string { workDir := f.workDir if f.installed { + installDir := "Agent" + if f.installOpts != nil && f.installOpts.Namespace != "" { + installDir = paths.InstallDirNameForNamespace(f.installOpts.Namespace) + } + if f.installOpts != nil && f.installOpts.BasePath != "" { - workDir = filepath.Join(f.installOpts.BasePath, "Elastic", "Agent") + workDir = filepath.Join(f.installOpts.BasePath, "Elastic", installDir) } else { - workDir = filepath.Join(paths.DefaultBasePath, "Elastic", "Agent") + workDir = filepath.Join(paths.DefaultBasePath, "Elastic", installDir) } } if f.packageFormat == "deb" || f.packageFormat == "rpm" { diff --git a/pkg/testing/fixture_install.go b/pkg/testing/fixture_install.go index 1ed50d0add5..81aa7db3ac6 100644 --- a/pkg/testing/fixture_install.go +++ b/pkg/testing/fixture_install.go @@ -14,6 +14,7 @@ import ( "io/fs" "os" "os/exec" + "path" "path/filepath" "runtime" "strconv" @@ -105,6 +106,8 @@ type InstallOpts struct { NonInteractive bool // --non-interactive ProxyURL string // --proxy-url DelayEnroll bool // --delay-enroll + Develop bool // --develop, not supported for DEB and RPM. Calling Install() sets Namespace to the development namespace so that checking only for a Namespace is sufficient. + Namespace string // --namespace, not supported for DEB and RPM. Privileged bool // inverse of --unprivileged (as false is the default) @@ -112,7 +115,7 @@ type InstallOpts struct { FleetBootstrapOpts } -func (i InstallOpts) toCmdArgs(operatingSystem string) ([]string, error) { +func (i *InstallOpts) toCmdArgs(operatingSystem string) ([]string, error) { var args []string if i.BasePath != "" { args = append(args, "--base-path", i.BasePath) @@ -135,6 +138,16 @@ func (i InstallOpts) toCmdArgs(operatingSystem string) ([]string, error) { if !i.Privileged { args = append(args, "--unprivileged") } + if i.Namespace != "" { + args = append(args, "--namespace="+i.Namespace) + } + if i.Develop { + args = append(args, "--develop") + if i.Namespace == "" { + // If --namespace was used it will override the development namespace. + i.Namespace = paths.DevelopmentNamespace + } + } args = append(args, i.EnrollOpts.toCmdArgs()...) args = append(args, i.FleetBootstrapOpts.toCmdArgs()...) @@ -152,8 +165,10 @@ func (i InstallOpts) toCmdArgs(operatingSystem string) ([]string, error) { func (f *Fixture) Install(ctx context.Context, installOpts *InstallOpts, opts ...process.CmdOption) ([]byte, error) { f.t.Logf("[test %s] Inside fixture install function", f.t.Name()) - // check for running agents before installing, but proceed anyway - assert.Empty(f.t, getElasticAgentProcesses(f.t), "there should be no running agent at beginning of Install()") + // check for running agents before installing, but only if not installed into a namespace whose point is allowing two agents at once. + if installOpts != nil && !installOpts.Develop && installOpts.Namespace == "" { + assert.Empty(f.t, getElasticAgentProcesses(f.t), "there should be no running agent at beginning of Install()") + } switch f.packageFormat { case "targz", "zip": @@ -196,14 +211,21 @@ func (f *Fixture) installNoPkgManager(ctx context.Context, installOpts *InstallO f.installed = true f.installOpts = installOpts + installDir := "Agent" + socketRunSymlink := paths.ControlSocketRunSymlink("") + if installOpts.Namespace != "" { + installDir = paths.InstallDirNameForNamespace(installOpts.Namespace) + socketRunSymlink = paths.ControlSocketRunSymlink(installOpts.Namespace) + } + if installOpts.BasePath == "" { - f.workDir = filepath.Join(paths.DefaultBasePath, "Elastic", "Agent") + f.workDir = filepath.Join(paths.DefaultBasePath, "Elastic", installDir) } else { - f.workDir = filepath.Join(installOpts.BasePath, "Elastic", "Agent") + f.workDir = filepath.Join(installOpts.BasePath, "Elastic", installDir) } // we just installed agent, the control socket is at a well-known location - socketPath := fmt.Sprintf("unix://%s", paths.ControlSocketRunSymlink) // use symlink as that works for all versions + socketPath := fmt.Sprintf("unix://%s", socketRunSymlink) // use symlink as that works for all versions if runtime.GOOS == "windows" { // Windows uses a fixed named pipe, that is always the same. // It is the same even running in unprivileged mode. @@ -229,6 +251,12 @@ func (f *Fixture) installNoPkgManager(ctx context.Context, installOpts *InstallO sanitizedTestName := strings.ReplaceAll(f.t.Name(), "/", "-") filePath := filepath.Join(dir, "build", "diagnostics", fmt.Sprintf("TEST-%s-%s-%s-ProcessDump.json", sanitizedTestName, f.operatingSystem, f.architecture)) + fileDir := path.Dir(filePath) + if err := os.MkdirAll(fileDir, 0777); err != nil { + f.t.Logf("failed to dump process; failed to create directory %s: %s", fileDir, err) + return + } + f.t.Logf("Dumping running processes in %s", filePath) file, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { @@ -250,7 +278,20 @@ func (f *Fixture) installNoPkgManager(ctx context.Context, installOpts *InstallO f.t.Cleanup(func() { // check for running agents after uninstall had a chance to run - assert.Empty(f.t, getElasticAgentProcesses(f.t), "there should be no running agent at the end of the test") + processes := getElasticAgentProcesses(f.t) + + // there can be a single agent left when using --develop mode + if f.installOpts != nil && f.installOpts.Namespace != "" { + assert.LessOrEqualf(f.t, len(processes), 1, "More than one agent left running at the end of the test when second agent in namespace %s was used: %v", f.installOpts.Namespace, processes) + // The agent left running has to be the non-development agent. The development agent should be uninstalled first as a convention. + if len(processes) > 0 { + assert.NotContainsf(f.t, processes[0].Cmdline, paths.InstallDirNameForNamespace(f.installOpts.Namespace), + "The agent installed into namespace %s was left running at the end of the test or was not uninstalled first: %v", f.installOpts.Namespace, processes) + } + return + } + + assert.Empty(f.t, processes, "there should be no running agent at the end of the test") }) f.t.Cleanup(func() { diff --git a/testing/installtest/checks.go b/testing/installtest/checks.go index 66db06b4c19..6160658b28a 100644 --- a/testing/installtest/checks.go +++ b/testing/installtest/checks.go @@ -11,11 +11,12 @@ import ( "path/filepath" "runtime" + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" atesting "github.com/elastic/elastic-agent/pkg/testing" "github.com/elastic/elastic-agent/pkg/testing/define" ) -func DefaultTopPath() string { +func defaultBasePath() string { var defaultBasePath string switch runtime.GOOS { case "darwin": @@ -25,10 +26,23 @@ func DefaultTopPath() string { case "windows": defaultBasePath = `C:\Program Files` } - return filepath.Join(defaultBasePath, "Elastic", "Agent") + return defaultBasePath +} + +func DefaultTopPath() string { + return filepath.Join(defaultBasePath(), "Elastic", "Agent") +} + +func NamespaceTopPath(namespace string) string { + return filepath.Join(defaultBasePath(), "Elastic", paths.InstallDirNameForNamespace(namespace)) +} + +type CheckOpts struct { + Privileged bool + Namespace string } -func CheckSuccess(ctx context.Context, f *atesting.Fixture, topPath string, unprivileged bool) error { +func CheckSuccess(ctx context.Context, f *atesting.Fixture, topPath string, opts *CheckOpts) error { // Use default topPath if one not defined. if topPath == "" { topPath = DefaultTopPath() @@ -42,7 +56,7 @@ func CheckSuccess(ctx context.Context, f *atesting.Fixture, topPath string, unpr // Check that a few expected installed files are present installedBinPath := filepath.Join(topPath, exeOnWindows("elastic-agent")) installedDataPath := filepath.Join(topPath, "data") - installMarkerPath := filepath.Join(topPath, ".installed") + installMarkerPath := filepath.Join(topPath, paths.MarkerFileName) _, err = os.Stat(installedBinPath) if err != nil { @@ -58,7 +72,7 @@ func CheckSuccess(ctx context.Context, f *atesting.Fixture, topPath string, unpr } // Specific checks depending on the platform. - return checkPlatform(ctx, f, topPath, unprivileged) + return checkPlatform(ctx, f, topPath, opts) } func exeOnWindows(filename string) string { diff --git a/testing/installtest/checks_unix.go b/testing/installtest/checks_unix.go index 841f3e97388..1cba1fd0925 100644 --- a/testing/installtest/checks_unix.go +++ b/testing/installtest/checks_unix.go @@ -20,8 +20,8 @@ import ( atesting "github.com/elastic/elastic-agent/pkg/testing" ) -func checkPlatform(ctx context.Context, _ *atesting.Fixture, topPath string, unprivileged bool) error { - if unprivileged { +func checkPlatform(ctx context.Context, _ *atesting.Fixture, topPath string, opts *CheckOpts) error { + if !opts.Privileged { // Check that the elastic-agent user/group exist. uid, err := install.FindUID(install.ElasticUsername) if err != nil { @@ -64,10 +64,15 @@ func checkPlatform(ctx context.Context, _ *atesting.Fixture, topPath string, unp } // Executing `elastic-agent status` as the `elastic-agent-user` user should work. + shellWrapperName := "elastic-agent" + if opts.Namespace != "" { + shellWrapperName = paths.ShellWrapperPathForNamespace(opts.Namespace) + } + var output []byte err = waitForNoError(ctx, func(_ context.Context) error { // #nosec G204 -- user cannot inject any parameters to this command - cmd := exec.Command("sudo", "-u", install.ElasticUsername, "elastic-agent", "status") + cmd := exec.Command("sudo", "-u", install.ElasticUsername, shellWrapperName, "status") output, err = cmd.CombinedOutput() if err != nil { return fmt.Errorf("elastic-agent status failed: %w (output: %s)", err, output) @@ -80,7 +85,7 @@ func checkPlatform(ctx context.Context, _ *atesting.Fixture, topPath string, unp originalUser := os.Getenv("SUDO_USER") if originalUser != "" { // #nosec G204 -- user cannot inject any parameters to this command - cmd := exec.Command("sudo", "-u", originalUser, "elastic-agent", "status") + cmd := exec.Command("sudo", "-u", originalUser, shellWrapperName, "status") output, err := cmd.CombinedOutput() if err == nil { return fmt.Errorf("sudo -u %s elastic-agent didn't fail: got output: %s", originalUser, output) diff --git a/testing/installtest/checks_windows.go b/testing/installtest/checks_windows.go index d16fbdf19c2..6a87f43f173 100644 --- a/testing/installtest/checks_windows.go +++ b/testing/installtest/checks_windows.go @@ -36,7 +36,7 @@ type accessAllowedAce struct { SidStart uint32 } -func checkPlatform(ctx context.Context, f *atesting.Fixture, topPath string, unprivileged bool) error { +func checkPlatform(ctx context.Context, f *atesting.Fixture, topPath string, opts *CheckOpts) error { secInfo, err := windows.GetNamedSecurityInfo(topPath, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION) if err != nil { return fmt.Errorf("GetNamedSecurityInfo failed for %s: %w", topPath, err) @@ -52,7 +52,7 @@ func checkPlatform(ctx context.Context, f *atesting.Fixture, topPath string, unp if err != nil { return fmt.Errorf("failed to get allowed SID's for %s: %w", topPath, err) } - if unprivileged { + if !opts.Privileged { // Check that the elastic-agent user/group exist. uid, err := install.FindUID(install.ElasticUsername) if err != nil { diff --git a/testing/integration/install_privileged_test.go b/testing/integration/install_privileged_test.go deleted file mode 100644 index 8b4f64a8da7..00000000000 --- a/testing/integration/install_privileged_test.go +++ /dev/null @@ -1,106 +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; -// you may not use this file except in compliance with the Elastic License. - -//go:build integration - -package integration - -import ( - "context" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" - - 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" -) - -func TestInstallPrivilegedWithoutBasePath(t *testing.T) { - define.Require(t, define.Requirements{ - Group: 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, - }) - - // 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 - require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, false)) - t.Run("check agent package version", testAgentPackageVersion(ctx, fixture, true)) -} - -func TestInstallPrivilegedWithBasePath(t *testing.T) { - define.Require(t, define.Requirements{ - Group: 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, - }) - - // 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) - - // Set up random temporary directory to serve as base path for Elastic Agent - // installation. - tmpDir := t.TempDir() - randomBasePath := filepath.Join(tmpDir, strings.ToLower(randStr(8))) - - // Run `elastic-agent install`. We use `--force` to prevent interactive - // execution. - opts := &atesting.InstallOpts{ - BasePath: randomBasePath, - 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 the custom base path - topPath := filepath.Join(randomBasePath, "Elastic", "Agent") - require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, false)) - t.Run("check agent package version", testAgentPackageVersion(ctx, fixture, true)) -} diff --git a/testing/integration/install_test.go b/testing/integration/install_test.go index 8fc9df9d6fa..f2159752943 100644 --- a/testing/integration/install_test.go +++ b/testing/integration/install_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" 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" @@ -51,8 +52,8 @@ func TestInstallWithoutBasePath(t *testing.T) { // Run `elastic-agent install`. We use `--force` to prevent interactive // execution. - opts := &atesting.InstallOpts{Force: true, Privileged: false} - out, err := fixture.Install(ctx, opts) + opts := atesting.InstallOpts{Force: true, Privileged: false} + out, err := fixture.Install(ctx, &opts) if err != nil { t.Logf("install output: %s", out) require.NoError(t, err) @@ -60,8 +61,11 @@ func TestInstallWithoutBasePath(t *testing.T) { // Check that Agent was installed in default base path topPath := installtest.DefaultTopPath() - require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, true)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, &installtest.CheckOpts{Privileged: opts.Privileged})) + t.Run("check agent package version", testAgentPackageVersion(ctx, fixture, true)) + t.Run("check second agent installs with --develop", testSecondAgentCanInstall(ctx, fixture, "", true, opts)) + // Make sure uninstall from within the topPath fails on Windows if runtime.GOOS == "windows" { cwd, err := os.Getwd() @@ -75,7 +79,6 @@ func TestInstallWithoutBasePath(t *testing.T) { require.Error(t, err, "uninstall should have failed") require.Containsf(t, string(out), "uninstall must be run from outside the installed path", "expected error string not found in: %s err: %s", out, err) } - } func TestInstallWithBasePath(t *testing.T) { @@ -123,12 +126,12 @@ func TestInstallWithBasePath(t *testing.T) { // Run `elastic-agent install`. We use `--force` to prevent interactive // execution. - opts := &atesting.InstallOpts{ + opts := atesting.InstallOpts{ BasePath: basePath, Force: true, Privileged: false, } - out, err := fixture.Install(ctx, opts) + out, err := fixture.Install(ctx, &opts) if err != nil { t.Logf("install output: %s", out) require.NoError(t, err) @@ -136,8 +139,11 @@ func TestInstallWithBasePath(t *testing.T) { // Check that Agent was installed in the custom base path topPath := filepath.Join(basePath, "Elastic", "Agent") - require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, true)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, &installtest.CheckOpts{Privileged: opts.Privileged})) + t.Run("check agent package version", testAgentPackageVersion(ctx, fixture, true)) + t.Run("check second agent installs with --namespace", testSecondAgentCanInstall(ctx, fixture, basePath, false, opts)) + // Make sure uninstall from within the topPath fails on Windows if runtime.GOOS == "windows" { cwd, err := os.Getwd() @@ -153,6 +159,129 @@ func TestInstallWithBasePath(t *testing.T) { } } +func TestInstallPrivilegedWithoutBasePath(t *testing.T) { + define.Require(t, define.Requirements{ + Group: 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, + }) + + // 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 + require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, &installtest.CheckOpts{Privileged: opts.Privileged})) + + t.Run("check agent package version", testAgentPackageVersion(ctx, fixture, true)) + t.Run("check second agent installs with --namespace", testSecondAgentCanInstall(ctx, fixture, "", false, opts)) +} + +func TestInstallPrivilegedWithBasePath(t *testing.T) { + define.Require(t, define.Requirements{ + Group: 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, + }) + + // 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) + + // Set up random temporary directory to serve as base path for Elastic Agent + // installation. + tmpDir := t.TempDir() + randomBasePath := filepath.Join(tmpDir, strings.ToLower(randStr(8))) + + // Run `elastic-agent install`. We use `--force` to prevent interactive + // execution. + opts := atesting.InstallOpts{ + BasePath: randomBasePath, + 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 the custom base path + topPath := filepath.Join(randomBasePath, "Elastic", "Agent") + require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, &installtest.CheckOpts{Privileged: opts.Privileged})) + t.Run("check agent package version", testAgentPackageVersion(ctx, fixture, true)) + t.Run("check second agent installs with --develop", testSecondAgentCanInstall(ctx, fixture, randomBasePath, true, opts)) +} + +// Tests that a second agent can be installed in an isolated namespace, using either --develop or --namespace. +func testSecondAgentCanInstall(ctx context.Context, fixture *atesting.Fixture, basePath string, develop bool, installOpts atesting.InstallOpts) func(*testing.T) { + return func(t *testing.T) { + // Get path to Elastic Agent executable + devFixture, err := define.NewFixtureFromLocalBuild(t, define.Version()) + require.NoError(t, err) + + // Prepare the Elastic Agent so the binary is extracted and ready to use. + err = devFixture.Prepare(ctx) + require.NoError(t, err) + + // If development mode was requested, the namespace will be automatically set to Development after Install(). + // Otherwise, install into a test namespace. + installOpts.Develop = develop + if !installOpts.Develop { + installOpts.Namespace = "Testing" + } + + devOut, err := devFixture.Install(ctx, &installOpts) + if err != nil { + t.Logf("install output: %s", devOut) + require.NoError(t, err) + } + + topPath := installtest.NamespaceTopPath(installOpts.Namespace) + if basePath != "" { + topPath = filepath.Join(basePath, "Elastic", paths.InstallDirNameForNamespace(installOpts.Namespace)) + } + + require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, &installtest.CheckOpts{ + Privileged: installOpts.Privileged, + Namespace: installOpts.Namespace, + })) + } +} + // TestRepeatedInstallUninstall will install then uninstall the agent // repeatedly. This test exists because of a number of race // conditions that have occurred in the uninstall process. Current @@ -196,7 +325,7 @@ func TestRepeatedInstallUninstall(t *testing.T) { } // Check that Agent was installed in default base path - require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, !opts.Privileged)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, &installtest.CheckOpts{Privileged: opts.Privileged})) t.Run("check agent package version", testAgentPackageVersion(ctx, fixture, true)) out, err = fixture.Uninstall(ctx, &atesting.UninstallOpts{Force: true}) require.NoErrorf(t, err, "uninstall failed: %s", err) diff --git a/testing/integration/logs_ingestion_test.go b/testing/integration/logs_ingestion_test.go index 294863d44ce..718581d2035 100644 --- a/testing/integration/logs_ingestion_test.go +++ b/testing/integration/logs_ingestion_test.go @@ -99,7 +99,7 @@ func TestLogIngestionFleetManaged(t *testing.T) { check.ConnectedToFleet(ctx, t, agentFixture, 5*time.Minute) // 3. Ensure installation is correct. - require.NoError(t, installtest.CheckSuccess(ctx, agentFixture, installOpts.BasePath, !installOpts.Privileged)) + require.NoError(t, installtest.CheckSuccess(ctx, agentFixture, installOpts.BasePath, &installtest.CheckOpts{Privileged: installOpts.Privileged})) t.Run("Monitoring logs are shipped", func(t *testing.T) { testMonitoringLogsAreShipped(t, ctx, info, agentFixture, policy) diff --git a/testing/integration/switch_privileged_test.go b/testing/integration/switch_privileged_test.go index 9a5acb73965..e11c3a1438c 100644 --- a/testing/integration/switch_privileged_test.go +++ b/testing/integration/switch_privileged_test.go @@ -55,7 +55,7 @@ func TestSwitchPrivilegedWithoutBasePath(t *testing.T) { } // Check that Agent was installed in default base path in unprivileged mode - require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, true)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, &installtest.CheckOpts{Privileged: false})) // Switch to privileged mode out, err = fixture.Exec(ctx, []string{"privileged", "-f"}) @@ -65,7 +65,7 @@ func TestSwitchPrivilegedWithoutBasePath(t *testing.T) { } // Check that Agent is running in default base path in privileged mode - require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, false)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, &installtest.CheckOpts{Privileged: true})) } func TestSwitchPrivilegedWithBasePath(t *testing.T) { @@ -125,7 +125,7 @@ func TestSwitchPrivilegedWithBasePath(t *testing.T) { // Check that Agent was installed in the custom base path in unprivileged mode topPath := filepath.Join(basePath, "Elastic", "Agent") - require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, true)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, &installtest.CheckOpts{Privileged: false})) // Switch to privileged mode out, err = fixture.Exec(ctx, []string{"privileged", "-f"}) @@ -135,5 +135,5 @@ func TestSwitchPrivilegedWithBasePath(t *testing.T) { } // Check that Agent is running in the custom base path in privileged mode - require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, false)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, &installtest.CheckOpts{Privileged: true})) } diff --git a/testing/integration/switch_unprivileged_test.go b/testing/integration/switch_unprivileged_test.go index 9429a59613b..ea2fdcc060f 100644 --- a/testing/integration/switch_unprivileged_test.go +++ b/testing/integration/switch_unprivileged_test.go @@ -55,7 +55,7 @@ func TestSwitchUnprivilegedWithoutBasePath(t *testing.T) { } // Check that Agent was installed in default base path in privileged mode - require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, false)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, &installtest.CheckOpts{Privileged: true})) // Switch to unprivileged mode out, err = fixture.Exec(ctx, []string{"unprivileged", "-f"}) @@ -65,7 +65,7 @@ func TestSwitchUnprivilegedWithoutBasePath(t *testing.T) { } // Check that Agent is running in default base path in unprivileged mode - require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, true)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, opts.BasePath, &installtest.CheckOpts{Privileged: false})) } func TestSwitchUnprivilegedWithBasePath(t *testing.T) { @@ -125,7 +125,7 @@ func TestSwitchUnprivilegedWithBasePath(t *testing.T) { // Check that Agent was installed in the custom base path in privileged mode topPath := filepath.Join(basePath, "Elastic", "Agent") - require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, false)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, &installtest.CheckOpts{Privileged: true})) // Switch to unprivileged mode out, err = fixture.Exec(ctx, []string{"unprivileged", "-f"}) @@ -135,5 +135,5 @@ func TestSwitchUnprivilegedWithBasePath(t *testing.T) { } // Check that Agent is running in the custom base path in unprivileged mode - require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, true)) + require.NoError(t, installtest.CheckSuccess(ctx, fixture, topPath, &installtest.CheckOpts{Privileged: false})) } diff --git a/testing/upgradetest/upgrader.go b/testing/upgradetest/upgrader.go index 12996ebd7fa..b8e8f70feb3 100644 --- a/testing/upgradetest/upgrader.go +++ b/testing/upgradetest/upgrader.go @@ -281,7 +281,7 @@ func PerformUpgrade( // validate installation is correct if InstallChecksAllowed(!installOpts.Privileged, startVersion) { - err = installtest.CheckSuccess(ctx, startFixture, installOpts.BasePath, !installOpts.Privileged) + err = installtest.CheckSuccess(ctx, startFixture, installOpts.BasePath, &installtest.CheckOpts{Privileged: installOpts.Privileged}) if err != nil { return fmt.Errorf("pre-upgrade installation checks failed: %w", err) } @@ -412,7 +412,7 @@ func PerformUpgrade( // validate again that the installation is correct, upgrade should not have changed installation validation if InstallChecksAllowed(!installOpts.Privileged, startVersion, endVersion) { - err = installtest.CheckSuccess(ctx, startFixture, installOpts.BasePath, !installOpts.Privileged) + err = installtest.CheckSuccess(ctx, startFixture, installOpts.BasePath, &installtest.CheckOpts{Privileged: installOpts.Privileged}) if err != nil { return fmt.Errorf("post-upgrade installation checks failed: %w", err) }