Skip to content

Commit 394dcc6

Browse files
committed
test watcher command
Add some unit tests for watchCmd function, covering the most common cases. This has required some refactoring to properly assert side effects and interactions.
1 parent e042dd0 commit 394dcc6

File tree

16 files changed

+531
-28
lines changed

16 files changed

+531
-28
lines changed

.mockery.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,11 @@ packages:
2929
github.com/elastic/elastic-agent/internal/pkg/agent/application/info:
3030
interfaces:
3131
Agent:
32+
github.com/elastic/elastic-agent/internal/pkg/agent/cmd:
33+
interfaces:
34+
agentWatcher:
35+
config:
36+
mockname: "AgentWatcher"
37+
installationModifier:
38+
config:
39+
mockname: "InstallationModifier"

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,8 @@ func loadMarker(markerFile string) (*UpdateMarker, error) {
313313
// SaveMarker serializes and persists the given upgrade marker to disk.
314314
// For critical upgrade transitions, pass shouldFsync as true so the marker
315315
// file is immediately flushed to persistent storage.
316-
func SaveMarker(marker *UpdateMarker, shouldFsync bool) error {
317-
return saveMarkerToPath(marker, markerFilePath(paths.Data()), shouldFsync)
316+
func SaveMarker(dataDirPath string, marker *UpdateMarker, shouldFsync bool) error {
317+
return saveMarkerToPath(marker, markerFilePath(dataDirPath), shouldFsync)
318318
}
319319

320320
func saveMarkerToPath(marker *UpdateMarker, markerFile string, shouldFsync bool) error {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ func (u *Upgrader) Ack(ctx context.Context, acker acker.Acker) error {
449449

450450
marker.Acked = true
451451

452-
return SaveMarker(marker, false)
452+
return SaveMarker(paths.Data(), marker, false)
453453
}
454454

455455
func (u *Upgrader) AckAction(ctx context.Context, acker acker.Acker, action fleetapi.Action) error {

internal/pkg/agent/cmd/watch.go

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func newWatchCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command
5353
// Make sure to flush any buffered logs before we're done.
5454
defer log.Sync() //nolint:errcheck // flushing buffered logs is best effort.
5555

56-
if err := watchCmd(log, cfg); err != nil {
56+
if err := watchCmd(log, paths.Top(), cfg.Settings.Upgrade.Watcher, new(agtWatcherImpl), new(agtInstallationModifierImpl)); err != nil {
5757
log.Errorw("Watch command failed", "error.message", err)
5858
fmt.Fprintf(streams.Err, "Watch command failed: %v\n%s\n", err, troubleshootMessage())
5959
os.Exit(4)
@@ -64,21 +64,30 @@ func newWatchCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command
6464
return cmd
6565
}
6666

67-
func watchCmd(log *logp.Logger, cfg *configuration.Configuration) error {
67+
type agentWatcher interface {
68+
Watch(ctx context.Context, tilGrace, errorCheckInterval time.Duration, log *logp.Logger) error
69+
}
70+
71+
type installationModifier interface {
72+
Cleanup(log *logger.Logger, topDirPath, currentVersionedHome, currentHash string, removeMarker, keepLogs bool) error
73+
Rollback(ctx context.Context, log *logger.Logger, c client.Client, topDirPath, prevVersionedHome, prevHash string) error
74+
}
75+
76+
func watchCmd(log *logp.Logger, topDir string, cfg *configuration.UpgradeWatcherConfig, watcher agentWatcher, installModifier installationModifier) error {
6877
log.Infow("Upgrade Watcher started", "process.pid", os.Getpid(), "agent.version", version.GetAgentPackageVersion())
69-
marker, err := upgrade.LoadMarker(paths.Data())
78+
dataDir := paths.DataFrom(topDir)
79+
marker, err := upgrade.LoadMarker(dataDir)
7080
if err != nil {
7181
log.Error("failed to load marker", err)
7282
return err
7383
}
7484
if marker == nil {
7585
// no marker found we're not in upgrade process
76-
log.Infof("update marker not present at '%s'", paths.Data())
86+
log.Infof("update marker not present at '%s'", dataDir)
7787
return nil
7888
}
7989

80-
log.Infof("Loaded update marker %+v", marker)
81-
90+
log.With("marker", marker, "details", marker.Details).Info("Loaded update marker")
8291
locker := filelock.NewAppLocker(paths.Top(), watcherLockFile)
8392
if err := locker.TryLock(); err != nil {
8493
if errors.Is(err, filelock.ErrAppAlreadyRunning) {
@@ -93,14 +102,18 @@ func watchCmd(log *logp.Logger, cfg *configuration.Configuration) error {
93102
_ = locker.Unlock()
94103
}()
95104

96-
isWithinGrace, tilGrace := gracePeriod(marker, cfg.Settings.Upgrade.Watcher.GracePeriod)
97-
if !isWithinGrace || isTerminalState(marker) {
98-
log.Infof("not within grace [updatedOn %v] %v or agent have been rolled back [state: %s]", marker.UpdatedOn.String(), time.Since(marker.UpdatedOn).String(), marker.Details.State)
105+
isWithinGrace, tilGrace := gracePeriod(marker, cfg.GracePeriod)
106+
if isTerminalState(marker) || !isWithinGrace {
107+
stateString := ""
108+
if marker.Details != nil {
109+
stateString = string(marker.Details.State)
110+
}
111+
log.Infof("not within grace [updatedOn %v] %v or agent have been rolled back [state: %s]", marker.UpdatedOn.String(), time.Since(marker.UpdatedOn).String(), stateString)
99112
// if it is started outside of upgrade loop
100113
// if we're not within grace and marker is still there it might mean
101114
// that cleanup was not performed ok, cleanup everything except current version
102115
// hash is the same as hash of agent which initiated watcher.
103-
if err := upgrade.Cleanup(log, paths.Top(), paths.VersionedHome(paths.Top()), release.ShortCommit(), true, false); err != nil {
116+
if err := installModifier.Cleanup(log, paths.Top(), paths.VersionedHome(topDir), release.ShortCommit(), true, false); err != nil {
104117
log.Error("clean up of prior watcher run failed", err)
105118
}
106119
// exit nicely
@@ -109,15 +122,18 @@ func watchCmd(log *logp.Logger, cfg *configuration.Configuration) error {
109122

110123
// About to start watching the upgrade. Initialize upgrade details and save them in the
111124
// upgrade marker.
112-
upgradeDetails := initUpgradeDetails(marker, upgrade.SaveMarker, log)
125+
saveMarkerFunc := func(marker *upgrade.UpdateMarker, b bool) error {
126+
return upgrade.SaveMarker(dataDir, marker, b)
127+
}
128+
upgradeDetails := initUpgradeDetails(marker, saveMarkerFunc, log)
113129

114-
errorCheckInterval := cfg.Settings.Upgrade.Watcher.ErrorCheck.Interval
130+
errorCheckInterval := cfg.ErrorCheck.Interval
115131
ctx := context.Background()
116-
if err := watch(ctx, tilGrace, errorCheckInterval, log); err != nil {
132+
if err := watcher.Watch(ctx, tilGrace, errorCheckInterval, log); err != nil {
117133
log.Error("Error detected, proceeding to rollback: %v", err)
118134

119135
upgradeDetails.SetStateWithReason(details.StateRollback, "automatic rollback")
120-
err = upgrade.Rollback(ctx, log, client.New(), paths.Top(), marker.PrevVersionedHome, marker.PrevHash)
136+
err = installModifier.Rollback(ctx, log, client.New(), paths.Top(), marker.PrevVersionedHome, marker.PrevHash)
121137
if err != nil {
122138
log.Error("rollback failed", err)
123139
upgradeDetails.Fail(err)
@@ -135,7 +151,7 @@ func watchCmd(log *logp.Logger, cfg *configuration.Configuration) error {
135151
// Why is this being skipped on Windows? The comment above is not clear.
136152
// issue: https://github.com/elastic/elastic-agent/issues/3027
137153
removeMarker := !isWindows()
138-
err = upgrade.Cleanup(log, paths.Top(), marker.VersionedHome, marker.Hash, removeMarker, false)
154+
err = installModifier.Cleanup(log, topDir, marker.VersionedHome, marker.Hash, removeMarker, false)
139155
if err != nil {
140156
log.Error("cleanup after successful watch failed", err)
141157
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 cmd
6+
7+
import (
8+
"context"
9+
"time"
10+
11+
"github.com/elastic/elastic-agent-libs/logp"
12+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade"
13+
"github.com/elastic/elastic-agent/pkg/control/v2/client"
14+
"github.com/elastic/elastic-agent/pkg/core/logger"
15+
)
16+
17+
type agtWatcherImpl struct{}
18+
19+
func (a agtWatcherImpl) Watch(ctx context.Context, tilGrace, errorCheckInterval time.Duration, log *logp.Logger) error {
20+
return watch(ctx, tilGrace, errorCheckInterval, log)
21+
}
22+
23+
type agtInstallationModifierImpl struct{}
24+
25+
func (a agtInstallationModifierImpl) Cleanup(log *logger.Logger, topDirPath, currentVersionedHome, currentHash string, removeMarker, keepLogs bool) error {
26+
return upgrade.Cleanup(log, topDirPath, currentVersionedHome, currentHash, removeMarker, keepLogs)
27+
}
28+
29+
func (a agtInstallationModifierImpl) Rollback(ctx context.Context, log *logger.Logger, c client.Client, topDirPath, prevVersionedHome, prevHash string) error {
30+
return upgrade.Rollback(ctx, log, c, topDirPath, prevVersionedHome, prevHash)
31+
}

internal/pkg/agent/cmd/watch_test.go

Lines changed: 211 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,28 @@
55
package cmd
66

77
import (
8+
"fmt"
9+
"os"
10+
"runtime"
811
"testing"
12+
"time"
913

14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/mock"
1016
"go.uber.org/zap/zapcore"
1117

18+
"github.com/stretchr/testify/require"
19+
20+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
1221
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
22+
"github.com/elastic/elastic-agent/internal/pkg/agent/configuration"
1323
"github.com/elastic/elastic-agent/internal/pkg/agent/errors"
1424
"github.com/elastic/elastic-agent/internal/pkg/fleetapi"
25+
"github.com/elastic/elastic-agent/internal/pkg/release"
1526
"github.com/elastic/elastic-agent/pkg/core/logger/loggertest"
1627

17-
"github.com/stretchr/testify/require"
18-
1928
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade"
29+
cmdmocks "github.com/elastic/elastic-agent/testing/mocks/internal_/pkg/agent/cmd"
2030
)
2131

2232
func TestInitUpgradeDetails(t *testing.T) {
@@ -74,3 +84,202 @@ func TestInitUpgradeDetails(t *testing.T) {
7484
require.Equal(t, zapcore.ErrorLevel, logs[0].Level)
7585
require.Equal(t, `unable to save upgrade marker after clearing upgrade details: some error`, logs[0].Message)
7686
}
87+
88+
func Test_watchCmd(t *testing.T) {
89+
type args struct {
90+
cfg *configuration.UpgradeWatcherConfig
91+
}
92+
tests := []struct {
93+
name string
94+
setupUpgradeMarker func(t *testing.T, tmpDir string, watcher *cmdmocks.AgentWatcher, installModifier *cmdmocks.InstallationModifier)
95+
args args
96+
wantErr assert.ErrorAssertionFunc
97+
}{
98+
{
99+
name: "no upgrade marker, no party",
100+
setupUpgradeMarker: func(t *testing.T, topDir string, watcher *cmdmocks.AgentWatcher, installModifier *cmdmocks.InstallationModifier) {
101+
dataDirPath := paths.DataFrom(topDir)
102+
err := os.MkdirAll(dataDirPath, 0755)
103+
require.NoError(t, err)
104+
},
105+
args: args{
106+
cfg: configuration.DefaultUpgradeConfig().Watcher,
107+
},
108+
wantErr: assert.NoError,
109+
},
110+
{
111+
name: "happy path: no error watching, cleanup prev install",
112+
setupUpgradeMarker: func(t *testing.T, topDir string, watcher *cmdmocks.AgentWatcher, installModifier *cmdmocks.InstallationModifier) {
113+
dataDirPath := paths.DataFrom(topDir)
114+
err := os.MkdirAll(dataDirPath, 0755)
115+
require.NoError(t, err)
116+
err = upgrade.SaveMarker(
117+
dataDirPath,
118+
&upgrade.UpdateMarker{
119+
Version: "4.5.6",
120+
Hash: "newver",
121+
VersionedHome: "elastic-agent-4.5.6-newver",
122+
UpdatedOn: time.Now(),
123+
PrevVersion: "1.2.3",
124+
PrevHash: "prvver",
125+
PrevVersionedHome: "elastic-agent-prvver",
126+
Acked: false,
127+
Action: nil,
128+
Details: nil, //details.NewDetails("4.5.6", details.StateReplacing, ""),
129+
DesiredOutcome: upgrade.OUTCOME_UPGRADE,
130+
},
131+
true,
132+
)
133+
require.NoError(t, err)
134+
135+
watcher.EXPECT().
136+
Watch(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
137+
Return(nil)
138+
139+
expectedRemoveMarkerFlag := true
140+
if runtime.GOOS == "windows" {
141+
// on windows the marker is not removed immediately to allow for cleanup on restart
142+
expectedRemoveMarkerFlag = false
143+
}
144+
installModifier.EXPECT().
145+
Cleanup(mock.Anything, topDir, "elastic-agent-4.5.6-newver", "newver", expectedRemoveMarkerFlag, false).
146+
Return(nil)
147+
},
148+
args: args{
149+
cfg: configuration.DefaultUpgradeConfig().Watcher,
150+
},
151+
wantErr: assert.NoError,
152+
},
153+
{
154+
name: "unhappy path: error watching, rollback to previous install",
155+
setupUpgradeMarker: func(t *testing.T, topDir string, watcher *cmdmocks.AgentWatcher, installModifier *cmdmocks.InstallationModifier) {
156+
dataDirPath := paths.DataFrom(topDir)
157+
err := os.MkdirAll(dataDirPath, 0755)
158+
require.NoError(t, err)
159+
err = upgrade.SaveMarker(
160+
dataDirPath,
161+
&upgrade.UpdateMarker{
162+
Version: "4.5.6",
163+
Hash: "newver",
164+
VersionedHome: "elastic-agent-4.5.6-newver",
165+
UpdatedOn: time.Now(),
166+
PrevVersion: "1.2.3",
167+
PrevHash: "prvver",
168+
PrevVersionedHome: "elastic-agent-prvver",
169+
Acked: false,
170+
Action: nil,
171+
Details: nil, //details.NewDetails("4.5.6", details.StateReplacing, ""),
172+
DesiredOutcome: upgrade.OUTCOME_UPGRADE,
173+
},
174+
true,
175+
)
176+
require.NoError(t, err)
177+
178+
watcher.EXPECT().
179+
Watch(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
180+
Return(errors.New("some watch error due to agent misbehaving"))
181+
installModifier.EXPECT().
182+
Rollback(mock.Anything, mock.Anything, mock.Anything, paths.Top(), "elastic-agent-prvver", "prvver").
183+
Return(nil)
184+
},
185+
args: args{
186+
cfg: configuration.DefaultUpgradeConfig().Watcher,
187+
},
188+
wantErr: assert.NoError,
189+
},
190+
{
191+
name: "upgrade rolled back: no watching, cleanup must be called",
192+
setupUpgradeMarker: func(t *testing.T, topDir string, watcher *cmdmocks.AgentWatcher, installModifier *cmdmocks.InstallationModifier) {
193+
dataDirPath := paths.DataFrom(topDir)
194+
err := os.MkdirAll(dataDirPath, 0755)
195+
require.NoError(t, err)
196+
err = upgrade.SaveMarker(
197+
dataDirPath,
198+
&upgrade.UpdateMarker{
199+
Version: "4.5.6",
200+
Hash: "newver",
201+
VersionedHome: "elastic-agent-4.5.6-newver",
202+
UpdatedOn: time.Now(),
203+
PrevVersion: "1.2.3",
204+
PrevHash: "prvver",
205+
PrevVersionedHome: "elastic-agent-prvver",
206+
Acked: false,
207+
Action: nil,
208+
Details: &details.Details{
209+
TargetVersion: "4.5.6",
210+
State: details.StateRollback,
211+
Metadata: details.Metadata{
212+
Reason: "automatic rollback",
213+
},
214+
},
215+
DesiredOutcome: upgrade.OUTCOME_UPGRADE,
216+
},
217+
true,
218+
)
219+
require.NoError(t, err)
220+
// topdir, prevVersionedHome and prevHash are not taken from the upgrade marker, otherwise they would be
221+
// <topDir, "topDir/data/elastic-agent-prvver", "prvver">
222+
installModifier.EXPECT().
223+
Cleanup(mock.Anything, paths.Top(), paths.VersionedHome(topDir), release.ShortCommit(), true, false).
224+
Return(nil)
225+
},
226+
args: args{
227+
cfg: configuration.DefaultUpgradeConfig().Watcher,
228+
},
229+
wantErr: assert.NoError,
230+
},
231+
{
232+
name: "after grace period: no watching, cleanup must be called",
233+
setupUpgradeMarker: func(t *testing.T, topDir string, watcher *cmdmocks.AgentWatcher, installModifier *cmdmocks.InstallationModifier) {
234+
dataDirPath := paths.DataFrom(topDir)
235+
err := os.MkdirAll(dataDirPath, 0755)
236+
require.NoError(t, err)
237+
updatedOn := time.Now().Add(-5 * time.Minute)
238+
err = upgrade.SaveMarker(
239+
dataDirPath,
240+
&upgrade.UpdateMarker{
241+
Version: "4.5.6",
242+
Hash: "newver",
243+
VersionedHome: "elastic-agent-4.5.6-newver",
244+
UpdatedOn: updatedOn,
245+
PrevVersion: "1.2.3",
246+
PrevHash: "prvver",
247+
PrevVersionedHome: "elastic-agent-prvver",
248+
Acked: false,
249+
Action: nil,
250+
Details: nil,
251+
DesiredOutcome: upgrade.OUTCOME_UPGRADE,
252+
},
253+
true,
254+
)
255+
require.NoError(t, err)
256+
257+
// topdir, prevVersionedHome and prevHash are not taken from the upgrade marker, otherwise they would be
258+
// <topDir, "topDir/data/elastic-agent-prvver", "prvver">
259+
installModifier.EXPECT().
260+
Cleanup(mock.Anything, paths.Top(), paths.VersionedHome(topDir), release.ShortCommit(), true, false).
261+
Return(nil)
262+
},
263+
args: args{
264+
cfg: &configuration.UpgradeWatcherConfig{
265+
GracePeriod: 2 * time.Minute,
266+
ErrorCheck: configuration.UpgradeWatcherCheckConfig{
267+
Interval: time.Second,
268+
},
269+
},
270+
},
271+
wantErr: assert.NoError,
272+
},
273+
}
274+
for _, tt := range tests {
275+
t.Run(tt.name, func(t *testing.T) {
276+
log, obs := loggertest.New(t.Name())
277+
tmpDir := t.TempDir()
278+
mockWatcher := cmdmocks.NewAgentWatcher(t)
279+
mockInstallModifier := cmdmocks.NewInstallationModifier(t)
280+
tt.setupUpgradeMarker(t, tmpDir, mockWatcher, mockInstallModifier)
281+
tt.wantErr(t, watchCmd(log, tmpDir, tt.args.cfg, mockWatcher, mockInstallModifier), fmt.Sprintf("watchCmd(%v, ...)", tt.args.cfg))
282+
t.Logf("watchCmd logs:\n%v", obs.All())
283+
})
284+
}
285+
}

0 commit comments

Comments
 (0)