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

Add unprivileged and privileged subcommand to Elastic Agent #4621

Merged
merged 29 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ee26696
Work on privileged/unprivileged command.
blakerouse Apr 25, 2024
df0160a
Add integration tests for switching between unprivileged and privileg…
blakerouse Apr 30, 2024
e4946e6
Merge branch 'main' into to-unprivileged
blakerouse Apr 30, 2024
a709d64
Merge branch 'main' into to-unprivileged
blakerouse May 9, 2024
71e6c0b
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse May 9, 2024
2f83735
Fix upstream rename.
blakerouse May 9, 2024
bfbf202
Add changelog.
blakerouse May 9, 2024
d1b8f4f
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse May 9, 2024
217632b
Merge branch 'main' into to-unprivileged
blakerouse May 15, 2024
a97f017
Merge branch 'main' into to-unprivileged
blakerouse May 16, 2024
9d2dc72
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse May 20, 2024
0986097
Switch to new install privileged/unprivileged checks.
blakerouse May 20, 2024
2298339
Adjust FixPermissions to take ownership back to Administrators.
blakerouse May 21, 2024
fc516b7
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse May 21, 2024
b25e9bf
Code review feedback.
blakerouse May 22, 2024
ebafe9e
Fix service component check. Support switching on macOS.
blakerouse May 22, 2024
d9a87eb
Fix lint.
blakerouse May 22, 2024
6e90411
Update to constant.
blakerouse May 22, 2024
77a077b
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse May 22, 2024
29b9f17
Add tests for unprivileged switch failure with endpoint installed.
blakerouse May 24, 2024
80fe23e
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse May 24, 2024
9387967
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse Jun 7, 2024
bd44589
Fix runtime check to keep runtime spec.
blakerouse Jun 10, 2024
9f33200
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse Jun 10, 2024
be73951
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse Jun 10, 2024
8434e72
Fix test contains.
blakerouse Jun 10, 2024
185b505
Merge remote-tracking branch 'upstream/main' into to-unprivileged
blakerouse Jun 11, 2024
9f915ec
Only run test on linux.
blakerouse Jun 11, 2024
53ba631
Linux only, more.
blakerouse Jun 11, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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: feature

# Change summary; a 80ish characters long description of the change.
summary: Add unprivileged and privileged switch commands

# 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: |
Adds ability to switch between privileged and unprivileged mode using the privileged and unprivileged
subcommands respectively.

# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
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:/elastic/elastic-agent/pull/4621

# 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:/elastic/ingest-dev/issues/2790
2 changes: 2 additions & 0 deletions internal/pkg/agent/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command {
cmd.AddCommand(newUpgradeCommandWithArgs(args, streams))
cmd.AddCommand(newEnrollCommandWithArgs(args, streams))
cmd.AddCommand(newInspectCommandWithArgs(args, streams))
cmd.AddCommand(newPrivilegedCommandWithArgs(args, streams))
cmd.AddCommand(newUnprivilegedCommandWithArgs(args, streams))
cmd.AddCommand(newWatchCommandWithArgs(args, streams))
cmd.AddCommand(newContainerCommand(args, streams))
cmd.AddCommand(newStatusCommand(args, streams))
Expand Down
51 changes: 2 additions & 49 deletions internal/pkg/agent/cmd/enroll_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/elastic/elastic-agent/internal/pkg/release"
"github.com/elastic/elastic-agent/internal/pkg/remote"
"github.com/elastic/elastic-agent/pkg/control/v2/client"
"github.com/elastic/elastic-agent/pkg/control/v2/client/wait"
"github.com/elastic/elastic-agent/pkg/core/logger"
"github.com/elastic/elastic-agent/pkg/core/process"
"github.com/elastic/elastic-agent/pkg/utils"
Expand Down Expand Up @@ -324,7 +325,7 @@ func (c *enrollCmd) fleetServerBootstrap(ctx context.Context, persistentConfig m
if err != nil {
if !c.options.FleetServer.SpawnAgent {
// wait longer to try and communicate with the Elastic Agent
err = waitForAgent(ctx, c.options.DaemonTimeout)
err = wait.ForAgent(ctx, c.options.DaemonTimeout)
if err != nil {
return "", errors.New("failed to communicate with elastic-agent daemon; is elastic-agent running?")
}
Expand Down Expand Up @@ -711,54 +712,6 @@ type waitResult struct {
err error
}

func waitForAgent(ctx context.Context, timeout time.Duration) error {
if timeout == 0 {
timeout = 1 * time.Minute
}
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
maxBackoff := timeout
if maxBackoff <= 0 {
// indefinite timeout
maxBackoff = 10 * time.Minute
}

resChan := make(chan waitResult)
innerCtx, innerCancel := context.WithCancel(context.Background())
defer innerCancel()
go func() {
backOff := expBackoffWithContext(innerCtx, 1*time.Second, maxBackoff)
for {
backOff.Wait()
_, err := getDaemonState(innerCtx)
if errors.Is(err, context.Canceled) {
resChan <- waitResult{err: err}
return
}
if err == nil {
resChan <- waitResult{}
break
}
}
}()

var res waitResult
select {
case <-ctx.Done():
innerCancel()
res = <-resChan
case res = <-resChan:
}

if res.err != nil {
return res.err
}
return nil
}

func waitForFleetServer(ctx context.Context, agentSubproc <-chan *os.ProcessState, log *logger.Logger, timeout time.Duration) (string, error) {
if timeout == 0 {
timeout = 2 * time.Minute
Expand Down
76 changes: 43 additions & 33 deletions internal/pkg/agent/cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,43 +252,12 @@ func inspectComponents(ctx context.Context, cfgPath string, opts inspectComponen
return err
}

// Load the requirements before trying to load the configuration. These should always load
// even if the configuration is wrong.
platform, err := component.LoadPlatformDetail()
if err != nil {
return fmt.Errorf("failed to gather system information: %w", err)
}
specs, err := component.LoadRuntimeSpecs(paths.Components(), platform)
if err != nil {
return fmt.Errorf("failed to detect inputs and outputs: %w", err)
}

isAdmin, err := utils.HasRoot()
if err != nil {
return fmt.Errorf("error checking for root/Administrator privileges: %w", err)
}

m, lvl, err := getConfigWithVariables(ctx, l, cfgPath, opts.variablesWait, !isAdmin)
comps, err := getComponentsFromPolicy(ctx, l, cfgPath, opts.variablesWait)
if err != nil {
// error already includes the context
return err
}

monitorFn, err := getMonitoringFn(ctx, m)
if err != nil {
return fmt.Errorf("failed to get monitoring: %w", err)
}

agentInfo, err := info.NewAgentInfoWithLog(ctx, "error", false)
if err != nil {
return fmt.Errorf("could not load agent info: %w", err)
}

// Compute the components from the computed configuration.
comps, err := specs.ToComponents(m, monitorFn, lvl, agentInfo)
if err != nil {
return fmt.Errorf("failed to render components: %w", err)
}

// Hide configuration unless toggled on.
if !opts.showConfig {
for i, comp := range comps {
Expand Down Expand Up @@ -349,6 +318,47 @@ func inspectComponents(ctx context.Context, cfgPath string, opts inspectComponen
return printComponents(allowed, blocked, streams)
}

func getComponentsFromPolicy(ctx context.Context, l *logger.Logger, cfgPath string, variablesWait time.Duration) ([]component.Component, error) {
// Load the requirements before trying to load the configuration. These should always load
// even if the configuration is wrong.
platform, err := component.LoadPlatformDetail()
if err != nil {
return nil, fmt.Errorf("failed to gather system information: %w", err)
}
specs, err := component.LoadRuntimeSpecs(paths.Components(), platform)
if err != nil {
return nil, fmt.Errorf("failed to detect inputs and outputs: %w", err)
}

isAdmin, err := utils.HasRoot()
if err != nil {
return nil, fmt.Errorf("error checking for root/Administrator privileges: %w", err)
}

m, lvl, err := getConfigWithVariables(ctx, l, cfgPath, variablesWait, !isAdmin)
if err != nil {
return nil, err
}

monitorFn, err := getMonitoringFn(ctx, m)
if err != nil {
return nil, fmt.Errorf("failed to get monitoring: %w", err)
}

agentInfo, err := info.NewAgentInfoWithLog(ctx, "error", false)
if err != nil {
return nil, fmt.Errorf("could not load agent info: %w", err)
}

// Compute the components from the computed configuration.
comps, err := specs.ToComponents(m, monitorFn, lvl, agentInfo)
if err != nil {
return nil, fmt.Errorf("failed to render components: %w", err)
}

return comps, nil
}

func getMonitoringFn(ctx context.Context, cfg map[string]interface{}) (component.GenerateMonitoringCfgFn, error) {
config, err := config.NewConfigFrom(cfg)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion internal/pkg/agent/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"time"

"github.com/spf13/cobra"

Expand Down Expand Up @@ -230,7 +231,7 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
defer func() {
if err != nil {
progBar.Describe("Stopping Service")
innerErr := install.StopService(topPath)
innerErr := install.StopService(topPath, 30*time.Second, 250*time.Millisecond)
blakerouse marked this conversation as resolved.
Show resolved Hide resolved
if innerErr != nil {
progBar.Describe("Failed to Stop Service")
} else {
Expand Down
98 changes: 98 additions & 0 deletions internal/pkg/agent/cmd/privileged.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package cmd

import (
"context"
"errors"
"fmt"
"os"
"runtime"

"github.com/spf13/cobra"

"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
"github.com/elastic/elastic-agent/internal/pkg/agent/install"
"github.com/elastic/elastic-agent/internal/pkg/cli"
"github.com/elastic/elastic-agent/pkg/control/v2/client/wait"
"github.com/elastic/elastic-agent/pkg/utils"
)

func newPrivilegedCommandWithArgs(s []string, streams *cli.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "privileged",
Short: "Switch installed Elastic Agent to run as privileged",
Long: `This command converts the installed Elastic Agent from running unprivileged to running as privileged.

By default this command will ask or a confirmation before making this change. You can bypass the confirmation request
blakerouse marked this conversation as resolved.
Show resolved Hide resolved
using the -f flag. This will always stop the running Elastic Agent (if running) before performing the switch it is
blakerouse marked this conversation as resolved.
Show resolved Hide resolved
possible that loss of metrics could occur during this small window of time. The command also always starts the
Copy link
Member

Choose a reason for hiding this comment

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

possible that loss of metrics could occur during this small window of time

Loss of metrics or data in general, if this is intended to be a warning that this is not a zero downtime operation you could just say that directly.

Copy link
Member

Choose a reason for hiding this comment

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

For both commands you may want to caution about what happens if their execution is interrupted.

Elastic Agent at the end (even if it was off to start). In the case that the Elastic Agent is already running
privileged it will still perform all the same work, including stopping and starting the Elastic Agent.
`,
Args: cobra.ExactArgs(0),
Run: func(c *cobra.Command, args []string) {
if err := privilegedCmd(streams, c); err != nil {
fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage())
os.Exit(1)
}
},
}

cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation")
cmd.Flags().DurationP("daemon-timeout", "", 0, "Timeout waiting for Elastic Agent daemon")
blakerouse marked this conversation as resolved.
Show resolved Hide resolved

return cmd
}

func privilegedCmd(streams *cli.IOStreams, cmd *cobra.Command) (err error) {
isAdmin, err := utils.HasRoot()
if err != nil {
return fmt.Errorf("unable to perform privileged command while checking for root/Administrator rights: %w", err)
}
if !isAdmin {
return fmt.Errorf("unable to perform privileged command, not executed with %s permissions", utils.PermissionUser)
}
blakerouse marked this conversation as resolved.
Show resolved Hide resolved

// TODO(blakerouse): More work to get this working on macOS.
// Need to switch the vault from keystore based to file based vault.
if runtime.GOOS == "darwin" {
return errors.New("unable to perform unprivileged on macOS (not supported)")
}

topPath := paths.Top()
daemonTimeout, _ := cmd.Flags().GetDuration("daemon-timeout")
force, _ := cmd.Flags().GetBool("force")
if !force {
confirm, err := cli.Confirm("This will restart the running Elastic Agent and convert it to run in privileged mode. Do you want to continue?", true)
if err != nil {
return fmt.Errorf("problem reading prompt response")
}
if !confirm {
return fmt.Errorf("unprivileged switch was cancelled by the user")
}
}

pt := install.CreateAndStartNewSpinner(streams.Out, "Converting Elastic Agent to privileged...")
err = install.SwitchExecutingMode(topPath, pt, "", "")
if err != nil {
// error already adds context
return err
}

// wait for the service
if daemonTimeout >= 0 {
blakerouse marked this conversation as resolved.
Show resolved Hide resolved
pt.Describe("Waiting for running service")
ctx := handleSignal(context.Background()) // allowed to be cancelled
err = wait.ForAgent(ctx, daemonTimeout)
if err != nil && !errors.Is(err, context.Canceled) {
pt.Describe("Failed waiting for running service")
return err
}
blakerouse marked this conversation as resolved.
Show resolved Hide resolved
pt.Describe("Service is up and running")
}

return nil
}
Loading
Loading