@@ -7,6 +7,9 @@ package application
77import (
88 "context"
99 "fmt"
10+ "os"
11+ "path/filepath"
12+ "runtime"
1013 "testing"
1114 "time"
1215
@@ -15,6 +18,9 @@ import (
1518
1619 "github.com/elastic/elastic-agent-libs/logp"
1720 "github.com/elastic/elastic-agent/internal/pkg/agent/application/info"
21+ "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
22+ "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade"
23+ "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details"
1824 "github.com/elastic/elastic-agent/internal/pkg/config"
1925 "github.com/elastic/elastic-agent/internal/pkg/testutils"
2026 "github.com/elastic/elastic-agent/pkg/core/logger/loggertest"
@@ -302,3 +308,188 @@ func TestInjectOutputOverrides(t *testing.T) {
302308 })
303309 }
304310}
311+
312+ func Test_normalizeInstallDescriptorAtStartup (t * testing.T ) {
313+
314+ now := time .Now ()
315+ tomorrow := now .Add (24 * time .Hour )
316+ yesterday := now .Add (- 24 * time .Hour )
317+
318+ tests := []struct {
319+ name string
320+ setup func (t * testing.T , topDir string ) (* upgrade.UpdateMarker , rollbacksSource )
321+ postNormalizeAssertions func (t * testing.T , topDir string , initialUpdateMarker * upgrade.UpdateMarker )
322+ }{
323+ {
324+ name : "happy path: single install, no rollbacks, no modifications needed" ,
325+ setup : func (t * testing.T , topDir string ) (* upgrade.UpdateMarker , rollbacksSource ) {
326+ mockRollbackSource := newMockRollbacksSource (t )
327+ mockRollbackSource .EXPECT ().Get ().Return (nil , nil )
328+ return nil , mockRollbackSource
329+ },
330+
331+ postNormalizeAssertions : nil ,
332+ },
333+ {
334+ name : "Agent was manually rolled back: rolled back install is removed from the list" ,
335+ setup : func (t * testing.T , topDir string ) (* upgrade.UpdateMarker , rollbacksSource ) {
336+ newAgentInstallPath := createFakeAgentInstall (t , topDir , "4.5.6" , "newversionhash" , true )
337+ oldAgentInstallPath := createFakeAgentInstall (t , topDir , "1.2.3" , "oldversionhash" , true )
338+
339+ mockRollbackSource := newMockRollbacksSource (t )
340+ mockRollbackSource .EXPECT ().Get ().Return (map [string ]upgrade.TTLMarker {
341+ oldAgentInstallPath : {
342+ Version : "1.2.3" ,
343+ Hash : "oldversionhash" ,
344+ ValidUntil : tomorrow ,
345+ },
346+ }, nil )
347+
348+ updateMarker := & upgrade.UpdateMarker {
349+ Version : "4.5.6" ,
350+ Hash : "newversionhash" ,
351+ VersionedHome : newAgentInstallPath ,
352+ UpdatedOn : now ,
353+ PrevVersion : "1.2.3" ,
354+ PrevHash : "oldversionhash" ,
355+ PrevVersionedHome : oldAgentInstallPath ,
356+ Acked : false ,
357+ Action : nil ,
358+ Details : & details.Details {
359+ TargetVersion : "4.5.6" ,
360+ State : details .StateRollback ,
361+ ActionID : "" ,
362+ Metadata : details.Metadata {
363+ Reason : details .ReasonManualRollbackPattern ,
364+ },
365+ },
366+ }
367+
368+ // expect code to clear the rollback
369+ mockRollbackSource .EXPECT ().Set (map [string ]upgrade.TTLMarker {}).Return (nil )
370+ return updateMarker , mockRollbackSource
371+ },
372+ postNormalizeAssertions : nil ,
373+ },
374+ {
375+ name : "Entries not having a matching install directory will be removed from the list" ,
376+ setup : func (t * testing.T , topDir string ) (* upgrade.UpdateMarker , rollbacksSource ) {
377+ _ = createFakeAgentInstall (t , topDir , "4.5.6" , "newversionhash" , true )
378+ oldAgentInstallPath := createFakeAgentInstall (t , topDir , "1.2.3" , "oldversionhash" , true )
379+
380+ mockRollbackSource := newMockRollbacksSource (t )
381+ nonExistingVersionedHome := filepath .Join ("data" , "thisdirectorydoesnotexist" )
382+ mockRollbackSource .EXPECT ().Get ().Return (map [string ]upgrade.TTLMarker {
383+ oldAgentInstallPath : {
384+ Version : "1.2.3" ,
385+ Hash : "oldversionhash" ,
386+ ValidUntil : tomorrow ,
387+ },
388+ nonExistingVersionedHome : {
389+ Version : "0.0.0" ,
390+ Hash : "nonExistingHash" ,
391+ ValidUntil : tomorrow ,
392+ },
393+ }, nil )
394+
395+ mockRollbackSource .EXPECT ().Set (map [string ]upgrade.TTLMarker {
396+ oldAgentInstallPath : {
397+ Version : "1.2.3" ,
398+ Hash : "oldversionhash" ,
399+ ValidUntil : tomorrow ,
400+ },
401+ }).Return (nil )
402+ return nil , mockRollbackSource
403+ },
404+ postNormalizeAssertions : nil ,
405+ },
406+ {
407+ name : "Expired installs still existing on disk will be removed from the install list and removed from disk" ,
408+ setup : func (t * testing.T , topDir string ) (* upgrade.UpdateMarker , rollbacksSource ) {
409+ _ = createFakeAgentInstall (t , topDir , "4.5.6" , "newversionhash" , true )
410+ oldAgentInstallPath := createFakeAgentInstall (t , topDir , "1.2.3" , "oldversionhash" , true )
411+
412+ // assert that the versionedHome of the old install is the same we check in postNormalizeAssertions
413+ assert .Equal (t , oldAgentInstallPath , filepath .Join ("data" , "elastic-agent-1.2.3-oldver" ),
414+ "Unexpected old install versioned home. Post normalize assertions may not be working" )
415+
416+ mockRollbackSource := newMockRollbacksSource (t )
417+ mockRollbackSource .EXPECT ().Get ().Return (
418+ map [string ]upgrade.TTLMarker {
419+ oldAgentInstallPath : {
420+ Version : "1.2.3" ,
421+ Hash : "oldver" ,
422+ ValidUntil : yesterday ,
423+ },
424+ },
425+ nil ,
426+ )
427+ // expect removal of the existing ttlmarker
428+ mockRollbackSource .EXPECT ().Set (map [string ]upgrade.TTLMarker {}).Return (nil )
429+ return nil , mockRollbackSource
430+ },
431+ postNormalizeAssertions : func (t * testing.T , topDir string , _ * upgrade.UpdateMarker ) {
432+ assert .NoDirExists (t , filepath .Join (topDir , "data" , "elastic-agent-1.2.3-oldver" ))
433+ },
434+ },
435+ }
436+ for _ , tt := range tests {
437+ t .Run (tt .name , func (t * testing.T ) {
438+ logger , _ := loggertest .New (t .Name ())
439+ tmpDir := t .TempDir ()
440+ updateMarker , installSource := tt .setup (t , tmpDir )
441+ normalizeAgentInstalls (logger , tmpDir , now , updateMarker , installSource )
442+ if tt .postNormalizeAssertions != nil {
443+ tt .postNormalizeAssertions (t , tmpDir , updateMarker )
444+ }
445+ })
446+ }
447+ }
448+
449+ // createFakeAgentInstall (copied from the upgrade package tests) will create a mock agent install within topDir, possibly
450+ // using the version in the directory name, depending on useVersionInPath it MUST return the path to the created versionedHome
451+ // relative to topDir, to mirror what step_unpack returns
452+ func createFakeAgentInstall (t * testing.T , topDir , version , hash string , useVersionInPath bool ) string {
453+
454+ // create versioned home
455+ versionedHome := fmt .Sprintf ("elastic-agent-%s" , hash [:upgrade .HashLen ])
456+ if useVersionInPath {
457+ // use the version passed as parameter
458+ versionedHome = fmt .Sprintf ("elastic-agent-%s-%s" , version , hash [:upgrade .HashLen ])
459+ }
460+ relVersionedHomePath := filepath .Join ("data" , versionedHome )
461+ absVersionedHomePath := filepath .Join (topDir , relVersionedHomePath )
462+
463+ // recalculate the binary path and launch a mkDirAll to account for MacOS weirdness
464+ // (the extra nesting of elastic agent binary within versionedHome)
465+ absVersioneHomeBinaryPath := paths .BinaryPath (absVersionedHomePath , "" )
466+ err := os .MkdirAll (absVersioneHomeBinaryPath , 0o750 )
467+ require .NoError (t , err , "error creating fake install versioned home directory (including binary path) %q" , absVersioneHomeBinaryPath )
468+
469+ // place a few directories in the fake install
470+ absComponentsDirPath := filepath .Join (absVersionedHomePath , "components" )
471+ err = os .MkdirAll (absComponentsDirPath , 0o750 )
472+ require .NoError (t , err , "error creating fake install components directory %q" , absVersionedHomePath )
473+
474+ absLogsDirPath := filepath .Join (absVersionedHomePath , "logs" )
475+ err = os .MkdirAll (absLogsDirPath , 0o750 )
476+ require .NoError (t , err , "error creating fake install logs directory %q" , absLogsDirPath )
477+
478+ absRunDirPath := filepath .Join (absVersionedHomePath , "run" )
479+ err = os .MkdirAll (absRunDirPath , 0o750 )
480+ require .NoError (t , err , "error creating fake install run directory %q" , absRunDirPath )
481+
482+ // put some placeholder for files
483+ agentExecutableName := upgrade .AgentName
484+ if runtime .GOOS == "windows" {
485+ agentExecutableName += ".exe"
486+ }
487+ err = os .WriteFile (paths .BinaryPath (absVersionedHomePath , agentExecutableName ), []byte (fmt .Sprintf ("Placeholder for agent %s" , version )), 0o750 )
488+ require .NoErrorf (t , err , "error writing elastic agent binary placeholder %q" , agentExecutableName )
489+ fakeLogPath := filepath .Join (absLogsDirPath , "fakelog.ndjson" )
490+ err = os .WriteFile (fakeLogPath , []byte (fmt .Sprintf ("Sample logs for agent %s" , version )), 0o750 )
491+ require .NoErrorf (t , err , "error writing fake log placeholder %q" , fakeLogPath )
492+
493+ // return the path relative to top exactly like the step_unpack does
494+ return relVersionedHomePath
495+ }
0 commit comments