Skip to content

Commit b57e0bd

Browse files
Enhancement/5235 wrap errors when marking upgrade (#9366) (#9967)
* 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) Co-authored-by: Kaan Yalti <[email protected]>
1 parent 5155e88 commit b57e0bd

File tree

6 files changed

+231
-43
lines changed

6 files changed

+231
-43
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: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package upgrade
66

77
import (
88
"encoding/json"
9+
goerrors "errors"
910
"fmt"
1011
"os"
1112
"path/filepath"
@@ -196,50 +197,54 @@ type agentInstall struct {
196197
versionedHome string
197198
}
198199

199-
// markUpgrade marks update happened so we can handle grace period
200-
func markUpgrade(log *logger.Logger, dataDirPath string, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, desiredOutcome UpgradeOutcome) error {
201-
202-
if len(previousAgent.hash) > hashLen {
203-
previousAgent.hash = previousAgent.hash[:hashLen]
204-
}
205-
206-
marker := &UpdateMarker{
207-
Version: agent.version,
208-
Hash: agent.hash,
209-
VersionedHome: agent.versionedHome,
210-
UpdatedOn: time.Now(),
211-
PrevVersion: previousAgent.version,
212-
PrevHash: previousAgent.hash,
213-
PrevVersionedHome: previousAgent.versionedHome,
214-
Action: action,
215-
Details: upgradeDetails,
216-
DesiredOutcome: desiredOutcome,
217-
}
218-
219-
markerBytes, err := yaml.Marshal(newMarkerSerializer(marker))
220-
if err != nil {
221-
return errors.New(err, errors.TypeConfig, "failed to parse marker file")
222-
}
200+
type updateActiveCommitFunc func(log *logger.Logger, topDirPath, hash string, writeFile writeFileFunc) error
223201

224-
markerPath := markerFilePath(dataDirPath)
225-
log.Infow("Writing upgrade marker file", "file.path", markerPath, "hash", marker.Hash, "prev_hash", marker.PrevHash)
226-
if err := os.WriteFile(markerPath, markerBytes, 0600); err != nil {
227-
return errors.New(err, errors.TypeFilesystem, "failed to create update marker file", errors.M(errors.MetaKeyPath, markerPath))
228-
}
202+
// markUpgrade marks update happened so we can handle grace period
203+
func markUpgradeProvider(updateActiveCommit updateActiveCommitFunc, writeFile writeFileFunc) markUpgradeFunc {
204+
return func(log *logger.Logger, dataDirPath string, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, desiredOutcome UpgradeOutcome) error {
205+
206+
if len(previousAgent.hash) > hashLen {
207+
previousAgent.hash = previousAgent.hash[:hashLen]
208+
}
209+
210+
marker := &UpdateMarker{
211+
Version: agent.version,
212+
Hash: agent.hash,
213+
VersionedHome: agent.versionedHome,
214+
UpdatedOn: time.Now(),
215+
PrevVersion: previousAgent.version,
216+
PrevHash: previousAgent.hash,
217+
PrevVersionedHome: previousAgent.versionedHome,
218+
Action: action,
219+
Details: upgradeDetails,
220+
DesiredOutcome: desiredOutcome,
221+
}
222+
223+
markerBytes, err := yaml.Marshal(newMarkerSerializer(marker))
224+
if err != nil {
225+
return errors.New(err, errors.TypeConfig, "failed to parse marker file")
226+
}
227+
228+
markerPath := markerFilePath(dataDirPath)
229+
log.Infow("Writing upgrade marker file", "file.path", markerPath, "hash", marker.Hash, "prev_hash", marker.PrevHash)
230+
if err := writeFile(markerPath, markerBytes, 0600); err != nil {
231+
return goerrors.Join(err, errors.New(errors.TypeFilesystem, "failed to create update marker file", errors.M(errors.MetaKeyPath, markerPath)))
232+
}
233+
234+
if err := updateActiveCommit(log, paths.Top(), agent.hash, writeFile); err != nil {
235+
return err
236+
}
229237

230-
if err := UpdateActiveCommit(log, paths.Top(), agent.hash); err != nil {
231-
return err
238+
return nil
232239
}
233-
234-
return nil
235240
}
236241

237242
// UpdateActiveCommit updates active.commit file to point to active version.
238-
func UpdateActiveCommit(log *logger.Logger, topDirPath, hash string) error {
243+
func UpdateActiveCommit(log *logger.Logger, topDirPath, hash string, writeFile writeFileFunc) error {
239244
activeCommitPath := filepath.Join(topDirPath, agentCommitFile)
240245
log.Infow("Updating active commit", "file.path", activeCommitPath, "hash", hash)
241-
if err := os.WriteFile(activeCommitPath, []byte(hash), 0600); err != nil {
242-
return errors.New(err, errors.TypeFilesystem, "failed to update active commit", errors.M(errors.MetaKeyPath, activeCommitPath))
246+
if err := writeFile(activeCommitPath, []byte(hash), 0600); err != nil {
247+
return goerrors.Join(err, errors.New(errors.TypeFilesystem, "failed to update active commit", errors.M(errors.MetaKeyPath, activeCommitPath)))
243248
}
244249

245250
return nil

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
package upgrade
66

77
import (
8+
"errors"
89
"os"
910
"path/filepath"
1011
"testing"
1112
"time"
1213

1314
"github.com/stretchr/testify/require"
1415

16+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
1517
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
1618
"github.com/elastic/elastic-agent/internal/pkg/fleetapi"
19+
"github.com/elastic/elastic-agent/pkg/core/logger"
20+
"github.com/elastic/elastic-agent/pkg/core/logger/loggertest"
1721
)
1822

1923
func TestSaveAndLoadMarker_NoLoss(t *testing.T) {
@@ -260,3 +264,96 @@ desired_outcome: true
260264
})
261265
}
262266
}
267+
268+
func TestMarkUpgrade(t *testing.T) {
269+
log, _ := loggertest.New("test")
270+
agent := agentInstall{
271+
version: "8.5.0",
272+
hash: "abc123",
273+
versionedHome: "home/v8.5.0",
274+
}
275+
previousAgent := agentInstall{
276+
version: "8.4.0",
277+
hash: "xyz789",
278+
versionedHome: "home/v8.4.0",
279+
}
280+
action := &fleetapi.ActionUpgrade{
281+
ActionID: "action-123",
282+
ActionType: "UPGRADE",
283+
Data: fleetapi.ActionUpgradeData{
284+
Version: "8.5.0",
285+
SourceURI: "https://example.com/upgrade",
286+
},
287+
}
288+
upgradeDetails := details.NewDetails("8.5.0", details.StateScheduled, "action-123")
289+
desiredOutcome := OUTCOME_UPGRADE
290+
291+
testError := errors.New("test error")
292+
293+
type testCase struct {
294+
fileName string
295+
expectedError error
296+
markUpgrade markUpgradeFunc
297+
}
298+
299+
testCases := map[string]testCase{
300+
"should return error if it fails updating the active commit file": {
301+
fileName: "commit",
302+
expectedError: testError,
303+
markUpgrade: markUpgradeProvider(func(log *logger.Logger, topDirPath, hash string, writeFile writeFileFunc) error {
304+
return testError
305+
}, func(name string, data []byte, perm os.FileMode) error {
306+
return nil
307+
}),
308+
},
309+
"should return error if it fails writing to marker file": {
310+
fileName: "marker",
311+
expectedError: testError,
312+
markUpgrade: markUpgradeProvider(func(log *logger.Logger, topDirPath, hash string, writeFile writeFileFunc) error {
313+
return nil
314+
}, func(name string, data []byte, perm os.FileMode) error {
315+
return testError
316+
}),
317+
},
318+
}
319+
320+
for name, tc := range testCases {
321+
t.Run(name, func(t *testing.T) {
322+
baseDir := t.TempDir()
323+
paths.SetTop(baseDir)
324+
325+
err := tc.markUpgrade(log, paths.Data(), agent, previousAgent, action, upgradeDetails, desiredOutcome)
326+
require.Error(t, err)
327+
require.ErrorIs(t, err, tc.expectedError)
328+
})
329+
}
330+
}
331+
332+
func TestUpdateActiveCommit(t *testing.T) {
333+
log, _ := loggertest.New("test")
334+
testError := errors.New("test error")
335+
testCases := map[string]struct {
336+
expectedError error
337+
writeFileFunc writeFileFunc
338+
}{
339+
"should return error if it fails writing to file": {
340+
expectedError: testError,
341+
writeFileFunc: func(name string, data []byte, perm os.FileMode) error {
342+
return testError
343+
},
344+
},
345+
"should not return error if it writes to file": {
346+
expectedError: nil,
347+
writeFileFunc: func(name string, data []byte, perm os.FileMode) error {
348+
return nil
349+
},
350+
},
351+
}
352+
for name, tc := range testCases {
353+
t.Run(name, func(t *testing.T) {
354+
err := UpdateActiveCommit(log, paths.Top(), "hash", tc.writeFileFunc)
355+
require.ErrorIs(t, err, tc.expectedError)
356+
})
357+
}
358+
359+
}

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

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

7881
// Types used to abstract stdlib functions
7982
type mkdirAllFunc func(name string, perm fs.FileMode) error
@@ -96,6 +99,9 @@ type Upgrader struct {
9699
extractAgentVersion func(metadata packageMetadata, upgradeVersion string) agentVersion
97100
copyActionStore copyActionStoreFunc
98101
copyRunDirectory copyRunDirectoryFunc
102+
markUpgrade markUpgradeFunc
103+
changeSymlink changeSymlinkFunc
104+
rollbackInstall rollbackInstallFunc
99105
}
100106

101107
// IsUpgradeable when agent is installed and running as a service or flag was provided.
@@ -119,6 +125,9 @@ func NewUpgrader(log *logger.Logger, settings *artifact.Config, agentInfo info.A
119125
extractAgentVersion: extractAgentVersion,
120126
copyActionStore: copyActionStoreProvider(os.ReadFile, os.WriteFile),
121127
copyRunDirectory: copyRunDirectoryProvider(os.MkdirAll, copy.Copy),
128+
markUpgrade: markUpgradeProvider(UpdateActiveCommit, os.WriteFile),
129+
changeSymlink: changeSymlink,
130+
rollbackInstall: rollbackInstall,
122131
}, nil
123132
}
124133

@@ -350,9 +359,9 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, sourceURI string
350359
return nil, fmt.Errorf("calculating home path relative to top, home: %q top: %q : %w", paths.Home(), paths.Top(), err)
351360
}
352361

353-
if err := changeSymlink(u.log, paths.Top(), symlinkPath, newPath); err != nil {
362+
if err := u.changeSymlink(u.log, paths.Top(), symlinkPath, newPath); err != nil {
354363
u.log.Errorw("Rolling back: changing symlink failed", "error.message", err)
355-
rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
364+
rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
356365
return nil, goerrors.Join(err, rollbackErr)
357366
}
358367

@@ -375,13 +384,13 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, sourceURI string
375384
versionedHome: currentVersionedHome,
376385
}
377386

378-
if err := markUpgrade(u.log,
387+
if err := u.markUpgrade(u.log,
379388
paths.Data(), // data dir to place the marker in
380389
current, // new agent version data
381390
previous, // old agent version data
382391
action, det, OUTCOME_UPGRADE); err != nil {
383392
u.log.Errorw("Rolling back: marking upgrade failed", "error.message", err)
384-
rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
393+
rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
385394
return nil, goerrors.Join(err, rollbackErr)
386395
}
387396

@@ -390,14 +399,14 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, sourceURI string
390399
var watcherCmd *exec.Cmd
391400
if watcherCmd, err = InvokeWatcher(u.log, watcherExecutable); err != nil {
392401
u.log.Errorw("Rolling back: starting watcher failed", "error.message", err)
393-
rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
402+
rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
394403
return nil, goerrors.Join(err, rollbackErr)
395404
}
396405

397406
watcherWaitErr := waitForWatcher(ctx, u.log, markerFilePath(paths.Data()), watcherMaxWaitTime)
398407
if watcherWaitErr != nil {
399408
killWatcherErr := watcherCmd.Process.Kill()
400-
rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
409+
rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome)
401410
return nil, goerrors.Join(watcherWaitErr, killWatcherErr, rollbackErr)
402411
}
403412

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,81 @@ func TestUpgradeErrorHandling(t *testing.T) {
14381438
}
14391439
},
14401440
},
1441+
"should return error if changeSymlink fails": {
1442+
isDiskSpaceErrorResult: false,
1443+
expectedError: testError,
1444+
upgraderMocker: func(upgrader *Upgrader) {
1445+
upgrader.artifactDownloader = &mockArtifactDownloader{}
1446+
upgrader.extractAgentVersion = func(metadata packageMetadata, upgradeVersion string) agentVersion {
1447+
return agentVersion{
1448+
version: upgradeVersion,
1449+
snapshot: false,
1450+
hash: metadata.hash,
1451+
}
1452+
}
1453+
upgrader.unpacker = &mockUnpacker{
1454+
returnPackageMetadata: packageMetadata{
1455+
manifest: &v1.PackageManifest{},
1456+
hash: "hash",
1457+
},
1458+
returnUnpackResult: UnpackResult{
1459+
Hash: "hash",
1460+
VersionedHome: "versionedHome",
1461+
},
1462+
}
1463+
upgrader.copyActionStore = func(log *logger.Logger, newHome string) error {
1464+
return nil
1465+
}
1466+
upgrader.copyRunDirectory = func(log *logger.Logger, oldRunPath, newRunPath string) error {
1467+
return nil
1468+
}
1469+
upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error {
1470+
return nil
1471+
}
1472+
upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error {
1473+
return testError
1474+
}
1475+
},
1476+
},
1477+
"should return error if markUpgrade fails": {
1478+
isDiskSpaceErrorResult: false,
1479+
expectedError: testError,
1480+
upgraderMocker: func(upgrader *Upgrader) {
1481+
upgrader.artifactDownloader = &mockArtifactDownloader{}
1482+
upgrader.extractAgentVersion = func(metadata packageMetadata, upgradeVersion string) agentVersion {
1483+
return agentVersion{
1484+
version: upgradeVersion,
1485+
snapshot: false,
1486+
hash: metadata.hash,
1487+
}
1488+
}
1489+
upgrader.unpacker = &mockUnpacker{
1490+
returnPackageMetadata: packageMetadata{
1491+
manifest: &v1.PackageManifest{},
1492+
hash: "hash",
1493+
},
1494+
returnUnpackResult: UnpackResult{
1495+
Hash: "hash",
1496+
VersionedHome: "versionedHome",
1497+
},
1498+
}
1499+
upgrader.copyActionStore = func(log *logger.Logger, newHome string) error {
1500+
return nil
1501+
}
1502+
upgrader.copyRunDirectory = func(log *logger.Logger, oldRunPath, newRunPath string) error {
1503+
return nil
1504+
}
1505+
upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error {
1506+
return nil
1507+
}
1508+
upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error {
1509+
return nil
1510+
}
1511+
upgrader.markUpgrade = func(log *logger.Logger, dataDirPath string, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, desiredOutcome UpgradeOutcome) error {
1512+
return testError
1513+
}
1514+
},
1515+
},
14411516
"should add disk space error to the error chain if downloadArtifact fails with disk space error": {
14421517
isDiskSpaceErrorResult: true,
14431518
expectedError: upgradeErrors.ErrInsufficientDiskSpace,

0 commit comments

Comments
 (0)