Skip to content
Closed
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
2 changes: 1 addition & 1 deletion internal/pkg/agent/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func New(

// monitoring is not supported in bootstrap mode https://github.com/elastic/elastic-agent/issues/1761
isMonitoringSupported := !disableMonitoring && cfg.Settings.V1MonitoringEnabled
upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, agentInfo)
upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, cfg.Settings.Upgrade, agentInfo)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to create upgrader: %w", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/artifact"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
"github.com/elastic/elastic-agent/internal/pkg/agent/configuration"
"github.com/elastic/elastic-agent/internal/pkg/agent/transpiler"
"github.com/elastic/elastic-agent/internal/pkg/config"
monitoringCfg "github.com/elastic/elastic-agent/internal/pkg/core/monitoring/config"
Expand Down Expand Up @@ -446,6 +447,7 @@ func TestCoordinatorReportsInvalidPolicy(t *testing.T) {
upgradeMgr, err := upgrade.NewUpgrader(
log,
&artifact.Config{},
&configuration.UpgradeConfig{},
&info.AgentInfo{},
)
require.NoError(t, err, "errored when creating a new upgrader")
Expand Down
29 changes: 27 additions & 2 deletions internal/pkg/agent/application/upgrade/rollback.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"github.com/elastic/elastic-agent/pkg/control/v2/client"
"github.com/elastic/elastic-agent/pkg/core/logger"
"github.com/elastic/elastic-agent/pkg/utils"
"github.com/elastic/elastic-agent/pkg/version"
)

const (
Expand All @@ -33,6 +34,9 @@
restartBackoffMax = 90 * time.Second
)

// Rollback window feature is only available starting with version >= 9.1.0-SNAPSHOT.
var rollbackWindowMinVersion = version.NewParsedSemVer(9, 1, 0, "SNAPSHOT", "")

// Rollback rollbacks to previous version which was functioning before upgrade.
func Rollback(ctx context.Context, log *logger.Logger, c client.Client, topDirPath, prevVersionedHome, prevHash string) error {
symlinkPath := filepath.Join(topDirPath, agentName)
Expand Down Expand Up @@ -144,13 +148,13 @@

// InvokeWatcher invokes an agent instance using watcher argument for watching behavior of
// agent during upgrade period.
func InvokeWatcher(log *logger.Logger, agentExecutable string) (*exec.Cmd, error) {
func InvokeWatcher(log *logger.Logger, agentExecutable string, rollbackWindow time.Duration) (*exec.Cmd, error) {
if !IsUpgradeable() {
log.Info("agent is not upgradable, not starting watcher")
return nil, nil
}

cmd := invokeCmd(agentExecutable)
cmd := makeOSWatchCmd(makeBaseWatchCmd(agentExecutable, rollbackWindow))
log.Infow("Starting upgrade watcher", "path", cmd.Path, "args", cmd.Args, "env", cmd.Env, "dir", cmd.Dir)
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start Upgrade Watcher: %w", err)
Expand Down Expand Up @@ -238,3 +242,24 @@
close(signal)
return nil
}

func makeBaseWatchCmd(agentExecutable string, rollbackWindow time.Duration) *exec.Cmd {
cmdArgs := []string{
watcherSubcommand,
"--path.config", paths.Config(),
"--path.home", paths.Top(),
}

if rollbackWindow > 0 {
cmdArgs = append(cmdArgs, "--rollback-window", fmt.Sprintf("%s", rollbackWindow.String()))

Check failure on line 254 in internal/pkg/agent/application/upgrade/rollback.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

S1025: the argument is already a string, there's no need to use fmt.Sprintf (staticcheck)
}

// #nosec G204 -- user cannot inject any parameters to this command
return exec.Command(agentExecutable, cmdArgs...)
}

// isRollbackWindowSupported checks if the rollback window feature is supported by the version of
// the target Agent, i.e the version being upgraded *to*.
func isRollbackWindowSupported(targetAgentVersion *version.ParsedSemVer) bool {
return !targetAgentVersion.Less(*rollbackWindowMinVersion)
}
14 changes: 3 additions & 11 deletions internal/pkg/agent/application/upgrade/rollback_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import (
"os/exec"
"syscall"
"time"

"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
)

const (
Expand All @@ -21,13 +19,7 @@ const (
afterRestartDelay = 2 * time.Second
)

func invokeCmd(agentExecutable string) *exec.Cmd {
// #nosec G204 -- user cannot inject any parameters to this command
cmd := exec.Command(agentExecutable, watcherSubcommand,
"--path.config", paths.Config(),
"--path.home", paths.Top(),
)

func makeOSWatchCmd(baseWatchCmd *exec.Cmd) *exec.Cmd {
var cred = &syscall.Credential{
Uid: uint32(os.Getuid()),
Gid: uint32(os.Getgid()),
Expand All @@ -38,6 +30,6 @@ func invokeCmd(agentExecutable string) *exec.Cmd {
Credential: cred,
Setsid: true,
}
cmd.SysProcAttr = sysproc
return cmd
baseWatchCmd.SysProcAttr = sysproc
return baseWatchCmd
}
14 changes: 3 additions & 11 deletions internal/pkg/agent/application/upgrade/rollback_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
"os/exec"
"syscall"
"time"

"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
)

const (
Expand All @@ -21,16 +19,10 @@
afterRestartDelay = 2 * time.Second
)

func invokeCmd(agentExecutable string) *exec.Cmd {
// #nosec G204 -- user cannot inject any parameters to this command
cmd := exec.Command(agentExecutable, watcherSubcommand,
"--path.config", paths.Config(),
"--path.home", paths.Top(),
)

func makeOSWatchCmd(baseWatchCmd *exec.Cmd) *exec.Cmd {
var cred = &syscall.Credential{
Uid: uint32(os.Getuid()),

Check failure on line 24 in internal/pkg/agent/application/upgrade/rollback_linux.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

G115: integer overflow conversion int -> uint32 (gosec)
Gid: uint32(os.Getgid()),

Check failure on line 25 in internal/pkg/agent/application/upgrade/rollback_linux.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

G115: integer overflow conversion int -> uint32 (gosec)
Groups: nil,
NoSetGroups: true,
}
Expand All @@ -40,6 +32,6 @@
// propagate sigint instead of sigkill so we can ignore it
Pdeathsig: syscall.SIGINT,
}
cmd.SysProcAttr = sysproc
return cmd
baseWatchCmd.SysProcAttr = sysproc
return baseWatchCmd
}
74 changes: 74 additions & 0 deletions internal/pkg/agent/application/upgrade/rollback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"path/filepath"
"runtime"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
Expand All @@ -19,6 +20,7 @@ import (
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
"github.com/elastic/elastic-agent/pkg/core/logger"
"github.com/elastic/elastic-agent/pkg/core/logger/loggertest"
"github.com/elastic/elastic-agent/pkg/version"
mocks "github.com/elastic/elastic-agent/testing/mocks/pkg/control/v2/client"
)

Expand Down Expand Up @@ -328,6 +330,78 @@ func TestRollback(t *testing.T) {
}
}

func TestIsRollbackWindowSupported(t *testing.T) {
tests := map[string]struct {
version string
want bool
}{
"supported_version": {
version: "9.2.0-SNAPSHOT",
want: true,
},
"older_version": {
version: "9.0.0-SNAPSHOT",
want: false,
},
"exactly_minimum_version": {
version: "9.1.0",
want: true,
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
v, err := version.ParseVersion(tt.version)
require.NoError(t, err)

got := isRollbackWindowSupported(v)
assert.Equal(t, tt.want, got, "isRollbackWindowSupported(%q)", tt.version)
})
}
}

func TestMakeBaseWatchCmd(t *testing.T) {
exec, err := os.Executable()
require.NoError(t, err)
exec, err = filepath.EvalSymlinks(exec)
require.NoError(t, err)
execDir := filepath.Dir(exec)

agentExecutable := "elastic-agent"
t.Run("no_rollback_window", func(t *testing.T) {
cmd := makeBaseWatchCmd(agentExecutable, 0)

// Expected command:
// elastic-agent watch --path.config /some/path --path.home /some/path
require.Equal(t, cmd.Args, []string{
agentExecutable,
"watch",
"--path.config",
execDir,
"--path.home",
execDir,
})
})

t.Run("with_rollback_window", func(t *testing.T) {
rollbackWindow := 2*time.Hour + 15*time.Minute
cmd := makeBaseWatchCmd(agentExecutable, rollbackWindow)

// Expected command:
// elastic-agent watch --path.config /some/path --path.home /some/path --rollback-window 8100s
require.Equal(t, cmd.Args, []string{
agentExecutable,
"watch",
"--path.config",
execDir,
"--path.home",
execDir,
"--rollback-window",
rollbackWindow.String(),
})
})
}

// checkFilesAfterCleanup is a convenience function to check the file structure within topDir.
// *AgentHome paths must be the expected old and new agent paths relative to topDir (typically in the form of "data/elastic-agent-*")
func checkFilesAfterCleanup(t *testing.T, topDir, newAgentHome string, oldAgentHomes ...string) {
Expand Down
11 changes: 2 additions & 9 deletions internal/pkg/agent/application/upgrade/rollback_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ package upgrade
import (
"os/exec"
"time"

"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
)

const (
Expand All @@ -19,11 +17,6 @@ const (
afterRestartDelay = 20 * time.Second
)

func invokeCmd(agentExecutable string) *exec.Cmd {
// #nosec G204 -- user cannot inject any parameters to this command
cmd := exec.Command(agentExecutable, watcherSubcommand,
"--path.config", paths.Config(),
"--path.home", paths.Top(),
)
return cmd
func makeOSWatchCmd(baseWatchCmd *exec.Cmd) *exec.Cmd {
return baseWatchCmd
}
2 changes: 1 addition & 1 deletion internal/pkg/agent/application/upgrade/step_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (u *Upgrader) downloadArtifact(ctx context.Context, parsedVersion *agtversi
pgpBytes = u.appendFallbackPGP(parsedVersion, pgpBytes)

// do not update source config
settings := *u.settings
settings := *u.downloadSettings
var downloaderFunc downloader
var factory downloaderFactory
var verifier download.Verifier
Expand Down
26 changes: 14 additions & 12 deletions internal/pkg/agent/application/upgrade/step_download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/artifact"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/artifact/download"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
"github.com/elastic/elastic-agent/internal/pkg/agent/configuration"
"github.com/elastic/elastic-agent/internal/pkg/agent/errors"
"github.com/elastic/elastic-agent/pkg/core/logger"
"github.com/elastic/elastic-agent/pkg/core/logger/loggertest"
Expand Down Expand Up @@ -78,29 +79,30 @@ func TestDownloadWithRetries(t *testing.T) {
expectedDownloadPath := "https://artifacts.elastic.co/downloads/beats/elastic-agent"
testLogger, obs := loggertest.New("TestDownloadWithRetries")

settings := artifact.Config{
downloadSettings := artifact.Config{
RetrySleepInitDuration: 20 * time.Millisecond,
HTTPTransportSettings: httpcommon.HTTPTransportSettings{
Timeout: 2 * time.Second,
},
}
upgradeSettings := configuration.UpgradeConfig{}

// Successful immediately (no retries)
t.Run("successful_immediately", func(t *testing.T) {
mockDownloaderCtor := func(version *agtversion.ParsedSemVer, log *logger.Logger, settings *artifact.Config, upgradeDetails *details.Details) (download.Downloader, error) {
return &mockDownloader{expectedDownloadPath, nil}, nil
}

u, err := NewUpgrader(testLogger, &settings, &info.AgentInfo{})
u, err := NewUpgrader(testLogger, &downloadSettings, &upgradeSettings, &info.AgentInfo{})
require.NoError(t, err)

parsedVersion, err := agtversion.ParseVersion("8.9.0")
require.NoError(t, err)

upgradeDetails, upgradeDetailsRetryUntil, upgradeDetailsRetryUntilWasUnset, upgradeDetailsRetryErrorMsg := mockUpgradeDetails(parsedVersion)
minRetryDeadline := time.Now().Add(settings.Timeout)
minRetryDeadline := time.Now().Add(downloadSettings.Timeout)

path, err := u.downloadWithRetries(context.Background(), mockDownloaderCtor, parsedVersion, &settings, upgradeDetails)
path, err := u.downloadWithRetries(context.Background(), mockDownloaderCtor, parsedVersion, &downloadSettings, upgradeDetails)
require.NoError(t, err)
require.Equal(t, expectedDownloadPath, path)

Expand Down Expand Up @@ -141,16 +143,16 @@ func TestDownloadWithRetries(t *testing.T) {
return nil, nil
}

u, err := NewUpgrader(testLogger, &settings, &info.AgentInfo{})
u, err := NewUpgrader(testLogger, &downloadSettings, &upgradeSettings, &info.AgentInfo{})
require.NoError(t, err)

parsedVersion, err := agtversion.ParseVersion("8.9.0")
require.NoError(t, err)

upgradeDetails, upgradeDetailsRetryUntil, upgradeDetailsRetryUntilWasUnset, upgradeDetailsRetryErrorMsg := mockUpgradeDetails(parsedVersion)
minRetryDeadline := time.Now().Add(settings.Timeout)
minRetryDeadline := time.Now().Add(downloadSettings.Timeout)

path, err := u.downloadWithRetries(context.Background(), mockDownloaderCtor, parsedVersion, &settings, upgradeDetails)
path, err := u.downloadWithRetries(context.Background(), mockDownloaderCtor, parsedVersion, &downloadSettings, upgradeDetails)
require.NoError(t, err)
require.Equal(t, expectedDownloadPath, path)

Expand Down Expand Up @@ -196,16 +198,16 @@ func TestDownloadWithRetries(t *testing.T) {
return nil, nil
}

u, err := NewUpgrader(testLogger, &settings, &info.AgentInfo{})
u, err := NewUpgrader(testLogger, &downloadSettings, &upgradeSettings, &info.AgentInfo{})
require.NoError(t, err)

parsedVersion, err := agtversion.ParseVersion("8.9.0")
require.NoError(t, err)

upgradeDetails, upgradeDetailsRetryUntil, upgradeDetailsRetryUntilWasUnset, upgradeDetailsRetryErrorMsg := mockUpgradeDetails(parsedVersion)
minRetryDeadline := time.Now().Add(settings.Timeout)
minRetryDeadline := time.Now().Add(downloadSettings.Timeout)

path, err := u.downloadWithRetries(context.Background(), mockDownloaderCtor, parsedVersion, &settings, upgradeDetails)
path, err := u.downloadWithRetries(context.Background(), mockDownloaderCtor, parsedVersion, &downloadSettings, upgradeDetails)
require.NoError(t, err)
require.Equal(t, expectedDownloadPath, path)

Expand All @@ -231,7 +233,7 @@ func TestDownloadWithRetries(t *testing.T) {

// Download timeout expired (before all retries are exhausted)
t.Run("download_timeout_expired", func(t *testing.T) {
testCaseSettings := settings
testCaseSettings := downloadSettings
testCaseSettings.Timeout = 500 * time.Millisecond
testCaseSettings.RetrySleepInitDuration = 10 * time.Millisecond
// exponential backoff with 10ms init and 500ms timeout should fit at least 3 attempts.
Expand All @@ -241,7 +243,7 @@ func TestDownloadWithRetries(t *testing.T) {
return &mockDownloader{"", errors.New("download failed")}, nil
}

u, err := NewUpgrader(testLogger, &settings, &info.AgentInfo{})
u, err := NewUpgrader(testLogger, &downloadSettings, &upgradeSettings, &info.AgentInfo{})
require.NoError(t, err)

parsedVersion, err := agtversion.ParseVersion("8.9.0")
Expand Down
Loading
Loading