Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow installing a second agent on the same machine for development #4822

Merged
merged 54 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b48f695
Define development mode, alter install path.
cmacknz May 27, 2024
ceb17e2
Change service name in development mode.
cmacknz May 27, 2024
4b2f1a7
Make shell wrapper path conditional on development mode.
cmacknz May 27, 2024
7ef8e1b
Make control socket run path account for dev mode.
cmacknz May 27, 2024
7d6f153
Development mode automatically binds to port 0.
cmacknz May 27, 2024
06c1335
Refactor dev mode into separate file.
cmacknz May 28, 2024
8e1c177
Shorten command to --develop.
cmacknz May 28, 2024
f20c909
Fix windows build error.
cmacknz May 28, 2024
39bb905
Fix using wrong config option in install.
cmacknz May 28, 2024
16ba140
Add run --develop command.
cmacknz May 29, 2024
1f3ee13
Add initial test for --develop.
cmacknz May 29, 2024
1e38915
Initial version of an integration test for --develop mode.
cmacknz May 29, 2024
347dc65
Wait on the watcher instead of just releasing it.
cmacknz May 30, 2024
959725a
Add --develop test with base path.
cmacknz May 30, 2024
f8b1331
Add privileged install tests with --develop
cmacknz May 30, 2024
ea3999d
Move install tests to the same file.
cmacknz May 30, 2024
3897f40
Refactor develop test into function.
cmacknz May 30, 2024
d373536
Invert condition to match installopts
cmacknz May 30, 2024
b780b9b
Automatically add development tag on enroll.
cmacknz May 30, 2024
2dbd0a4
Change shell wrapper path to development.
cmacknz May 30, 2024
7546d09
Add documentation for --develop.
cmacknz May 31, 2024
3b0d997
Use lowercase for consistency.
cmacknz May 31, 2024
ce929fb
Remove TODO comments.
cmacknz May 31, 2024
03fe19c
Fix README typos.
cmacknz May 31, 2024
3c1ed11
Adjust comments.
cmacknz May 31, 2024
6e36b1a
More typo fixes.
cmacknz May 31, 2024
58afd75
Fix description not to mention beats.
cmacknz Jun 3, 2024
b6bd76a
Change windows service name to avoid collision.
cmacknz Jun 3, 2024
7b8e541
Make service display name unique on Windows.
cmacknz Jun 3, 2024
428e439
Merge branch 'main' into second-agent-same-machine
cmacknz Jun 7, 2024
724d5da
Merge branch 'main' into second-agent-same-machine
cmacknz Jun 10, 2024
18e7e5e
Add concept of an installation namespace.
cmacknz Jun 10, 2024
d340e19
Add nolint directives.
cmacknz Jun 10, 2024
3696791
Switch from strings.Replace to fmt.Sprintf.
cmacknz Jun 11, 2024
8d36a0a
Allow empty nolint directives.
cmacknz Jun 11, 2024
891a9e9
Enforce agent prefix. Add whitespace tests.
cmacknz Jun 11, 2024
ad85971
Fix typo.
cmacknz Jun 11, 2024
fc8e7dd
Remove unnecessary conditional check.
cmacknz Jun 11, 2024
2d38aec
Read the namespace once at startup.
cmacknz Jun 11, 2024
6e98115
Add missing license header.
cmacknz Jun 11, 2024
35bebe8
Also disable staticcheck for Windows lint warnings.
cmacknz Jun 11, 2024
b318abd
Revert "Allow empty nolint directives."
cmacknz Jun 12, 2024
18904f8
Better handling of empty fmt strings on windows.
cmacknz Jun 12, 2024
750983f
Merge branch 'main' into second-agent-same-machine
cmacknz Jun 12, 2024
967aabf
Fix use of hard coded service name.
cmacknz Jun 12, 2024
d5d3e3b
Fix merge errors in integration tests.
cmacknz Jun 12, 2024
5f2eab8
Properly handle empty format string on Windows.
cmacknz Jun 12, 2024
3efe29d
Add Address dropped in merge conflict resolution.
cmacknz Jun 13, 2024
9202c25
Merge branch 'main' into second-agent-same-machine
cmacknz Jun 13, 2024
414ae84
Fix ServiceName uses after merge.
cmacknz Jun 13, 2024
162d430
Merge branch 'main' into second-agent-same-machine
cmacknz Jun 17, 2024
f69ccfe
Add --namespace installation option.
cmacknz Jun 17, 2024
d4efa5f
Use --namespace in integration tests.
cmacknz Jun 17, 2024
051d4a8
Get integration tests to pass.
cmacknz Jun 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 0 additions & 5 deletions internal/pkg/agent/application/paths/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
130 changes: 130 additions & 0 deletions internal/pkg/agent/application/paths/common_namespace.go
Original file line number Diff line number Diff line change
@@ -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)
}
76 changes: 76 additions & 0 deletions internal/pkg/agent/application/paths/common_namespace_test.go
Original file line number Diff line number Diff line change
@@ -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"))
}
17 changes: 10 additions & 7 deletions internal/pkg/agent/application/paths/paths_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 $@
`
)
Expand Down
17 changes: 10 additions & 7 deletions internal/pkg/agent/application/paths/paths_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions internal/pkg/agent/application/paths/paths_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading