Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
127 commits
Select commit Hold shift + click to select a range
8e5ac20
enhancement(5235): added disc space error function to respond to the …
Jul 24, 2025
f0a1bce
enhancement(5235): updated error wrapping
Jul 24, 2025
c12f10e
enhancement(5235): revert test timeout
Jul 24, 2025
daeaf92
enhancement(5235): added progress reporter test
Jul 24, 2025
7a2e118
enhancement(5235): added test for ReportFailed
Jul 25, 2025
ffc4ec0
enhancement(5235): created disk space error type, wrapping backoff pe…
Jul 25, 2025
9d072f9
enhancement(5235): reverted downloader with retry changes
Jul 25, 2025
9f5c84a
enhancement(5235): added insufficient disk space error type, moved er…
Jul 25, 2025
eb915fb
enhancement(5235): updated insufficient disk error, removed backoff r…
Jul 28, 2025
c4aa985
enhancement(5235): moved disk space error into its own package under …
Jul 28, 2025
fd35453
enhancement(5235): removed unnecessary error files
Jul 28, 2025
cebb42e
enhancement(5235): added progress reporter interface in http download…
Jul 28, 2025
197ad8d
enhancement(5235): updated tests
Jul 28, 2025
5b7378f
enhancement(5235): updated tests
Jul 28, 2025
5450532
enhancement(5235): added downloader factory provider and updated tests
Jul 28, 2025
3d86720
enhancement(5235): updated progress reporter prepare function, fixed …
Jul 29, 2025
55c05e4
enhancement(5235): refactored end to end upgrade tests to assert copy…
Jul 30, 2025
9ba200d
enhancement(5235): updated fs downloader download tests
Jul 30, 2025
8362d11
enhancement(5235): added tests for progress reporter prepare
Jul 30, 2025
dccf341
enhancement(5235): removed unnecessary logging
Jul 30, 2025
901bdf4
enhancement(5235): injecting details provider. added tests for settin…
Jul 31, 2025
7e18028
enhancement(5235): ran mage check
Jul 31, 2025
54152f8
enhancement(5235): added changelog
Jul 31, 2025
423839d
enhancment(5235): removed commented code
Jul 31, 2025
0577d36
enhancement(5235): fix linting errors
Jul 31, 2025
40e4485
enhancement(5235): fix test cases
Jul 31, 2025
1d38805
enhancement(5235): updated tests, removed unused var
Jul 31, 2025
961a86d
enhancement(5235): updated progress reporter to avoid data race
Jul 31, 2025
43e1f9d
enhancement(5235): removed reference to embedded type
Jul 31, 2025
f44e5a3
enhancement(5235): returning path from download functions
Jul 31, 2025
64b1047
enhancement(5235): using config arch instead of runtime arch
Jul 31, 2025
7b758d0
enhancement(5235): updated download functions to remove the base dire…
Jul 31, 2025
d6c6063
enhancement(5235): refactored artifact test
Jul 31, 2025
a6b4f16
enhancement(5235): ran mage check
Jul 31, 2025
dae42d5
enhancement(5235): removed unnecessary comments
Jul 31, 2025
0aedf85
enhancement(5235): reverted downloader cleanup to
Aug 4, 2025
064c411
enhancement(5235): took notes
Aug 1, 2025
d6b3921
enhancement(5235): added upgrade cleaner
Aug 2, 2025
7f3e787
enhancement(5235): downloaders return download result instead of path…
Aug 4, 2025
2dc8977
enhancement(5235): refactored downloaders, refactored cleanup functio…
Aug 4, 2025
96bd06c
enhancement(5235): removed calls to rollback in the upgrade function,…
Aug 4, 2025
ec02678
enhancement(5235): removed unused rollback function
Aug 4, 2025
d3428d3
enhancement(5235): updated error handling in upgrade function
Aug 5, 2025
c097d72
enhancement(5235): added artifact downloader interface, updated tests
Aug 5, 2025
ac25c3d
enhancement(5235): added unpacker interface, updated tests
Aug 5, 2025
4cb407b
enhancement(5235): added replacer interface
Aug 5, 2025
3f841a9
enhancement(5235): added watcher interface and updated tests
Aug 6, 2025
8a3cf7a
enhancement(5235): removed test case
Aug 6, 2025
66362f8
enhancement(5235): moved clean up function into step download, remove…
Aug 6, 2025
17f91d3
enhancement(5235): added relinker interface
Aug 6, 2025
c201c9d
enhancement(5235): added directory copier interface
Aug 6, 2025
1d690ff
enhancement(5235): moved unpacker functions into one file
Aug 6, 2025
49fcef2
enhancement(5235): moved ugprade wather and marker functions into one…
Aug 6, 2025
1fcd3fa
enhancement(5235): refactpred upgrade watcher functions and tests
Aug 6, 2025
659037e
enhancement(5235): removed replacer interface
Aug 6, 2025
dd9ea31
enhancement(5235): moved directory copy functions, moved relevant tests
Aug 6, 2025
9933583
enhancement(5235): added invokeWatcher to upgradeWatcher to implement…
Aug 6, 2025
20eba22
enhancement(5235): added upgrade executor
Aug 6, 2025
876ff2f
enhancement(5235): removed commented code
Aug 6, 2025
185e5ec
enhancement(5235): updated download error tests
Aug 6, 2025
fb97058
enhancement(5235): refactored interfaces and commented unused code
Aug 7, 2025
acd4852
enhancement(5235): added download artifact step test case
Aug 7, 2025
a3a40ac
enhancement(5235): remove commented code
Aug 7, 2025
d7fde0e
enhancement(5235): added tests for executor download step, will need …
Aug 7, 2025
7ea282b
enhancement(5235): updated mockery config and added mocks for the int…
Aug 7, 2025
b4fe2a5
enhancemen(5235): added wathcer mock
Aug 7, 2025
794ae57
enhancement(5235): added download step tests
Aug 7, 2025
225a951
enhancement(5235): removed newHash from unpack step result
Aug 7, 2025
812a6fe
enhancement(5235): injecting check upgrade fn into unpack step
Aug 7, 2025
bf42cb5
enhancement(5235): removed testValues from download step test
Aug 7, 2025
342a6f6
enhancement(5235): added unpack step tests
Aug 7, 2025
117d691
enhancement(5235): removed import type alias
Aug 7, 2025
2634a3d
enhancement(5235): added check upgrade fn
Aug 7, 2025
9e9ecc4
enhancement(5235): remove commented struct fields
Aug 7, 2025
d04f850
enhancement(5235): removed commented code
Aug 7, 2025
fe339e5
enhancement(5235): injecting check upgrade fn in upgrade function
Aug 7, 2025
37c48a1
enhancement(5235): removed logger from replace old with new function …
Aug 8, 2025
7add803
enhancement(5235): removed logger from replace old with new func defi…
Aug 8, 2025
07a3ece
enhancement(5235): added replace old with new unit tests
Aug 8, 2025
33e0114
enhancement(5235): remove unnecessary func encapsulation
Aug 8, 2025
c3893e7
enhancement(5235): removed unnecessary func encapsulation
Aug 8, 2025
e749936
enhancement(5235): added import
Aug 8, 2025
e495f8e
enhancement(5235): removed logger from watchNewAgent function signature
Aug 8, 2025
adb70f5
enhancmenet(5235): removed log from watch new agent function call
Aug 8, 2025
00dc68a
enhancement(5235): added tests for watch new agent
Aug 8, 2025
53add36
enhancement(5235): removed upgrade abstraction, using the new upgrade…
Aug 8, 2025
8136642
enhancement(5235): added upgrade executor mock, added tests
Aug 8, 2025
02e542b
enhancement(5235): removed nil error check from deferred cleanup
Aug 8, 2025
41e4566
enhancement(5235): removed commented test code from fs downloader tests
Aug 8, 2025
e468776
enhancement(5235): using download results instead of archive path in …
Aug 8, 2025
93029b8
enhancement(5235): using download result instead of archive path in h…
Aug 8, 2025
5221a85
enhancement(5235): removed dev logs
Aug 8, 2025
a7153fc
enhancement(5235): asserting downloads dir is cleaned up if upgrade i…
Aug 8, 2025
3b1c62d
enhancement(5235): added package scoped var for unpack step to abstra…
Aug 8, 2025
41ef242
enhancement(5235): returning unpack result with hash
Aug 8, 2025
4d32d4b
enhancement(5235): added tests for copy func errors in unpack
Aug 8, 2025
460ad37
enhancement(5235): added asc downloader and tests back in
Aug 9, 2025
6bd4a2b
enhancement(5235): asserting returned error
Aug 9, 2025
4eda991
enhancement(5235): moved test functions around
Aug 9, 2025
69c94d1
enhancement(5235): added unpack step error handling tests
Aug 9, 2025
9c894f4
enhancement(5235): removed unnecessary commented code
Aug 9, 2025
6d5f508
enhancement(5235): removed dev comment
Aug 9, 2025
2fc7cfe
enhancement(5235): using config to get os and arch instead of runtime
Aug 9, 2025
5af449b
enhancmenet(5235): using config to get os and arch
Aug 9, 2025
6d923ce
enhancement(5235): updated comment
Aug 9, 2025
3c2dfb4
enhancement(5235): added package level exported var for paths.Home() …
Aug 10, 2025
ba288a1
enhancement(5235): added package level vars for cop.Copy and os.Write…
Aug 10, 2025
07db133
enhancement(5235): added function to check archive files are extracte…
Aug 10, 2025
29ec259
enhancement(5235): added helper functions to create archives and adde…
Aug 10, 2025
e8b582d
enhancement(5235): updated the use of archive helpers in unpack error…
Aug 10, 2025
5e41094
enhancement(5235): updated test function names
Aug 10, 2025
9bef459
enhancement(5235): use home path to get run path
Aug 10, 2025
8a1ee3c
enhancement(5235): added run dir copy error test
Aug 10, 2025
a9aaa6c
enhancement(5235): refactored action store copy and run dir copy erro…
Aug 10, 2025
df1180e
enhancement(5235): updated archive file modification functions
Aug 10, 2025
66e7864
enhancement(5235): using config target path for assertion
Aug 10, 2025
d58a4ad
enhancement(5235): added package symlink func to abstract os symlink
Aug 10, 2025
cdba013
enhancement(5235): removed unused var from function, regenerated mocks
Aug 10, 2025
5eced8d
enhancement(5235): using homepath instead of home
Aug 10, 2025
e66408a
enhancement(5235): update mock use in test
Aug 10, 2025
5267635
enhancement(5235): added symlink error test
Aug 10, 2025
203a1a1
enhancement(5235): added mark upgrade error handling test
Aug 10, 2025
400b547
enhancement(5235): added diskspace error conversion
Aug 10, 2025
8418253
enhancement(5235): using writefile stub
Aug 10, 2025
818994f
enhancement(5235): added command exec package var in rollback
Aug 10, 2025
64a6035
enhancement(5235): added release ugpradeable and context timeout pack…
Aug 10, 2025
b824ae6
enhancement(5235): added invoke wathcer and wait for watcher error ha…
Aug 10, 2025
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
14 changes: 14 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,17 @@ packages:
installationModifier:
config:
mockname: "InstallationModifier"
github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade:
config:
inpackage: true
dir: internal/pkg/agent/application/upgrade
mockname: "mock_{{.InterfaceName}}"
interfaces:
artifactDownloader:
unpacker:
relinker:
watcher:
agentDirectoryCopier:
upgradeCleaner:
upgradeExecutor:

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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: enhancement

# Change summary; a 80ish characters long description of the change.
summary: Return disk space error message when agent runs out of space while downloading upgrade artifact.

# 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:

# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
component: elastic-agent

# 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/elastic/elastic-agent/pull/9122

# 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/5235
12 changes: 10 additions & 2 deletions internal/pkg/agent/application/coordinator/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/elastic/elastic-agent/internal/pkg/agent/application/reexec"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
upgradeErrors "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/errors"
"github.com/elastic/elastic-agent/internal/pkg/agent/configuration"
"github.com/elastic/elastic-agent/internal/pkg/agent/storage"
"github.com/elastic/elastic-agent/internal/pkg/agent/transpiler"
Expand Down Expand Up @@ -359,6 +360,7 @@ type Coordinator struct {
// run a ticker that checks to see if we have a new PID.
componentPIDTicker *time.Ticker
componentPidRequiresUpdate *atomic.Bool
detailsProvider func(string, details.State, string) *details.Details
}

// The channels Coordinator reads to receive updates from the various managers.
Expand Down Expand Up @@ -478,7 +480,8 @@ func New(
componentPIDTicker: time.NewTicker(time.Second * 30),
componentPidRequiresUpdate: &atomic.Bool{},

fleetAcker: fleetAcker,
fleetAcker: fleetAcker,
detailsProvider: details.NewDetails,
}
// Setup communication channels for any non-nil components. This pattern
// lets us transparently accept nil managers / simulated events during
Expand Down Expand Up @@ -734,7 +737,7 @@ func (c *Coordinator) Upgrade(ctx context.Context, version string, sourceURI str
if action != nil {
actionID = action.ActionID
}
det := details.NewDetails(version, details.StateRequested, actionID)
det := c.detailsProvider(version, details.StateRequested, actionID)
det.RegisterObserver(c.SetUpgradeDetails)

cb, err := c.upgradeMgr.Upgrade(ctx, version, sourceURI, action, det, skipVerifyOverride, skipDefaultPgp, pgpBytes...)
Expand All @@ -745,6 +748,11 @@ func (c *Coordinator) Upgrade(ctx context.Context, version string, sourceURI str
det.SetState(details.StateCompleted)
return c.upgradeMgr.AckAction(ctx, c.fleetAcker, action)
}

if errors.Is(err, upgradeErrors.ErrInsufficientDiskSpace) {
err = upgradeErrors.ErrInsufficientDiskSpace
}

det.Fail(err)
return err
}
Expand Down
154 changes: 154 additions & 0 deletions internal/pkg/agent/application/coordinator/coordinator_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"

"github.com/elastic/elastic-agent-client/v7/pkg/proto"
"github.com/elastic/elastic-agent/internal/pkg/fleetapi/acker"
"github.com/elastic/elastic-agent/internal/pkg/testutils"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status"
Expand All @@ -41,10 +44,12 @@ import (
"github.com/elastic/elastic-agent/internal/pkg/agent/application/info"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/monitoring/reload"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/reexec"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/secret"
"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"
upgradeErrors "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/errors"
"github.com/elastic/elastic-agent/internal/pkg/agent/configuration"
"github.com/elastic/elastic-agent/internal/pkg/agent/storage"
"github.com/elastic/elastic-agent/internal/pkg/agent/transpiler"
Expand Down Expand Up @@ -1500,6 +1505,7 @@ func TestCoordinatorInitiatesUpgrade(t *testing.T) {
overrideStateChan: overrideStateChan,
upgradeDetailsChan: upgradeDetailsChan,
upgradeMgr: upgradeMgr,
detailsProvider: details.NewDetails,
logger: logp.NewLogger("testing"),
}

Expand Down Expand Up @@ -1959,3 +1965,151 @@ func TestHasEndpoint(t *testing.T) {
})
}
}

type mockUpgradeMgrUpgradeErrorHandlingTest struct {
upgradeErr error
}

func (m *mockUpgradeMgrUpgradeErrorHandlingTest) Upgradeable() bool {
return true
}

func (m *mockUpgradeMgrUpgradeErrorHandlingTest) Reload(rawConfig *config.Config) error {
return nil
}

func (m *mockUpgradeMgrUpgradeErrorHandlingTest) Upgrade(ctx context.Context, version string, sourceURI string, action *fleetapi.ActionUpgrade, details *details.Details, skipVerifyOverride bool, skipDefaultPgp bool, pgpBytes ...string) (reexec.ShutdownCallbackFn, error) {
return nil, m.upgradeErr
}

func (m *mockUpgradeMgrUpgradeErrorHandlingTest) Ack(ctx context.Context, acker acker.Acker) error {
return nil
}

func (m *mockUpgradeMgrUpgradeErrorHandlingTest) AckAction(ctx context.Context, acker acker.Acker, action fleetapi.Action) error {
return errors.New("ack action error")
}

func (m *mockUpgradeMgrUpgradeErrorHandlingTest) MarkerWatcher() upgrade.MarkerWatcher {
return nil
}

type testDetail struct {
initialState details.State
expectedState details.State
failedState details.State
errorMsg string
}

func TestCoordinatorUpgradeErrorHandling(t *testing.T) {
testCases := map[string]struct {
upgradeErr error
expectedError error
detail testDetail
}{
"insufficient disk space": {
upgradeErr: upgradeErrors.ErrInsufficientDiskSpace,
expectedError: upgradeErrors.ErrInsufficientDiskSpace,
detail: testDetail{
initialState: details.StateRequested,
expectedState: details.StateFailed,
failedState: details.StateRequested,
errorMsg: upgradeErrors.ErrInsufficientDiskSpace.Error(),
},
},
"wrapped insufficient disk space": {
upgradeErr: fmt.Errorf("wrapped: %w", upgradeErrors.ErrInsufficientDiskSpace),
expectedError: upgradeErrors.ErrInsufficientDiskSpace,
detail: testDetail{
initialState: details.StateRequested,
expectedState: details.StateFailed,
failedState: details.StateRequested,
errorMsg: upgradeErrors.ErrInsufficientDiskSpace.Error(),
},
},
"same version error": {
upgradeErr: upgrade.ErrUpgradeSameVersion,
expectedError: errors.New("ack action error"),
detail: testDetail{
initialState: details.StateRequested,
expectedState: details.StateCompleted,
failedState: "",
errorMsg: "",
},
},
"generic error": {
upgradeErr: errors.New("test error"),
expectedError: errors.New("test error"),
detail: testDetail{
initialState: details.StateRequested,
expectedState: details.StateFailed,
failedState: details.StateRequested,
errorMsg: "test error",
},
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
upgradeMgr := &mockUpgradeMgrUpgradeErrorHandlingTest{}
upgradeMgr.upgradeErr = tc.upgradeErr

det := details.NewDetails("1.0.0", tc.detail.initialState, "test-action-id")
detailsProvider := func(version string, state details.State, actionID string) *details.Details {
return det
}

coord := &Coordinator{
upgradeMgr: upgradeMgr,
detailsProvider: detailsProvider,
stateBroadcaster: broadcaster.New(State{State: agentclient.Healthy, Message: "Running"}, 64, 32),
overrideStateChan: make(chan *coordinatorOverrideState),
upgradeDetailsChan: make(chan *details.Details, 2),
}

go func() {
state1 := <-coord.overrideStateChan
assert.Equal(t, agentclient.Upgrading, state1.state)

state2 := <-coord.overrideStateChan
assert.Nil(t, state2)
}()

err := coord.Upgrade(t.Context(), "mockversion", "mockuri", nil, false, false)
require.Error(t, err)
require.Equal(t, err, tc.expectedError)

require.Equal(t, tc.detail.expectedState, det.State, "State mismatch")
require.Equal(t, tc.detail.failedState, det.Metadata.FailedState, "FailedState mismatch")
require.Equal(t, tc.detail.errorMsg, det.Metadata.ErrorMsg, "ErrorMsg mismatch")
})
}

}

func TestCoordinatorNew(t *testing.T) {
t.Run("correctly sets details provider", func(t *testing.T) {
coord := New(
nil,
nil,
logp.InfoLevel,
nil,
component.RuntimeSpecs{},
nil,
nil,
nil,
nil,
nil,
nil,
nil,
false,
nil,
nil,
)

assert.NotNil(t, coord.detailsProvider)
detailsProviderPtr := reflect.ValueOf(coord.detailsProvider).Pointer()
newDetailsPtr := reflect.ValueOf(details.NewDetails).Pointer()
assert.Equal(t, newDetailsPtr, detailsProviderPtr, "detailsProvider should be details.NewDetails")
})
}
2 changes: 1 addition & 1 deletion internal/pkg/agent/application/paths/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func DataFrom(topDirPath string) string {

// Run returns the run directory for Agent
func Run() string {
return filepath.Join(Home(), "run")
return filepath.Join(HomePath(), "run")
}

// Components returns the component directory for Agent
Expand Down
8 changes: 5 additions & 3 deletions internal/pkg/agent/application/paths/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,21 @@ func AgentCapabilitiesPath() string {
return filepath.Join(Config(), defaultAgentCapabilitiesFile)
}

var HomePath = Home // used only for mocking Home() for testing purposes

// AgentActionStoreFile is the file that contains the action that can be replayed after restart.
func AgentActionStoreFile() string {
return filepath.Join(Home(), defaultAgentActionStoreFile)
return filepath.Join(HomePath(), defaultAgentActionStoreFile)
}

// AgentStateStoreYmlFile is the file that contains the persisted state of the agent including the action that can be replayed after restart.
func AgentStateStoreYmlFile() string {
return filepath.Join(Home(), defaultAgentStateStoreYmlFile)
return filepath.Join(HomePath(), defaultAgentStateStoreYmlFile)
}

// AgentStateStoreFile is the file that contains the persisted state of the agent including the action that can be replayed after restart encrypted.
func AgentStateStoreFile() string {
return filepath.Join(Home(), defaultAgentStateStoreFile)
return filepath.Join(HomePath(), defaultAgentStateStoreFile)
}

// AgentInputsDPath is directory that contains the fragment of inputs yaml for K8s deployment.
Expand Down
Loading
Loading