@@ -7,17 +7,24 @@ package application
77import (
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
307314func 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