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

"install -f" uses exec to uninstall an existing agent #4965

Merged
merged 18 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Kind can be one of:
# - breaking-change: a change to previously-documented behavior
# - deprecation: functionality that is being removed in a later release
# - bug-fix: fixes a problem in a previous version
# - enhancement: extends functionality but does not break or fix existing behavior
# - feature: new functionality
# - known-issue: problems that we are aware of in a given version
# - security: impacts on the security of a product or a user’s deployment.
# - upgrade: important information for someone upgrading from a prior version
# - other: does not fit into any of the other categories
kind: bug

# Change summary; a 80ish characters long description of the change.
summary: Use installed agent to uninstall itself when install -f is used.

# Long description; in case the summary is not enough to describe the change
# this field accommodate a description without length limits.
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
description: |
When using "elastic-agent install -f", the agent will exec "elastic-agent uninstall -f"
using the agent found in the system's path. This ensures all path references are correctly
loaded and tamper protection errors will cause the install attempt to fail.

# Affected component; a word indicating the component this changeset affects.
component:

# PR URL; optional; the PR number that added the changeset.
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
# Please provide it if you are adding a fragment for a different PR.
#pr: https://github.com/owner/repo/1234

# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
# If not present is automatically filled by the tooling with the issue linked to the PR number.
issue: https://github.com/elastic/elastic-agent/issues/4506
57 changes: 53 additions & 4 deletions internal/pkg/agent/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ import (
)

const (
flagInstallBasePath = "base-path"
flagInstallUnprivileged = "unprivileged"
flagInstallDevelopment = "develop"
flagInstallNamespace = "namespace"
flagInstallBasePath = "base-path"
flagInstallUnprivileged = "unprivileged"
flagInstallDevelopment = "develop"
flagInstallNamespace = "namespace"
flagInstallRunUninstallFromBinary = "run-uninstall-from-binary"
)

func newInstallCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command {
Expand All @@ -51,6 +52,9 @@ would like the Agent to operate.
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().Bool(flagInstallRunUninstallFromBinary, false, "Run the uninstall command from this binary instead of using the binary found in the system's path.")
michel-laterman marked this conversation as resolved.
Show resolved Hide resolved
_ = cmd.Flags().MarkHidden(flagInstallRunUninstallFromBinary) // Advanced option to force a new agent to override an existing installation, it may orphan installed components.

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.

Expand Down Expand Up @@ -110,6 +114,11 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
return fmt.Errorf("already installed at: %s", topPath)
}

runUninstallBinary, _ := cmd.Flags().GetBool(flagInstallRunUninstallFromBinary)
if status == install.Installed && force && runUninstallBinary {
fmt.Fprintln(streams.Out, "Uninstall will not be ran from the agent installed in system path, components may persist.")
}

nonInteractive, _ := cmd.Flags().GetBool("non-interactive")
if nonInteractive {
fmt.Fprintln(streams.Out, "Installing in non-interactive mode.")
Expand Down Expand Up @@ -221,6 +230,24 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {

var ownership utils.FileOwner
cfgFile := paths.ConfigFile()
if status == install.Installed {
// Uninstall the agent
progBar.Describe("Uninstalling current Elastic Agent")
if !runUninstallBinary {
err := execUninstall(streams)
if err != nil {
progBar.Describe("Uninstall failed")
return err
}
} else {
err := install.Uninstall(cfgFile, topPath, "", log, progBar)
if err != nil {
progBar.Describe("Uninstall from binary failed")
return err
}
}
progBar.Describe("Successfully uninstalled Elastic Agent")
}
if status != install.PackageInstall {
ownership, err = install.Install(cfgFile, topPath, unprivileged, log, progBar, streams)
if err != nil {
Expand Down Expand Up @@ -300,3 +327,25 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
fmt.Fprint(streams.Out, "\nElastic Agent has been successfully installed.\n")
return nil
}

// execUninstall execs "elastic-agent uninstall --force" from the elastic agent installed on the system (found in PATH)
func execUninstall(streams *cli.IOStreams) error {
args := []string{
"uninstall",
"--force",
}
execPath, err := exec.LookPath(paths.BinaryName)
if err != nil {
return fmt.Errorf("unable to find %s on path: %w", paths.BinaryName, err)
}
uninstall := exec.Command(execPath, args...)
uninstall.Stdout = streams.Out
uninstall.Stderr = streams.Err
if err := uninstall.Start(); err != nil {
return fmt.Errorf("unable to start elastic-agent uninstall: %w", err)
}
if err := uninstall.Wait(); err != nil {
return fmt.Errorf("failed to uninstall elastic-agent: %w", err)
}
return nil
}
20 changes: 0 additions & 20 deletions internal/pkg/agent/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,6 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p
return utils.FileOwner{}, errors.New(err, "failed to discover the source directory for installation", errors.TypeFilesystem)
}

// We only uninstall Agent if it is currently installed.
status, _ := Status(topPath)
if status == Installed {
// Uninstall current installation
//
// There is no uninstall token for "install" command.
// Uninstall will fail on protected agent.
// The protected Agent will need to be uninstalled first before it can be installed.
pt.Describe("Uninstalling current Elastic Agent")
err = Uninstall(cfgFile, topPath, "", log, pt)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously this call to uninstall was failing because paths.Components() would use a value that reflected the location from where elastic-agent install -f was being ran and not the installed agent.
This would result in the uninstall detecting 0 components, and skipping calling uninstall on each of the services, so endpoint uninstall was never called

if err != nil {
pt.Describe("Failed to uninstall current Elastic Agent")
return utils.FileOwner{}, errors.New(
err,
fmt.Sprintf("failed to uninstall Agent at (%s)", filepath.Dir(topPath)),
errors.M("directory", filepath.Dir(topPath)))
}
pt.Describe("Successfully uninstalled current Elastic Agent")
}

var ownership utils.FileOwner
username := ""
groupName := ""
Expand Down
167 changes: 96 additions & 71 deletions testing/integration/endpoint_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,22 +133,10 @@ func TestInstallWithEndpointSecurityAndRemoveEndpointIntegration(t *testing.T) {
}
}

// buildPolicyWithTamperProtection helper function to build the policy request with or without tamper protection
func buildPolicyWithTamperProtection(policy kibana.AgentPolicy, protected bool) kibana.AgentPolicy {
if protected {
policy.AgentFeatures = append(policy.AgentFeatures, map[string]interface{}{
"name": "tamper_protection",
"enabled": true,
})
}
policy.IsProtected = protected
return policy
}

func testInstallAndCLIUninstallWithEndpointSecurity(t *testing.T, info *define.Info, protected bool) {
deadline := time.Now().Add(10 * time.Minute)
ctx, cancel := testcontext.WithDeadline(t, context.Background(), deadline)
defer cancel()
// installSecurityAgent is a helper function to install an elastic-agent in priviliged mode with the force+non-interactve flags.
// the policy the agent is enrolled with can have protection enabled if passed
func installSecurityAgent(ctx context.Context, t *testing.T, info *define.Info, protected bool) (*atesting.Fixture, kibana.PolicyResponse) {
t.Helper()

// Get path to agent executable.
fixture, err := define.NewFixtureFromLocalBuild(t, define.Version())
Expand Down Expand Up @@ -179,6 +167,27 @@ func testInstallAndCLIUninstallWithEndpointSecurity(t *testing.T, info *define.I
policy, err := tools.InstallAgentWithPolicy(ctx, t,
installOpts, fixture, info.KibanaClient, createPolicyReq)
require.NoError(t, err, "failed to install agent with policy")
return fixture, policy
}

// buildPolicyWithTamperProtection helper function to build the policy request with or without tamper protection
func buildPolicyWithTamperProtection(policy kibana.AgentPolicy, protected bool) kibana.AgentPolicy {
if protected {
policy.AgentFeatures = append(policy.AgentFeatures, map[string]interface{}{
"name": "tamper_protection",
"enabled": true,
})
}
policy.IsProtected = protected
return policy
}

func testInstallAndCLIUninstallWithEndpointSecurity(t *testing.T, info *define.Info, protected bool) {
deadline := time.Now().Add(10 * time.Minute)
ctx, cancel := testcontext.WithDeadline(t, context.Background(), deadline)
defer cancel()

fixture, policy := installSecurityAgent(ctx, t, info, protected)

t.Cleanup(func() {
t.Log("Un-enrolling Elastic Agent...")
Expand Down Expand Up @@ -210,39 +219,13 @@ func testInstallAndCLIUninstallWithEndpointSecurity(t *testing.T, info *define.I
}

func testInstallAndUnenrollWithEndpointSecurity(t *testing.T, info *define.Info, protected bool) {
// Get path to agent executable.
fixture, err := define.NewFixtureFromLocalBuild(t, define.Version())
require.NoError(t, err)

t.Log("Enrolling the agent in Fleet")
policyUUID := uuid.New().String()
createPolicyReq := buildPolicyWithTamperProtection(
kibana.AgentPolicy{
Name: "test-policy-" + policyUUID,
Namespace: "default",
Description: "Test policy " + policyUUID,
MonitoringEnabled: []kibana.MonitoringEnabledOption{
kibana.MonitoringEnabledLogs,
kibana.MonitoringEnabledMetrics,
},
},
protected,
)

installOpts := atesting.InstallOpts{
NonInteractive: true,
Force: true,
Privileged: true,
}

ctx, cn := testcontext.WithDeadline(t, context.Background(), time.Now().Add(10*time.Minute))
defer cn()

policy, err := tools.InstallAgentWithPolicy(ctx, t, installOpts, fixture, info.KibanaClient, createPolicyReq)
require.NoError(t, err)
fixture, policy := installSecurityAgent(ctx, t, info, protected)

t.Log("Installing Elastic Defend")
_, err = installElasticDefendPackage(t, info, policy.ID)
_, err := installElasticDefendPackage(t, info, policy.ID)
require.NoError(t, err)

t.Log("Polling for endpoint-security to become Healthy")
Expand Down Expand Up @@ -323,36 +306,10 @@ func testInstallAndUnenrollWithEndpointSecurity(t *testing.T, info *define.Info,
}

func testInstallWithEndpointSecurityAndRemoveEndpointIntegration(t *testing.T, info *define.Info, protected bool) {
// Get path to agent executable.
fixture, err := define.NewFixtureFromLocalBuild(t, define.Version())
require.NoError(t, err)

t.Log("Enrolling the agent in Fleet")
policyUUID := uuid.New().String()
createPolicyReq := buildPolicyWithTamperProtection(
kibana.AgentPolicy{
Name: "test-policy-" + policyUUID,
Namespace: "default",
Description: "Test policy " + policyUUID,
MonitoringEnabled: []kibana.MonitoringEnabledOption{
kibana.MonitoringEnabledLogs,
kibana.MonitoringEnabledMetrics,
},
},
protected,
)

installOpts := atesting.InstallOpts{
NonInteractive: true,
Force: true,
Privileged: true,
}

ctx, cn := testcontext.WithDeadline(t, context.Background(), time.Now().Add(10*time.Minute))
defer cn()

policy, err := tools.InstallAgentWithPolicy(ctx, t, installOpts, fixture, info.KibanaClient, createPolicyReq)
require.NoError(t, err)
fixture, policy := installSecurityAgent(ctx, t, info, protected)

t.Log("Installing Elastic Defend")
pkgPolicyResp, err := installElasticDefendPackage(t, info, policy.ID)
Expand Down Expand Up @@ -874,3 +831,71 @@ func agentIsHealthyNoEndpoint(t *testing.T, ctx context.Context, agentClient cli

return true
}

// TestForceInstallOverProtectedPolicy tests that running `elastic-agent install -f`
// when an installed agent is running a policy with tamper protection enabled fails.
func TestForceInstallOverProtectedPolicy(t *testing.T) {
info := define.Require(t, define.Requirements{
Group: Fleet,
Stack: &define.Stack{},
Local: false, // requires Agent installation
Sudo: true, // requires Agent installation
OS: []define.OS{
{Type: define.Linux},
},
})

deadline := time.Now().Add(10 * time.Minute)
ctx, cancel := testcontext.WithDeadline(t, context.Background(), deadline)
defer cancel()

fixture, policy := installSecurityAgent(ctx, t, info, true)

t.Cleanup(func() {
t.Log("Un-enrolling Elastic Agent...")
// Use a separate context as the one in the test body will have been cancelled at this point.
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), time.Minute)
defer cleanupCancel()
assert.NoError(t, fleettools.UnEnrollAgent(cleanupCtx, info.KibanaClient, policy.ID))
})

t.Log("Installing Elastic Defend")
pkgPolicyResp, err := installElasticDefendPackage(t, info, policy.ID)
require.NoErrorf(t, err, "Policy Response was: %v", pkgPolicyResp)

t.Log("Polling for endpoint-security to become Healthy")
ctx, cancel = context.WithTimeout(ctx, endpointHealthPollingTimeout)
defer cancel()

agentClient := fixture.Client()
err = agentClient.Connect(ctx)
require.NoError(t, err, "could not connect to local agent")

require.Eventually(t,
func() bool { return agentAndEndpointAreHealthy(t, ctx, agentClient) },
endpointHealthPollingTimeout,
time.Second,
"Endpoint component or units are not healthy.",
)
t.Log("Verified endpoint component and units are healthy")

t.Log("Run elastic-agent install -f...")
// We use the same policy with tamper protection enabled for this test and expect it to fail.
token, err := info.KibanaClient.CreateEnrollmentAPIKey(ctx, kibana.CreateEnrollmentAPIKeyRequest{
PolicyID: policy.ID,
})
require.NoError(t, err)
url, err := fleettools.DefaultURL(ctx, info.KibanaClient)
require.NoError(t, err)

args := []string{
"install",
"--force",
"--url",
url,
"--enrollment-token",
token.APIKey,
}
out, err := fixture.Exec(ctx, args)
require.Errorf(t, err, "No error detected, command output: %s", out)
}