Skip to content

Commit 29c1a6b

Browse files
committed
fixup! WIP - normalize install descriptors at startup
1 parent bed26f5 commit 29c1a6b

File tree

12 files changed

+361
-58
lines changed

12 files changed

+361
-58
lines changed

internal/pkg/agent/application/application.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,13 +340,20 @@ func normalizeInstallDescriptorAtStartup(log *logger.Logger, topDir string, now
340340
log.Warnf("Error getting install descriptor from installDescriptorSource during startup check: %s", err)
341341
return
342342
}
343+
343344
if installDescriptor == nil {
344345
log.Warnf("Got a nil install descriptor from installDescriptorSource during startup check, skipping")
345346
return
346347
}
347348

348349
versionedHomesToCleanup := []string{}
349350
for _, installDesc := range installDescriptor.AgentInstalls {
351+
352+
if installDesc.Active || filepath.Join(topDir, installDesc.VersionedHome) == paths.HomeFrom(topDir) {
353+
// skip the current install
354+
continue
355+
}
356+
350357
versionedHomeAbsPath := filepath.Join(topDir, installDesc.VersionedHome)
351358
_, err = os.Stat(versionedHomeAbsPath)
352359
if errors.Is(err, os.ErrNotExist) {
@@ -360,18 +367,14 @@ func normalizeInstallDescriptorAtStartup(log *logger.Logger, topDir string, now
360367
continue
361368
}
362369

363-
if installDesc.Active || filepath.Join(topDir, installDesc.VersionedHome) == paths.HomeFrom(topDir) {
364-
// skip the current install
365-
continue
366-
}
367-
368370
if installDesc.TTL != nil && now.After(*installDesc.TTL) {
369371
// the install directory exists but it's expired. Remove the files and add the versioned home to the entries to remove on the install descriptor
370372
log.Infof("agent install descriptor %+v is expired, removing directory %q", installDesc, versionedHomeAbsPath)
371373
if cleanupErr := installpkg.RemoveBut(versionedHomeAbsPath, true); cleanupErr != nil {
372374
log.Warnf("Error removing directory %q: %s", versionedHomeAbsPath, cleanupErr)
373375
} else {
374376
log.Infof("Directory %q was removed, selecting agent install %+v for removal from the install descriptor", versionedHomeAbsPath, installDesc)
377+
versionedHomesToCleanup = append(versionedHomesToCleanup, installDesc.VersionedHome)
375378
}
376379
}
377380
}

internal/pkg/agent/application/application_test.go

Lines changed: 310 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,24 @@ package application
77
import (
88
"context"
99
"fmt"
10+
"os"
11+
"path/filepath"
12+
"runtime"
1013
"testing"
1114
"time"
1215

1316
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/mock"
1418
"github.com/stretchr/testify/require"
1519

1620
"github.com/elastic/elastic-agent-libs/logp"
1721
"github.com/elastic/elastic-agent/internal/pkg/agent/application/info"
22+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
1823
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade"
24+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
1925
"github.com/elastic/elastic-agent/internal/pkg/config"
2026
"github.com/elastic/elastic-agent/internal/pkg/testutils"
27+
v1 "github.com/elastic/elastic-agent/pkg/api/v1"
2128
"github.com/elastic/elastic-agent/pkg/core/logger/loggertest"
2229
"github.com/elastic/elastic-agent/pkg/limits"
2330
)
@@ -305,26 +312,319 @@ func TestInjectOutputOverrides(t *testing.T) {
305312
}
306313

307314
func Test_normalizeInstallDescriptorAtStartup(t *testing.T) {
308-
type args struct {
309-
topDir string
310-
now time.Time
311-
initialUpdateMarker *upgrade.UpdateMarker
312-
}
315+
316+
now := time.Now()
317+
tomorrow := now.Add(24 * time.Hour)
318+
yesterday := now.Add(-24 * time.Hour)
313319

314320
tests := []struct {
315321
name string
316-
setup func(t *testing.T, topDir string, initialUpdateMarker *upgrade.UpdateMarker, installSource *mockInstallDescriptorSource)
317-
args args
322+
setup func(t *testing.T, topDir string) (*upgrade.UpdateMarker, installDescriptorSource)
318323
postNormalizeAssertions func(t *testing.T, topDir string, initialUpdateMarker *upgrade.UpdateMarker)
319324
}{
325+
{
326+
name: "happy path: single install, no modifications needed",
327+
setup: func(t *testing.T, topDir string) (*upgrade.UpdateMarker, installDescriptorSource) {
328+
mockInstallSource := newMockInstallDescriptorSource(t)
329+
mockInstallSource.EXPECT().GetInstallDesc().Return(
330+
&v1.InstallDescriptor{
331+
AgentInstalls: []v1.AgentInstallDesc{
332+
{
333+
Version: "1.2.3-current",
334+
Hash: "current",
335+
VersionedHome: filepath.Join("data", "elastic-agent-1.2.3-curren"),
336+
Flavor: "basic",
337+
Active: true,
338+
},
339+
},
340+
},
341+
nil,
342+
)
343+
return nil, mockInstallSource
344+
},
345+
346+
postNormalizeAssertions: nil,
347+
},
348+
{
349+
name: "Agent was manually rolled back: rolled back install is removed from the list",
350+
setup: func(t *testing.T, topDir string) (*upgrade.UpdateMarker, installDescriptorSource) {
351+
newAgentInstallPath := createFakeAgentInstall(t, topDir, "4.5.6", "newversionhash", true)
352+
oldAgentInstallPath := createFakeAgentInstall(t, topDir, "1.2.3", "oldversionhash", true)
353+
354+
mockInstallSource := newMockInstallDescriptorSource(t)
355+
fakeInstallDescriptor := v1.InstallDescriptor{
356+
AgentInstalls: []v1.AgentInstallDesc{
357+
{
358+
Version: "4.5.6",
359+
Hash: "newversionhash",
360+
VersionedHome: newAgentInstallPath,
361+
Flavor: "basic",
362+
Active: true,
363+
},
364+
{
365+
Version: "1.2.3",
366+
Hash: "oldversionhash",
367+
VersionedHome: oldAgentInstallPath,
368+
Flavor: "basic",
369+
OptionalTTLItem: v1.OptionalTTLItem{TTL: &tomorrow},
370+
},
371+
},
372+
}
373+
mockInstallSource.EXPECT().GetInstallDesc().Return(
374+
&fakeInstallDescriptor,
375+
nil,
376+
)
377+
updateMarker := &upgrade.UpdateMarker{
378+
Version: "4.5.6",
379+
Hash: "newversionhash",
380+
VersionedHome: newAgentInstallPath,
381+
UpdatedOn: now,
382+
PrevVersion: "1.2.3",
383+
PrevHash: "oldversionhash",
384+
PrevVersionedHome: oldAgentInstallPath,
385+
Acked: false,
386+
Action: nil,
387+
Details: &details.Details{
388+
TargetVersion: "4.5.6",
389+
State: details.StateRollback,
390+
ActionID: "",
391+
Metadata: details.Metadata{
392+
Reason: details.ReasonManualRollbackPattern,
393+
},
394+
},
395+
}
396+
397+
mockInstallSource.EXPECT().ModifyInstallDesc(mock.Anything).RunAndReturn(func(f func(*v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error) {
398+
399+
for i := range fakeInstallDescriptor.AgentInstalls {
400+
err := f(&fakeInstallDescriptor.AgentInstalls[i])
401+
assert.NoErrorf(t, err, "unexpected error while modifying install descriptor %+v", i)
402+
}
403+
404+
assert.False(t, fakeInstallDescriptor.AgentInstalls[0].Active, "install we rolled back from should be set to not active")
405+
assert.False(t, fakeInstallDescriptor.AgentInstalls[0].Active, "install we rolled back to should be set to active")
406+
return &fakeInstallDescriptor, nil
407+
})
320408

321-
// TODO: Add test cases.
409+
// returned modified install descriptor content is not important
410+
mockInstallSource.EXPECT().RemoveAgentInstallDesc(newAgentInstallPath).Return(&fakeInstallDescriptor, nil)
411+
412+
return updateMarker, mockInstallSource
413+
},
414+
postNormalizeAssertions: nil,
415+
},
416+
{
417+
name: "Entries not having a matching install directory will be removed from the list",
418+
setup: func(t *testing.T, topDir string) (*upgrade.UpdateMarker, installDescriptorSource) {
419+
newAgentInstallPath := createFakeAgentInstall(t, topDir, "4.5.6", "newversionhash", true)
420+
oldAgentInstallPath := createFakeAgentInstall(t, topDir, "1.2.3", "oldversionhash", true)
421+
422+
mockInstallSource := newMockInstallDescriptorSource(t)
423+
nonExistingVersionedHome := filepath.Join("data", "thisdirectorydoesnotexist")
424+
fakeInstallDescriptor := v1.InstallDescriptor{
425+
AgentInstalls: []v1.AgentInstallDesc{
426+
{
427+
Version: "4.5.6",
428+
Hash: "currentVersionHash",
429+
VersionedHome: newAgentInstallPath,
430+
Flavor: "basic",
431+
Active: true,
432+
},
433+
{
434+
Version: "1.2.3",
435+
Hash: "oldversionhash",
436+
VersionedHome: oldAgentInstallPath,
437+
Flavor: "basic",
438+
OptionalTTLItem: v1.OptionalTTLItem{TTL: &tomorrow},
439+
},
440+
{
441+
Version: "0.0.0",
442+
Hash: "nonExistingHash",
443+
VersionedHome: nonExistingVersionedHome,
444+
Flavor: "basic",
445+
},
446+
},
447+
}
448+
mockInstallSource.EXPECT().GetInstallDesc().Return(
449+
&fakeInstallDescriptor,
450+
nil,
451+
)
452+
453+
// returned modified install descriptor content is not important
454+
mockInstallSource.EXPECT().RemoveAgentInstallDesc(nonExistingVersionedHome).Return(&fakeInstallDescriptor, nil)
455+
456+
return nil, mockInstallSource
457+
},
458+
postNormalizeAssertions: nil,
459+
},
460+
{
461+
name: "Expired installs still existing on disk will be removed from the install list and removed from disk",
462+
setup: func(t *testing.T, topDir string) (*upgrade.UpdateMarker, installDescriptorSource) {
463+
newAgentInstallPath := createFakeAgentInstall(t, topDir, "4.5.6", "newversionhash", true)
464+
oldAgentInstallPath := createFakeAgentInstall(t, topDir, "1.2.3", "oldversionhash", true)
465+
466+
// assert that the versionedHome of the old install is the same we check in postNormalizeAssertions
467+
assert.Equal(t, oldAgentInstallPath, filepath.Join("data", "elastic-agent-1.2.3-oldver"),
468+
"Unexpected old install versioned home. Post normalize assertions may not be working")
469+
470+
mockInstallSource := newMockInstallDescriptorSource(t)
471+
fakeInstallDescriptor := v1.InstallDescriptor{
472+
AgentInstalls: []v1.AgentInstallDesc{
473+
{
474+
Version: "4.5.6",
475+
Hash: "newversionhash",
476+
VersionedHome: newAgentInstallPath,
477+
Flavor: "basic",
478+
Active: true,
479+
},
480+
{
481+
Version: "1.2.3",
482+
Hash: "oldversionhash",
483+
VersionedHome: oldAgentInstallPath,
484+
Flavor: "basic",
485+
OptionalTTLItem: v1.OptionalTTLItem{TTL: &yesterday},
486+
},
487+
},
488+
}
489+
mockInstallSource.EXPECT().GetInstallDesc().Return(
490+
&fakeInstallDescriptor,
491+
nil,
492+
)
493+
494+
mockInstallSource.EXPECT().RemoveAgentInstallDesc(oldAgentInstallPath).Return(&fakeInstallDescriptor, nil)
495+
496+
return nil, mockInstallSource
497+
},
498+
postNormalizeAssertions: func(t *testing.T, topDir string, _ *upgrade.UpdateMarker) {
499+
assert.NoDirExists(t, filepath.Join(topDir, "data", "elastic-agent-1.2.3-oldver"))
500+
},
501+
},
502+
{
503+
name: "If a directory cannot be checked, the entry is left alone in the installDescriptor (with a warning in the logs)",
504+
setup: func(t *testing.T, topDir string) (*upgrade.UpdateMarker, installDescriptorSource) {
505+
506+
if runtime.GOOS == "windows" {
507+
t.Skipf("This test rely on permission settings not available on Windows")
508+
}
509+
510+
newAgentInstallPath := createFakeAgentInstall(t, topDir, "4.5.6", "newversionhash", true)
511+
oldAgentInstallPath := createFakeAgentInstall(t, topDir, "1.2.3", "oldversionhash", true)
512+
513+
// assert that the versionedHome of the old install is the same we check in postNormalizeAssertions
514+
assert.Equal(t, oldAgentInstallPath, filepath.Join("data", "elastic-agent-1.2.3-oldver"),
515+
"Unexpected old install versioned home. Post normalize assertions may not be working")
516+
517+
// make `data` unreadable
518+
dataDir := paths.DataFrom(topDir)
519+
dataStat, err := os.Stat(dataDir)
520+
require.NoError(t, err, "data should be accessible")
521+
err = os.Chmod(dataDir, 0o222)
522+
require.NoError(t, err, "Error making data unreadable")
523+
524+
//restore data permissions on test exit
525+
t.Cleanup(func() {
526+
cleanupErr := os.Chmod(dataDir, dataStat.Mode())
527+
assert.NoError(t, cleanupErr, "error restoring data permissions")
528+
})
529+
530+
_, err = os.Stat(filepath.Join(topDir, newAgentInstallPath))
531+
require.Errorf(t, err, "os.Stat on %s shoud not be successful", newAgentInstallPath)
532+
533+
_, err = os.Stat(filepath.Join(topDir, oldAgentInstallPath))
534+
require.Errorf(t, err, "os.Stat on %s shoud not be successful", oldAgentInstallPath)
535+
536+
mockInstallSource := newMockInstallDescriptorSource(t)
537+
fakeInstallDescriptor := v1.InstallDescriptor{
538+
AgentInstalls: []v1.AgentInstallDesc{
539+
{
540+
Version: "4.5.6",
541+
Hash: "newversionhash",
542+
VersionedHome: newAgentInstallPath,
543+
Flavor: "basic",
544+
Active: true,
545+
},
546+
{
547+
Version: "1.2.3",
548+
Hash: "oldversionhash",
549+
VersionedHome: oldAgentInstallPath,
550+
Flavor: "basic",
551+
OptionalTTLItem: v1.OptionalTTLItem{TTL: &yesterday},
552+
},
553+
},
554+
}
555+
mockInstallSource.EXPECT().GetInstallDesc().Return(
556+
&fakeInstallDescriptor,
557+
nil,
558+
)
559+
560+
return nil, mockInstallSource
561+
},
562+
postNormalizeAssertions: func(t *testing.T, topDir string, _ *upgrade.UpdateMarker) {
563+
// make data readable again
564+
dataDir := paths.DataFrom(topDir)
565+
err := os.Chmod(dataDir, 0o755)
566+
require.NoError(t, err, "error reopening data permissions")
567+
assert.DirExists(t, filepath.Join(topDir, "data", "elastic-agent-1.2.3-oldver"))
568+
},
569+
},
322570
}
323571
for _, tt := range tests {
324572
t.Run(tt.name, func(t *testing.T) {
325573
logger, _ := loggertest.New(t.Name())
326-
installSource := newMockInstallDescriptorSource(t)
327-
normalizeInstallDescriptorAtStartup(logger, tt.args.topDir, tt.args.now, tt.args.initialUpdateMarker, installSource)
574+
tmpDir := t.TempDir()
575+
updateMarker, installSource := tt.setup(t, tmpDir)
576+
normalizeInstallDescriptorAtStartup(logger, tmpDir, now, updateMarker, installSource)
577+
if tt.postNormalizeAssertions != nil {
578+
tt.postNormalizeAssertions(t, tmpDir, updateMarker)
579+
}
328580
})
329581
}
330582
}
583+
584+
// createFakeAgentInstall (copied from the upgrade package tests) will create a mock agent install within topDir, possibly
585+
// using the version in the directory name, depending on useVersionInPath it MUST return the path to the created versionedHome
586+
// relative to topDir, to mirror what step_unpack returns
587+
func createFakeAgentInstall(t *testing.T, topDir, version, hash string, useVersionInPath bool) string {
588+
589+
// create versioned home
590+
versionedHome := fmt.Sprintf("elastic-agent-%s", hash[:upgrade.HashLen])
591+
if useVersionInPath {
592+
// use the version passed as parameter
593+
versionedHome = fmt.Sprintf("elastic-agent-%s-%s", version, hash[:upgrade.HashLen])
594+
}
595+
relVersionedHomePath := filepath.Join("data", versionedHome)
596+
absVersionedHomePath := filepath.Join(topDir, relVersionedHomePath)
597+
598+
// recalculate the binary path and launch a mkDirAll to account for MacOS weirdness
599+
// (the extra nesting of elastic agent binary within versionedHome)
600+
absVersioneHomeBinaryPath := paths.BinaryPath(absVersionedHomePath, "")
601+
err := os.MkdirAll(absVersioneHomeBinaryPath, 0o750)
602+
require.NoError(t, err, "error creating fake install versioned home directory (including binary path) %q", absVersioneHomeBinaryPath)
603+
604+
// place a few directories in the fake install
605+
absComponentsDirPath := filepath.Join(absVersionedHomePath, "components")
606+
err = os.MkdirAll(absComponentsDirPath, 0o750)
607+
require.NoError(t, err, "error creating fake install components directory %q", absVersionedHomePath)
608+
609+
absLogsDirPath := filepath.Join(absVersionedHomePath, "logs")
610+
err = os.MkdirAll(absLogsDirPath, 0o750)
611+
require.NoError(t, err, "error creating fake install logs directory %q", absLogsDirPath)
612+
613+
absRunDirPath := filepath.Join(absVersionedHomePath, "run")
614+
err = os.MkdirAll(absRunDirPath, 0o750)
615+
require.NoError(t, err, "error creating fake install run directory %q", absRunDirPath)
616+
617+
// put some placeholder for files
618+
agentExecutableName := upgrade.AgentName
619+
if runtime.GOOS == "windows" {
620+
agentExecutableName += ".exe"
621+
}
622+
err = os.WriteFile(paths.BinaryPath(absVersionedHomePath, agentExecutableName), []byte(fmt.Sprintf("Placeholder for agent %s", version)), 0o750)
623+
require.NoErrorf(t, err, "error writing elastic agent binary placeholder %q", agentExecutableName)
624+
fakeLogPath := filepath.Join(absLogsDirPath, "fakelog.ndjson")
625+
err = os.WriteFile(fakeLogPath, []byte(fmt.Sprintf("Sample logs for agent %s", version)), 0o750)
626+
require.NoErrorf(t, err, "error writing fake log placeholder %q", fakeLogPath)
627+
628+
// return the path relative to top exactly like the step_unpack does
629+
return relVersionedHomePath
630+
}

0 commit comments

Comments
 (0)