Skip to content

Commit ec59bfc

Browse files
[8.18] (backport #9366) Enhancement/5235 wrap errors when marking upgrade (#9965)
* Enhancement/5235 wrap errors when marking upgrade (#9366) * enhancement(5235): added markUpgradeFunc to abstract markUpgrade * enhancement(5235): using markUpgradeFunc in Upgrade function * enhancement(5235): wrapping writeFile errors in markUpgrade enhancement(5235): added goerrors in step_mark * enhancement(5235): using writeFile wrapper in markUpgrade * enhancement(5235): added tests mark upgrade tests enhancement(5235): updated step mark test to mock writeFile * enhancement(5235): updated markUpgrade function so that it can be tested, jecting dependencies. updated relevant tests * enhancement(5235): updated use of markUpgrade in rollback and rollback tests * enhancement(5235): abstracted markupgrade from upgrader, added relevant types and updated the upgrader struct. added tests case for mark upgrade error handling in the upgrade function * enhancement(5235): added abstractions for changesymlink and rollbackInstall in upgrader, updated error handling tests to use these abstractions * enhancement(5235): added error handling test for changesymlink (cherry picked from commit 2e4e777) # Conflicts: # internal/pkg/agent/application/upgrade/step_mark.go # internal/pkg/agent/application/upgrade/step_mark_test.go # internal/pkg/agent/application/upgrade/upgrade.go # internal/pkg/agent/application/upgrade/upgrade_test.go * resolve conflicts * removed unused tests * removed unused var --------- Co-authored-by: Kaan Yalti <[email protected]>
1 parent 3015b5c commit ec59bfc

File tree

6 files changed

+230
-42
lines changed

6 files changed

+230
-42
lines changed

internal/pkg/agent/application/upgrade/rollback.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func Rollback(ctx context.Context, log *logger.Logger, c client.Client, topDirPa
5252
}
5353

5454
// revert active commit
55-
if err := UpdateActiveCommit(log, topDirPath, prevHash); err != nil {
55+
if err := UpdateActiveCommit(log, topDirPath, prevHash, os.WriteFile); err != nil {
5656
return err
5757
}
5858

internal/pkg/agent/application/upgrade/rollback_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,8 @@ func createUpdateMarker(t *testing.T, log *logger.Logger, topDir, newAgentVersio
504504
hash: oldAgentHash,
505505
versionedHome: oldAgentVersionedHome,
506506
}
507+
508+
markUpgrade := markUpgradeProvider(UpdateActiveCommit, os.WriteFile)
507509
err := markUpgrade(log,
508510
paths.DataFrom(topDir),
509511
newAgentInstall,

internal/pkg/agent/application/upgrade/step_mark.go

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package upgrade
66

77
import (
8+
goerrors "errors"
89
"os"
910
"path/filepath"
1011
"time"
@@ -125,49 +126,53 @@ type agentInstall struct {
125126
versionedHome string
126127
}
127128

128-
// markUpgrade marks update happened so we can handle grace period
129-
func markUpgrade(log *logger.Logger, dataDirPath string, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details) error {
130-
131-
if len(previousAgent.hash) > hashLen {
132-
previousAgent.hash = previousAgent.hash[:hashLen]
133-
}
134-
135-
marker := &UpdateMarker{
136-
Version: agent.version,
137-
Hash: agent.hash,
138-
VersionedHome: agent.versionedHome,
139-
UpdatedOn: time.Now(),
140-
PrevVersion: previousAgent.version,
141-
PrevHash: previousAgent.hash,
142-
PrevVersionedHome: previousAgent.versionedHome,
143-
Action: action,
144-
Details: upgradeDetails,
145-
}
146-
147-
markerBytes, err := yaml.Marshal(newMarkerSerializer(marker))
148-
if err != nil {
149-
return errors.New(err, errors.TypeConfig, "failed to parse marker file")
150-
}
129+
type updateActiveCommitFunc func(log *logger.Logger, topDirPath, hash string, writeFile writeFileFunc) error
151130

152-
markerPath := markerFilePath(dataDirPath)
153-
log.Infow("Writing upgrade marker file", "file.path", markerPath, "hash", marker.Hash, "prev_hash", marker.PrevHash)
154-
if err := os.WriteFile(markerPath, markerBytes, 0600); err != nil {
155-
return errors.New(err, errors.TypeFilesystem, "failed to create update marker file", errors.M(errors.MetaKeyPath, markerPath))
156-
}
131+
// markUpgrade marks update happened so we can handle grace period
132+
func markUpgradeProvider(updateActiveCommit updateActiveCommitFunc, writeFile writeFileFunc) markUpgradeFunc {
133+
return func(log *logger.Logger, dataDirPath string, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details) error {
134+
135+
if len(previousAgent.hash) > hashLen {
136+
previousAgent.hash = previousAgent.hash[:hashLen]
137+
}
138+
139+
marker := &UpdateMarker{
140+
Version: agent.version,
141+
Hash: agent.hash,
142+
VersionedHome: agent.versionedHome,
143+
UpdatedOn: time.Now(),
144+
PrevVersion: previousAgent.version,
145+
PrevHash: previousAgent.hash,
146+
PrevVersionedHome: previousAgent.versionedHome,
147+
Action: action,
148+
Details: upgradeDetails,
149+
}
150+
151+
markerBytes, err := yaml.Marshal(newMarkerSerializer(marker))
152+
if err != nil {
153+
return errors.New(err, errors.TypeConfig, "failed to parse marker file")
154+
}
155+
156+
markerPath := markerFilePath(dataDirPath)
157+
log.Infow("Writing upgrade marker file", "file.path", markerPath, "hash", marker.Hash, "prev_hash", marker.PrevHash)
158+
if err := writeFile(markerPath, markerBytes, 0600); err != nil {
159+
return goerrors.Join(err, errors.New(errors.TypeFilesystem, "failed to create update marker file", errors.M(errors.MetaKeyPath, markerPath)))
160+
}
161+
162+
if err := updateActiveCommit(log, paths.Top(), agent.hash, writeFile); err != nil {
163+
return err
164+
}
157165

158-
if err := UpdateActiveCommit(log, paths.Top(), agent.hash); err != nil {
159-
return err
166+
return nil
160167
}
161-
162-
return nil
163168
}
164169

165170
// UpdateActiveCommit updates active.commit file to point to active version.
166-
func UpdateActiveCommit(log *logger.Logger, topDirPath, hash string) error {
171+
func UpdateActiveCommit(log *logger.Logger, topDirPath, hash string, writeFile writeFileFunc) error {
167172
activeCommitPath := filepath.Join(topDirPath, agentCommitFile)
168173
log.Infow("Updating active commit", "file.path", activeCommitPath, "hash", hash)
169-
if err := os.WriteFile(activeCommitPath, []byte(hash), 0600); err != nil {
170-
return errors.New(err, errors.TypeFilesystem, "failed to update active commit", errors.M(errors.MetaKeyPath, activeCommitPath))
174+
if err := writeFile(activeCommitPath, []byte(hash), 0600); err != nil {
175+
return goerrors.Join(err, errors.New(errors.TypeFilesystem, "failed to update active commit", errors.M(errors.MetaKeyPath, activeCommitPath)))
171176
}
172177

173178
return nil
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
package upgrade
6+
7+
import (
8+
"errors"
9+
"os"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
14+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
15+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
16+
"github.com/elastic/elastic-agent/internal/pkg/fleetapi"
17+
"github.com/elastic/elastic-agent/pkg/core/logger"
18+
"github.com/elastic/elastic-agent/pkg/core/logger/loggertest"
19+
)
20+
21+
func TestMarkUpgrade(t *testing.T) {
22+
log, _ := loggertest.New("test")
23+
agent := agentInstall{
24+
version: "8.5.0",
25+
hash: "abc123",
26+
versionedHome: "home/v8.5.0",
27+
}
28+
previousAgent := agentInstall{
29+
version: "8.4.0",
30+
hash: "xyz789",
31+
versionedHome: "home/v8.4.0",
32+
}
33+
action := &fleetapi.ActionUpgrade{
34+
ActionID: "action-123",
35+
ActionType: "UPGRADE",
36+
Data: fleetapi.ActionUpgradeData{
37+
Version: "8.5.0",
38+
SourceURI: "https://example.com/upgrade",
39+
},
40+
}
41+
upgradeDetails := details.NewDetails("8.5.0", details.StateScheduled, "action-123")
42+
43+
testError := errors.New("test error")
44+
45+
type testCase struct {
46+
fileName string
47+
expectedError error
48+
markUpgrade markUpgradeFunc
49+
}
50+
51+
testCases := map[string]testCase{
52+
"should return error if it fails updating the active commit file": {
53+
fileName: "commit",
54+
expectedError: testError,
55+
markUpgrade: markUpgradeProvider(func(log *logger.Logger, topDirPath, hash string, writeFile writeFileFunc) error {
56+
return testError
57+
}, func(name string, data []byte, perm os.FileMode) error {
58+
return nil
59+
}),
60+
},
61+
"should return error if it fails writing to marker file": {
62+
fileName: "marker",
63+
expectedError: testError,
64+
markUpgrade: markUpgradeProvider(func(log *logger.Logger, topDirPath, hash string, writeFile writeFileFunc) error {
65+
return nil
66+
}, func(name string, data []byte, perm os.FileMode) error {
67+
return testError
68+
}),
69+
},
70+
}
71+
72+
for name, tc := range testCases {
73+
t.Run(name, func(t *testing.T) {
74+
baseDir := t.TempDir()
75+
paths.SetTop(baseDir)
76+
77+
err := tc.markUpgrade(log, paths.Data(), agent, previousAgent, action, upgradeDetails)
78+
require.Error(t, err)
79+
require.ErrorIs(t, err, tc.expectedError)
80+
})
81+
}
82+
}
83+
84+
func TestUpdateActiveCommit(t *testing.T) {
85+
log, _ := loggertest.New("test")
86+
testError := errors.New("test error")
87+
testCases := map[string]struct {
88+
expectedError error
89+
writeFileFunc writeFileFunc
90+
}{
91+
"should return error if it fails writing to file": {
92+
expectedError: testError,
93+
writeFileFunc: func(name string, data []byte, perm os.FileMode) error {
94+
return testError
95+
},
96+
},
97+
"should not return error if it writes to file": {
98+
expectedError: nil,
99+
writeFileFunc: func(name string, data []byte, perm os.FileMode) error {
100+
return nil
101+
},
102+
},
103+
}
104+
for name, tc := range testCases {
105+
t.Run(name, func(t *testing.T) {
106+
err := UpdateActiveCommit(log, paths.Top(), "hash", tc.writeFileFunc)
107+
require.ErrorIs(t, err, tc.expectedError)
108+
})
109+
}
110+
111+
}

internal/pkg/agent/application/upgrade/upgrade.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ type unpackHandler interface {
7171
type copyActionStoreFunc func(log *logger.Logger, newHome string) error
7272
type copyRunDirectoryFunc func(log *logger.Logger, oldRunPath, newRunPath string) error
7373
type fileDirCopyFunc func(from, to string, opts ...copy.Options) error
74+
type markUpgradeFunc func(log *logger.Logger, dataDirPath string, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details) error
75+
type changeSymlinkFunc func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error
76+
type rollbackInstallFunc func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error
7477

7578
// Types used to abstract stdlib functions
7679
type mkdirAllFunc func(name string, perm fs.FileMode) error
@@ -92,6 +95,9 @@ type Upgrader struct {
9295
isDiskSpaceErrorFunc func(err error) bool
9396
copyActionStore copyActionStoreFunc
9497
copyRunDirectory copyRunDirectoryFunc
98+
markUpgrade markUpgradeFunc
99+
changeSymlink changeSymlinkFunc
100+
rollbackInstall rollbackInstallFunc
95101
}
96102

97103
// IsUpgradeable when agent is installed and running as a service or flag was provided.
@@ -114,6 +120,9 @@ func NewUpgrader(log *logger.Logger, settings *artifact.Config, agentInfo info.A
114120
isDiskSpaceErrorFunc: upgradeErrors.IsDiskSpaceError,
115121
copyActionStore: copyActionStoreProvider(os.ReadFile, os.WriteFile),
116122
copyRunDirectory: copyRunDirectoryProvider(os.MkdirAll, copy.Copy),
123+
markUpgrade: markUpgradeProvider(UpdateActiveCommit, os.WriteFile),
124+
changeSymlink: changeSymlink,
125+
rollbackInstall: rollbackInstall,
117126
}, nil
118127
}
119128

@@ -317,9 +326,9 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, sourceURI string
317326
return nil, fmt.Errorf("calculating home path relative to top, home: %q top: %q : %w", paths.Home(), paths.Top(), err)
318327
}
319328

320-
if err := changeSymlink(u.log, paths.Top(), symlinkPath, newPath); err != nil {
329+
if err := u.changeSymlink(u.log, paths.Top(), symlinkPath, newPath); err != nil {
321330
u.log.Errorw("Rolling back: changing symlink failed", "error.message", err)
322-
rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
331+
rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
323332
return nil, goerrors.Join(err, rollbackErr)
324333
}
325334

@@ -342,13 +351,13 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, sourceURI string
342351
versionedHome: currentVersionedHome,
343352
}
344353

345-
if err := markUpgrade(u.log,
354+
if err := u.markUpgrade(u.log,
346355
paths.Data(), // data dir to place the marker in
347356
current, // new agent version data
348357
previous, // old agent version data
349358
action, det); err != nil {
350359
u.log.Errorw("Rolling back: marking upgrade failed", "error.message", err)
351-
rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
360+
rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
352361
return nil, goerrors.Join(err, rollbackErr)
353362
}
354363

@@ -357,14 +366,14 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, sourceURI string
357366
var watcherCmd *exec.Cmd
358367
if watcherCmd, err = InvokeWatcher(u.log, watcherExecutable); err != nil {
359368
u.log.Errorw("Rolling back: starting watcher failed", "error.message", err)
360-
rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
369+
rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
361370
return nil, goerrors.Join(err, rollbackErr)
362371
}
363372

364373
watcherWaitErr := waitForWatcher(ctx, u.log, markerFilePath(paths.Data()), watcherMaxWaitTime)
365374
if watcherWaitErr != nil {
366375
killWatcherErr := watcherCmd.Process.Kill()
367-
rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
376+
rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
368377
return nil, goerrors.Join(watcherWaitErr, killWatcherErr, rollbackErr)
369378
}
370379

internal/pkg/agent/application/upgrade/upgrade_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,67 @@ func TestUpgradeErrorHandling(t *testing.T) {
12551255
}
12561256
},
12571257
},
1258+
"should return error if changeSymlink fails": {
1259+
isDiskSpaceErrorResult: false,
1260+
expectedError: testError,
1261+
upgraderMocker: func(upgrader *Upgrader) {
1262+
upgrader.artifactDownloader = &mockArtifactDownloader{}
1263+
upgrader.unpacker = &mockUnpacker{
1264+
returnPackageMetadata: packageMetadata{
1265+
manifest: &v1.PackageManifest{},
1266+
hash: "hash",
1267+
},
1268+
returnUnpackResult: UnpackResult{
1269+
Hash: "hash",
1270+
VersionedHome: "versionedHome",
1271+
},
1272+
}
1273+
upgrader.copyActionStore = func(log *logger.Logger, newHome string) error {
1274+
return nil
1275+
}
1276+
upgrader.copyRunDirectory = func(log *logger.Logger, oldRunPath, newRunPath string) error {
1277+
return nil
1278+
}
1279+
upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error {
1280+
return nil
1281+
}
1282+
upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error {
1283+
return testError
1284+
}
1285+
},
1286+
},
1287+
"should return error if markUpgrade fails": {
1288+
isDiskSpaceErrorResult: false,
1289+
expectedError: testError,
1290+
upgraderMocker: func(upgrader *Upgrader) {
1291+
upgrader.artifactDownloader = &mockArtifactDownloader{}
1292+
upgrader.unpacker = &mockUnpacker{
1293+
returnPackageMetadata: packageMetadata{
1294+
manifest: &v1.PackageManifest{},
1295+
hash: "hash",
1296+
},
1297+
returnUnpackResult: UnpackResult{
1298+
Hash: "hash",
1299+
VersionedHome: "versionedHome",
1300+
},
1301+
}
1302+
upgrader.copyActionStore = func(log *logger.Logger, newHome string) error {
1303+
return nil
1304+
}
1305+
upgrader.copyRunDirectory = func(log *logger.Logger, oldRunPath, newRunPath string) error {
1306+
return nil
1307+
}
1308+
upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error {
1309+
return nil
1310+
}
1311+
upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error {
1312+
return nil
1313+
}
1314+
upgrader.markUpgrade = func(log *logger.Logger, dataDirPath string, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details) error {
1315+
return testError
1316+
}
1317+
},
1318+
},
12581319
"should add disk space error to the error chain if downloadArtifact fails with disk space error": {
12591320
isDiskSpaceErrorResult: true,
12601321
expectedError: upgradeErrors.ErrInsufficientDiskSpace,

0 commit comments

Comments
 (0)