From 0921bec367662a48e16ef1727d1ba013f61d2126 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Fri, 29 Aug 2025 14:03:58 +0200 Subject: [PATCH 01/18] extract manifest PathMapper to its own package --- .../agent/application/upgrade/step_unpack.go | 26 +++++-------------- .../integration/ess/upgrade_rollback_test.go | 1 - 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/internal/pkg/agent/application/upgrade/step_unpack.go b/internal/pkg/agent/application/upgrade/step_unpack.go index ae0f964ee65..987bbe50a4b 100644 --- a/internal/pkg/agent/application/upgrade/step_unpack.go +++ b/internal/pkg/agent/application/upgrade/step_unpack.go @@ -24,6 +24,7 @@ import ( v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/component" "github.com/elastic/elastic-agent/pkg/core/logger" + manifestutils "github.com/elastic/elastic-agent/pkg/utils/manifest" ) // UnpackResult contains the location and hash of the unpacked agent files @@ -115,7 +116,7 @@ func unzip(log *logger.Logger, archivePath, dataDir string, flavor string, copy fileNamePrefix := strings.TrimSuffix(filepath.Base(archivePath), ".zip") + "/" // omitting `elastic-agent-{version}-{os}-{arch}/` in filename - pm := pathMapper{} + var pm *manifestutils.PathMapper var versionedHome string metadata, err := getPackageMetadataFromZipReader(r, fileNamePrefix) @@ -126,10 +127,11 @@ func unzip(log *logger.Logger, archivePath, dataDir string, flavor string, copy hash = metadata.hash[:hashLen] var registry map[string][]string if metadata.manifest != nil { - pm.mappings = metadata.manifest.Package.PathMappings + pm = manifestutils.NewPathMapper(metadata.manifest.Package.PathMappings) versionedHome = filepath.FromSlash(pm.Map(metadata.manifest.Package.VersionedHome)) registry = metadata.manifest.Package.Flavors } else { + pm = manifestutils.NewPathMapper(nil) // if at this point we didn't load the manifest, set the versioned to the backup value versionedHome = createVersionedHomeFromHash(hash) } @@ -361,7 +363,7 @@ func untar(log *logger.Logger, archivePath, dataDir string, flavor string, copy var hash string // Look up manifest in the archive and prepare path mappings, if any - pm := pathMapper{} + var pm *manifestutils.PathMapper metadata, err := getPackageMetadataFromTar(archivePath) if err != nil { @@ -373,10 +375,11 @@ func untar(log *logger.Logger, archivePath, dataDir string, flavor string, copy if metadata.manifest != nil { // set the path mappings - pm.mappings = metadata.manifest.Package.PathMappings + pm = manifestutils.NewPathMapper(metadata.manifest.Package.PathMappings) versionedHome = filepath.FromSlash(pm.Map(metadata.manifest.Package.VersionedHome)) registry = metadata.manifest.Package.Flavors } else { + pm = manifestutils.NewPathMapper(nil) // set default value of versioned home if it wasn't set by reading the manifest versionedHome = createVersionedHomeFromHash(metadata.hash) } @@ -661,21 +664,6 @@ func validFileName(p string) bool { return true } -type pathMapper struct { - mappings []map[string]string -} - -func (pm pathMapper) Map(packagePath string) string { - for _, mapping := range pm.mappings { - for pkgPath, mappedPath := range mapping { - if strings.HasPrefix(packagePath, pkgPath) { - return path.Join(mappedPath, packagePath[len(pkgPath):]) - } - } - } - return packagePath -} - type tarCloser struct { tarFile *os.File gzipReader *gzip.Reader diff --git a/testing/integration/ess/upgrade_rollback_test.go b/testing/integration/ess/upgrade_rollback_test.go index 71e94ff2cc2..75f065a6d14 100644 --- a/testing/integration/ess/upgrade_rollback_test.go +++ b/testing/integration/ess/upgrade_rollback_test.go @@ -229,7 +229,6 @@ func TestStandaloneUpgradeRollbackOnRestarts(t *testing.T) { atesting.WithFetcher(atesting.ArtifactFetcher()), ) require.NoError(t, err) - return fromFixture, toFixture }, }, From 5f5dadca28b88ff86dabcfbf0c698c79c724b644 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Fri, 29 Aug 2025 14:58:12 +0200 Subject: [PATCH 02/18] add install descriptor during initial install --- internal/pkg/agent/cmd/run.go | 2 +- internal/pkg/agent/install/install.go | 43 ++++++++++++++++------ internal/pkg/agent/install/install_test.go | 16 +++++++- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index bc386c7263e..611e4debbc4 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -777,7 +777,7 @@ func ensureInstallMarkerPresent() error { if err != nil { return fmt.Errorf("failed to get current file owner: %w", err) } - if err := install.CreateInstallMarker(paths.Top(), ownership); err != nil { + if err := install.CreateInstallMarker(paths.Top(), ownership, paths.Home(), version.GetAgentPackageVersion()); err != nil { return fmt.Errorf("unable to create installation marker file during upgrade: %w", err) } diff --git a/internal/pkg/agent/install/install.go b/internal/pkg/agent/install/install.go index 97ae6315c5f..11a73519554 100644 --- a/internal/pkg/agent/install/install.go +++ b/internal/pkg/agent/install/install.go @@ -17,6 +17,7 @@ import ( "github.com/kardianos/service" "github.com/otiai10/copy" "github.com/schollz/progressbar/v3" + "gopkg.in/yaml.v3" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" @@ -25,6 +26,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/cli" v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/utils" + manifestutils "github.com/elastic/elastic-agent/pkg/utils/manifest" ) const ( @@ -61,17 +63,20 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p } } - err = setupInstallPath(topPath, ownership) - if err != nil { - return utils.FileOwner{}, fmt.Errorf("error setting up install path: %w", err) - } - manifest, err := readPackageManifest(dir) if err != nil { return utils.FileOwner{}, fmt.Errorf("reading package manifest: %w", err) } pathMappings := manifest.Package.PathMappings + pathMapper := manifestutils.NewPathMapper(pathMappings) + + targetVersionedHome := filepath.FromSlash(pathMapper.Map(manifest.Package.VersionedHome)) + + err = setupInstallPath(topPath, ownership, targetVersionedHome, manifest.Package.Version) + if err != nil { + return utils.FileOwner{}, fmt.Errorf("error setting up install path: %w", err) + } pt.Describe("Copying install files") copyConcurrency := calculateCopyConcurrency(streams) @@ -184,7 +189,7 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p } // setup the basic topPath, and the .installed file -func setupInstallPath(topPath string, ownership utils.FileOwner) error { +func setupInstallPath(topPath string, ownership utils.FileOwner, versionedHome string, version string) error { // ensure parent directory exists err := os.MkdirAll(filepath.Dir(topPath), 0755) if err != nil { @@ -198,7 +203,7 @@ func setupInstallPath(topPath string, ownership utils.FileOwner) error { } // create the install marker - if err := CreateInstallMarker(topPath, ownership); err != nil { + if err := CreateInstallMarker(topPath, ownership, versionedHome, version); err != nil { return fmt.Errorf("failed to create install marker: %w", err) } return nil @@ -516,16 +521,32 @@ func hasAllSSDs(block ghw.BlockInfo) bool { // CreateInstallMarker creates a `.installed` file at the given install path, // and then calls fixInstallMarkerPermissions to set the ownership provided by `ownership` -func CreateInstallMarker(topPath string, ownership utils.FileOwner) error { +func CreateInstallMarker(topPath string, ownership utils.FileOwner, home string, version string) error { markerFilePath := filepath.Join(topPath, paths.MarkerFileName) - handle, err := os.Create(markerFilePath) + err := createInstallMarkerFile(markerFilePath, version, home) if err != nil { - return err + return fmt.Errorf("creating install marker: %w", err) } - _ = handle.Close() return fixInstallMarkerPermissions(markerFilePath, ownership) } +func createInstallMarkerFile(markerFilePath string, version string, home string) error { + handle, err := os.Create(markerFilePath) + if err != nil { + return fmt.Errorf("creating destination file %q : %w", markerFilePath, err) + } + defer func() { + _ = handle.Close() + }() + installDescriptor := v1.NewInstallDescriptor() + installDescriptor.AgentInstalls = []v1.AgentInstallDesc{{Version: version, VersionedHome: home}} + err = yaml.NewEncoder(handle).Encode(installDescriptor) + if err != nil { + return fmt.Errorf("writing install descriptor: %w", err) + } + return nil +} + func UnprivilegedUser(username, password string) (string, string) { if username != "" { return username, password diff --git a/internal/pkg/agent/install/install_test.go b/internal/pkg/agent/install/install_test.go index f2716e493f6..98aea5cce10 100644 --- a/internal/pkg/agent/install/install_test.go +++ b/internal/pkg/agent/install/install_test.go @@ -224,7 +224,19 @@ func TestSetupInstallPath(t *testing.T) { tmpdir := t.TempDir() ownership, err := utils.CurrentFileOwner() require.NoError(t, err) - err = setupInstallPath(tmpdir, ownership) + err = setupInstallPath(tmpdir, ownership, "data/elastic-agent-1.2.3-SNAPSHOT", "1.2.3-SNAPSHOT") require.NoError(t, err) - require.FileExists(t, filepath.Join(tmpdir, paths.MarkerFileName)) + markerFilePath := filepath.Join(tmpdir, paths.MarkerFileName) + require.FileExists(t, markerFilePath) + + const expectedInstallDescriptor = ` + version: co.elastic.agent/v1 + kind: InstallDescriptor + agentInstalls: + - version: 1.2.3-SNAPSHOT + versioned-home: data/elastic-agent-1.2.3-SNAPSHOT + ` + actualInstallDescriptorBytes, err := os.ReadFile(markerFilePath) + require.NoError(t, err, "error reading actual install descriptor") + assert.YAMLEq(t, expectedInstallDescriptor, string(actualInstallDescriptorBytes), "expected and actual install descriptor do not match") } From d832b107f7e97b2c9b4eed41a587745a63e39379 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Fri, 5 Sep 2025 13:22:04 +0200 Subject: [PATCH 03/18] Introduce install descriptor --- .../pkg/agent/application/upgrade/upgrade.go | 77 +++++++++++++++++-- pkg/api/v1/install.go | 52 +++++++++++++ pkg/utils/manifest/path_mapper.go | 30 ++++++++ 3 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 pkg/api/v1/install.go create mode 100644 pkg/utils/manifest/path_mapper.go diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index fbc2b38f414..1e07030562d 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -16,7 +16,7 @@ import ( "strings" "time" - "github.com/otiai10/copy" + filecopy "github.com/otiai10/copy" "go.elastic.co/apm/v2" "github.com/elastic/elastic-agent/internal/pkg/agent/application/filelock" @@ -35,6 +35,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/fleetapi/acker" fleetclient "github.com/elastic/elastic-agent/internal/pkg/fleetapi/client" "github.com/elastic/elastic-agent/internal/pkg/release" + v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/control/v2/client" "github.com/elastic/elastic-agent/pkg/control/v2/cproto" "github.com/elastic/elastic-agent/pkg/core/logger" @@ -89,7 +90,7 @@ type unpackHandler interface { // Types used to abstract copyActionStore, copyRunDirectory and github.com/otiai10/copy.Copy type copyActionStoreFunc func(log *logger.Logger, newHome string) error type copyRunDirectoryFunc func(log *logger.Logger, oldRunPath, newRunPath string) error -type fileDirCopyFunc func(from, to string, opts ...copy.Options) error +type fileDirCopyFunc func(from, to string, opts ...filecopy.Options) error type markUpgradeFunc func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, rollbackWindow time.Duration) error type changeSymlinkFunc func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error type rollbackInstallFunc func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error @@ -161,7 +162,7 @@ func NewUpgrader(log *logger.Logger, settings *artifact.Config, upgradeConfig *c isDiskSpaceErrorFunc: upgradeErrors.IsDiskSpaceError, extractAgentVersion: extractAgentVersion, copyActionStore: copyActionStoreProvider(os.ReadFile, os.WriteFile), - copyRunDirectory: copyRunDirectoryProvider(os.MkdirAll, copy.Copy), + copyRunDirectory: copyRunDirectoryProvider(os.MkdirAll, filecopy.Copy), markUpgrade: markUpgradeProvider(UpdateActiveCommit, os.WriteFile), changeSymlink: changeSymlink, rollbackInstall: rollbackInstall, @@ -400,6 +401,9 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return nil, err } + //FIXME make it nicer + err = addInstallDesc(version, unpackRes.VersionedHome, unpackRes.Hash, detectedFlavor) + newHash := unpackRes.Hash if newHash == "" { return nil, errors.New("unknown hash") @@ -498,6 +502,65 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return cb, nil } +func addInstallDesc(version string, home string, hash string, flavor string) error { + installMarkerFilePath := filepath.Join(paths.Top(), paths.MarkerFileName) + installDescriptor, err := readInstallMarker(installMarkerFilePath) + if err != nil { + return err + } + + if installDescriptor == nil { + return fmt.Errorf("no install descriptor found at %q") + } + + existingInstalls := installDescriptor.AgentInstalls + installDescriptor.AgentInstalls = make([]v1.AgentInstallDesc, len(existingInstalls)+1) + newInstall := v1.AgentInstallDesc{ + Version: version, + Hash: hash, + VersionedHome: home, + Flavor: flavor, + } + installDescriptor.AgentInstalls[0] = newInstall + copied := copy(installDescriptor.AgentInstalls[1:], existingInstalls) + if copied != len(existingInstalls) { + return fmt.Errorf("error adding new install %v to existing installs %v", newInstall, existingInstalls) + } + + err = writeInstallMarker(installMarkerFilePath, installDescriptor) + if err != nil { + return fmt.Errorf("writing updated install marker: %w", err) + } + + return nil +} + +func writeInstallMarker(markerFilePath string, descriptor *v1.InstallDescriptor) error { + installMarkerFile, err := os.Create(markerFilePath) + if err != nil { + return fmt.Errorf("opening install marker file: %w", err) + } + defer func(installMarkerFile *os.File) { + _ = installMarkerFile.Close() + }(installMarkerFile) + return v1.WriteInstallDescriptor(installMarkerFile, descriptor) +} + +func readInstallMarker(markerFilePath string) (*v1.InstallDescriptor, error) { + installMarkerFile, err := os.Open(markerFilePath) + if err != nil { + return nil, fmt.Errorf("opening install marker file: %w", err) + } + defer func(installMarkerFile *os.File) { + _ = installMarkerFile.Close() + }(installMarkerFile) + installDescriptor, err := v1.ParseInstallDescriptor(installMarkerFile) + if err != nil { + return nil, fmt.Errorf("parsing install marker file: %w", err) + } + return installDescriptor, nil +} + func (u *Upgrader) rollbackToPreviousVersion(ctx context.Context, topDir string, now time.Time, version string, action *fleetapi.ActionUpgrade) (reexec.ShutdownCallbackFn, error) { if version == "" { return nil, ErrEmptyRollbackVersion @@ -783,7 +846,7 @@ func shutdownCallback(l *logger.Logger, homePath, prevVersion, newVersion, newHo newRelPath = strings.ReplaceAll(newRelPath, oldHome, newHome) newDir := filepath.Join(newHome, newRelPath) l.Debugf("copying %q -> %q", processDir, newDir) - if err := copyDir(l, processDir, newDir, true, copy.Copy); err != nil { + if err := copyDir(l, processDir, newDir, true, filecopy.Copy); err != nil { return err } } @@ -855,9 +918,9 @@ func copyDir(l *logger.Logger, from, to string, ignoreErrs bool, fileDirCopy fil copyConcurrency = runtime.NumCPU() * 4 } - return fileDirCopy(from, to, copy.Options{ - OnSymlink: func(_ string) copy.SymlinkAction { - return copy.Shallow + return fileDirCopy(from, to, filecopy.Options{ + OnSymlink: func(_ string) filecopy.SymlinkAction { + return filecopy.Shallow }, Sync: true, OnError: onErr, diff --git a/pkg/api/v1/install.go b/pkg/api/v1/install.go new file mode 100644 index 00000000000..74ddb7cfd88 --- /dev/null +++ b/pkg/api/v1/install.go @@ -0,0 +1,52 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package v1 + +import ( + "io" + "time" + + "gopkg.in/yaml.v2" +) + +const ( + InstallDescriptorKind = "InstallDescriptor" +) + +type OptionalTTLItem struct { + TTL *time.Time `yaml:"ttl,omitempty" json:"ttl,omitempty"` +} + +type AgentInstallDesc struct { + OptionalTTLItem `yaml:",inline" json:",inline"` + Version string `yaml:"version,omitempty" json:"version,omitempty"` + Hash string `yaml:"hash,omitempty" json:"hash,omitempty"` + VersionedHome string `yaml:"versionedHome,omitempty" json:"versionedHome,omitempty"` + Flavor string `yaml:"flavor,omitempty" json:"flavor,omitempty"` +} + +type InstallDescriptor struct { + apiObject `yaml:",inline"` + AgentInstalls []AgentInstallDesc `yaml:"agentInstalls,omitempty" json:"agentInstalls,omitempty"` +} + +func NewInstallDescriptor() *InstallDescriptor { + return &InstallDescriptor{ + apiObject: apiObject{ + Version: VERSION, + Kind: InstallDescriptorKind, + }, + } +} + +func ParseInstallDescriptor(r io.Reader) (*InstallDescriptor, error) { + id := NewInstallDescriptor() + err := yaml.NewDecoder(r).Decode(id) + return id, err +} + +func WriteInstallDescriptor(w io.Writer, id *InstallDescriptor) error { + return yaml.NewEncoder(w).Encode(id) +} diff --git a/pkg/utils/manifest/path_mapper.go b/pkg/utils/manifest/path_mapper.go new file mode 100644 index 00000000000..3d28dd43d4b --- /dev/null +++ b/pkg/utils/manifest/path_mapper.go @@ -0,0 +1,30 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package manifest + +import ( + "path" + "strings" +) + +// PathMapper is a utility object that will help with File mappings specified in a v1/Manifest +type PathMapper struct { + mappings []map[string]string +} + +func (pm PathMapper) Map(packagePath string) string { + for _, mapping := range pm.mappings { + for pkgPath, mappedPath := range mapping { + if strings.HasPrefix(packagePath, pkgPath) { + return path.Join(mappedPath, packagePath[len(pkgPath):]) + } + } + } + return packagePath +} + +func NewPathMapper(mappings []map[string]string) *PathMapper { + return &PathMapper{mappings} +} From b2ed4c35e8babc2546f6cf77fde3c6a91dbf5fd7 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Mon, 15 Sep 2025 21:18:12 +0200 Subject: [PATCH 04/18] WIP - add install descriptors --- .../pkg/agent/application/upgrade/upgrade.go | 81 +++++++++++++++++-- internal/pkg/agent/cmd/run.go | 2 +- internal/pkg/agent/install/install.go | 14 ++-- internal/pkg/agent/install/install_test.go | 2 +- pkg/api/v1/install.go | 1 + 5 files changed, 83 insertions(+), 17 deletions(-) diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index 1e07030562d..9e280e8d9d9 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -67,7 +67,7 @@ var ( ErrNilUpdateMarker = errors.New("loaded a nil update marker") ErrEmptyRollbackVersion = errors.New("rollback version is empty") ErrNoRollbacksAvailable = errors.New("no rollbacks available") - + ErrAgentInstallNotFound = errors.New("agent install descriptor not found") // Version_9_2_0_SNAPSHOT is the minimum version for manual rollback and rollback reason Version_9_2_0_SNAPSHOT = agtversion.NewParsedSemVer(9, 2, 0, "SNAPSHOT", "") ) @@ -401,8 +401,22 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return nil, err } + currentVersionedHome, err := filepath.Rel(paths.Top(), paths.Home()) + if err != nil { + return nil, fmt.Errorf("calculating home path relative to top, home: %q top: %q : %w", paths.Home(), paths.Top(), err) + } + //FIXME make it nicer - err = addInstallDesc(version, unpackRes.VersionedHome, unpackRes.Hash, detectedFlavor) + err = addInstallDesc(version, unpackRes.VersionedHome, unpackRes.Hash, detectedFlavor, false) + if err != nil { + err = fmt.Errorf("error encountered when adding install description: %w", err) + + rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), unpackRes.VersionedHome, currentVersionedHome) + if rollbackErr != nil { + return nil, goerrors.Join(err, rollbackErr) + } + return nil, err + } newHash := unpackRes.Hash if newHash == "" { @@ -430,17 +444,33 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s // paths.BinaryPath properly derives the binary directory depending on the platform. The path to the binary for macOS is inside of the app bundle. newPath := paths.BinaryPath(filepath.Join(paths.Top(), hashedDir), agentName) - currentVersionedHome, err := filepath.Rel(paths.Top(), paths.Home()) - if err != nil { - return nil, fmt.Errorf("calculating home path relative to top, home: %q top: %q : %w", paths.Home(), paths.Top(), err) - } - if err := u.changeSymlink(u.log, paths.Top(), symlinkPath, newPath); err != nil { u.log.Errorw("Rolling back: changing symlink failed", "error.message", err) rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) return nil, goerrors.Join(err, rollbackErr) } + //FIXME make it nicer + err = modifyInstallDesc(func(desc *v1.AgentInstallDesc) error { + if desc.VersionedHome == unpackRes.VersionedHome { + desc.Active = true + return nil + } + + desc.Active = false + return nil + }) + + if err != nil { + err = fmt.Errorf("error encountered when adding install description: %w", err) + + rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), unpackRes.VersionedHome, currentVersionedHome) + if rollbackErr != nil { + return nil, goerrors.Join(err, rollbackErr) + } + return nil, err + } + // We rotated the symlink successfully: prepare the current and previous agent installation details for the update marker // In update marker the `current` agent install is the one where the symlink is pointing (the new one we didn't start yet) // while the `previous` install is the currently executing elastic-agent that is no longer reachable via the symlink. @@ -502,7 +532,7 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return cb, nil } -func addInstallDesc(version string, home string, hash string, flavor string) error { +func addInstallDesc(version string, home string, hash string, flavor string, active bool) error { installMarkerFilePath := filepath.Join(paths.Top(), paths.MarkerFileName) installDescriptor, err := readInstallMarker(installMarkerFilePath) if err != nil { @@ -520,6 +550,7 @@ func addInstallDesc(version string, home string, hash string, flavor string) err Hash: hash, VersionedHome: home, Flavor: flavor, + Active: active, } installDescriptor.AgentInstalls[0] = newInstall copied := copy(installDescriptor.AgentInstalls[1:], existingInstalls) @@ -535,6 +566,36 @@ func addInstallDesc(version string, home string, hash string, flavor string) err return nil } +func modifyInstallDesc(modifierFunc func(desc *v1.AgentInstallDesc) error) error { + installMarkerFilePath := filepath.Join(paths.Top(), paths.MarkerFileName) + installDescriptor, err := readInstallMarker(installMarkerFilePath) + if err != nil { + return err + } + + if installDescriptor == nil { + return fmt.Errorf("no install descriptor found at %q") + } + + for i := range installDescriptor.AgentInstalls { + err = modifierFunc(&installDescriptor.AgentInstalls[i]) + if err != nil { + return fmt.Errorf("modifying agent install %s: %w", installDescriptor.AgentInstalls[i].VersionedHome, err) + } + } + + err = writeInstallMarker(installMarkerFilePath, installDescriptor) + if err != nil { + return fmt.Errorf("writing updated install marker: %w", err) + } + + return nil +} + +func removeAgentInstallDesc(versionedHome string) error { + +} + func writeInstallMarker(markerFilePath string, descriptor *v1.InstallDescriptor) error { installMarkerFile, err := os.Create(markerFilePath) if err != nil { @@ -767,6 +828,10 @@ func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versio if err != nil && !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("rolling back install: removing new agent install at %q failed: %w", newAgentInstallPath, err) } + err = removeAgentInstallDesc(versionedHome) + if err != nil && !errors.Is(err, ErrAgentInstallNotFound) { + return fmt.Errorf("rolling back install: removing agent install descriptor at %q failed: %w", versionedHome, err) + } return nil } diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index 611e4debbc4..e140b162a6a 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -777,7 +777,7 @@ func ensureInstallMarkerPresent() error { if err != nil { return fmt.Errorf("failed to get current file owner: %w", err) } - if err := install.CreateInstallMarker(paths.Top(), ownership, paths.Home(), version.GetAgentPackageVersion()); err != nil { + if err := install.CreateInstallMarker(paths.Top(), ownership, paths.Home(), version.GetAgentPackageVersion(), ""); err != nil { return fmt.Errorf("unable to create installation marker file during upgrade: %w", err) } diff --git a/internal/pkg/agent/install/install.go b/internal/pkg/agent/install/install.go index 11a73519554..b9c39d29e4a 100644 --- a/internal/pkg/agent/install/install.go +++ b/internal/pkg/agent/install/install.go @@ -73,7 +73,7 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p targetVersionedHome := filepath.FromSlash(pathMapper.Map(manifest.Package.VersionedHome)) - err = setupInstallPath(topPath, ownership, targetVersionedHome, manifest.Package.Version) + err = setupInstallPath(topPath, ownership, targetVersionedHome, manifest.Package.Version, flavor) if err != nil { return utils.FileOwner{}, fmt.Errorf("error setting up install path: %w", err) } @@ -189,7 +189,7 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p } // setup the basic topPath, and the .installed file -func setupInstallPath(topPath string, ownership utils.FileOwner, versionedHome string, version string) error { +func setupInstallPath(topPath string, ownership utils.FileOwner, versionedHome string, version string, flavor string) error { // ensure parent directory exists err := os.MkdirAll(filepath.Dir(topPath), 0755) if err != nil { @@ -203,7 +203,7 @@ func setupInstallPath(topPath string, ownership utils.FileOwner, versionedHome s } // create the install marker - if err := CreateInstallMarker(topPath, ownership, versionedHome, version); err != nil { + if err := CreateInstallMarker(topPath, ownership, versionedHome, version, flavor); err != nil { return fmt.Errorf("failed to create install marker: %w", err) } return nil @@ -521,16 +521,16 @@ func hasAllSSDs(block ghw.BlockInfo) bool { // CreateInstallMarker creates a `.installed` file at the given install path, // and then calls fixInstallMarkerPermissions to set the ownership provided by `ownership` -func CreateInstallMarker(topPath string, ownership utils.FileOwner, home string, version string) error { +func CreateInstallMarker(topPath string, ownership utils.FileOwner, home string, version string, flavor string) error { markerFilePath := filepath.Join(topPath, paths.MarkerFileName) - err := createInstallMarkerFile(markerFilePath, version, home) + err := createInstallMarkerFile(markerFilePath, version, home, flavor) if err != nil { return fmt.Errorf("creating install marker: %w", err) } return fixInstallMarkerPermissions(markerFilePath, ownership) } -func createInstallMarkerFile(markerFilePath string, version string, home string) error { +func createInstallMarkerFile(markerFilePath string, version string, home string, flavor string) error { handle, err := os.Create(markerFilePath) if err != nil { return fmt.Errorf("creating destination file %q : %w", markerFilePath, err) @@ -539,7 +539,7 @@ func createInstallMarkerFile(markerFilePath string, version string, home string) _ = handle.Close() }() installDescriptor := v1.NewInstallDescriptor() - installDescriptor.AgentInstalls = []v1.AgentInstallDesc{{Version: version, VersionedHome: home}} + installDescriptor.AgentInstalls = []v1.AgentInstallDesc{{Version: version, VersionedHome: home, Flavor: flavor, Active: true}} err = yaml.NewEncoder(handle).Encode(installDescriptor) if err != nil { return fmt.Errorf("writing install descriptor: %w", err) diff --git a/internal/pkg/agent/install/install_test.go b/internal/pkg/agent/install/install_test.go index 98aea5cce10..df35fac1096 100644 --- a/internal/pkg/agent/install/install_test.go +++ b/internal/pkg/agent/install/install_test.go @@ -224,7 +224,7 @@ func TestSetupInstallPath(t *testing.T) { tmpdir := t.TempDir() ownership, err := utils.CurrentFileOwner() require.NoError(t, err) - err = setupInstallPath(tmpdir, ownership, "data/elastic-agent-1.2.3-SNAPSHOT", "1.2.3-SNAPSHOT") + err = setupInstallPath(tmpdir, ownership, "data/elastic-agent-1.2.3-SNAPSHOT", "1.2.3-SNAPSHOT", "") require.NoError(t, err) markerFilePath := filepath.Join(tmpdir, paths.MarkerFileName) require.FileExists(t, markerFilePath) diff --git a/pkg/api/v1/install.go b/pkg/api/v1/install.go index 74ddb7cfd88..e76ee5ba345 100644 --- a/pkg/api/v1/install.go +++ b/pkg/api/v1/install.go @@ -25,6 +25,7 @@ type AgentInstallDesc struct { Hash string `yaml:"hash,omitempty" json:"hash,omitempty"` VersionedHome string `yaml:"versionedHome,omitempty" json:"versionedHome,omitempty"` Flavor string `yaml:"flavor,omitempty" json:"flavor,omitempty"` + Active bool `yaml:"active,omitempty" json:"active,omitempty"` } type InstallDescriptor struct { From 2b5b2cf5dedf01c0a14caa15d7713d3ca4942751 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Mon, 22 Sep 2025 18:37:38 +0200 Subject: [PATCH 05/18] Add InstallDescriptorSource abstraction --- .mockery.yaml | 1 + internal/pkg/agent/application/application.go | 4 +- .../coordinator/coordinator_unit_test.go | 4 +- .../mock_installdescriptorsource_test.go | 214 +++++++ .../pkg/agent/application/upgrade/upgrade.go | 188 ++---- .../agent/application/upgrade/upgrade_test.go | 64 +- internal/pkg/agent/install/install.go | 24 +- internal/pkg/agent/install/install_test.go | 6 +- pkg/utils/install/file_source.go | 123 ++++ pkg/utils/install/file_source_test.go | 583 ++++++++++++++++++ 10 files changed, 1048 insertions(+), 163 deletions(-) create mode 100644 internal/pkg/agent/application/upgrade/mock_installdescriptorsource_test.go create mode 100644 pkg/utils/install/file_source.go create mode 100644 pkg/utils/install/file_source_test.go diff --git a/.mockery.yaml b/.mockery.yaml index 53efe9faabd..a35fa8ae490 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -18,6 +18,7 @@ packages: interfaces: WatcherHelper: {} watcherGrappler: {} + installDescriptorSource: {} github.com/elastic/elastic-agent/internal/pkg/agent/cmd: interfaces: agentWatcher: {} diff --git a/internal/pkg/agent/application/application.go b/internal/pkg/agent/application/application.go index 1c9c53fe409..373c1257758 100644 --- a/internal/pkg/agent/application/application.go +++ b/internal/pkg/agent/application/application.go @@ -7,12 +7,14 @@ package application import ( "context" "fmt" + "path/filepath" "time" "go.elastic.co/apm/v2" componentmonitoring "github.com/elastic/elastic-agent/internal/pkg/agent/application/monitoring/component" + "github.com/elastic/elastic-agent/pkg/utils/install" "github.com/elastic/go-ucfg" "github.com/elastic/elastic-agent-libs/logp" @@ -129,7 +131,7 @@ func New( // monitoring is not supported in bootstrap mode https://github.com/elastic/elastic-agent/issues/1761 isMonitoringSupported := !disableMonitoring && cfg.Settings.V1MonitoringEnabled - upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, cfg.Settings.Upgrade, agentInfo, new(upgrade.AgentWatcherHelper)) + upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, cfg.Settings.Upgrade, agentInfo, new(upgrade.AgentWatcherHelper), install.NewFileDescriptorSource(filepath.Join(paths.Top(), paths.MarkerFileName))) if err != nil { return nil, nil, nil, fmt.Errorf("failed to create upgrader: %w", err) } diff --git a/internal/pkg/agent/application/coordinator/coordinator_unit_test.go b/internal/pkg/agent/application/coordinator/coordinator_unit_test.go index 6dd014761a6..92b11e3e2d1 100644 --- a/internal/pkg/agent/application/coordinator/coordinator_unit_test.go +++ b/internal/pkg/agent/application/coordinator/coordinator_unit_test.go @@ -29,6 +29,7 @@ import ( "github.com/elastic/elastic-agent-client/v7/pkg/proto" "github.com/elastic/elastic-agent/internal/pkg/fleetapi/acker" "github.com/elastic/elastic-agent/internal/pkg/testutils" + "github.com/elastic/elastic-agent/pkg/utils/install" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status" "go.opentelemetry.io/collector/component/componentstatus" @@ -467,7 +468,8 @@ func TestCoordinatorReportsInvalidPolicy(t *testing.T) { } }() - upgradeMgr, err := upgrade.NewUpgrader(log, &artifact.Config{}, nil, &info.AgentInfo{}, new(upgrade.AgentWatcherHelper)) + tmpDir := t.TempDir() + upgradeMgr, err := upgrade.NewUpgrader(log, &artifact.Config{}, nil, &info.AgentInfo{}, new(upgrade.AgentWatcherHelper), install.NewFileDescriptorSource(filepath.Join(tmpDir, paths.MarkerFileName))) require.NoError(t, err, "errored when creating a new upgrader") // Channels have buffer length 1, so we don't have to run on multiple diff --git a/internal/pkg/agent/application/upgrade/mock_installdescriptorsource_test.go b/internal/pkg/agent/application/upgrade/mock_installdescriptorsource_test.go new file mode 100644 index 00000000000..4d23507749d --- /dev/null +++ b/internal/pkg/agent/application/upgrade/mock_installdescriptorsource_test.go @@ -0,0 +1,214 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package upgrade + +import ( + mock "github.com/stretchr/testify/mock" + + v1 "github.com/elastic/elastic-agent/pkg/api/v1" +) + +// mockInstallDescriptorSource is an autogenerated mock type for the installDescriptorSource type +type mockInstallDescriptorSource struct { + mock.Mock +} + +type mockInstallDescriptorSource_Expecter struct { + mock *mock.Mock +} + +func (_m *mockInstallDescriptorSource) EXPECT() *mockInstallDescriptorSource_Expecter { + return &mockInstallDescriptorSource_Expecter{mock: &_m.Mock} +} + +// AddInstallDesc provides a mock function with given fields: desc +func (_m *mockInstallDescriptorSource) AddInstallDesc(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) { + ret := _m.Called(desc) + + if len(ret) == 0 { + panic("no return value specified for AddInstallDesc") + } + + var r0 *v1.InstallDescriptor + var r1 error + if rf, ok := ret.Get(0).(func(v1.AgentInstallDesc) (*v1.InstallDescriptor, error)); ok { + return rf(desc) + } + if rf, ok := ret.Get(0).(func(v1.AgentInstallDesc) *v1.InstallDescriptor); ok { + r0 = rf(desc) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.InstallDescriptor) + } + } + + if rf, ok := ret.Get(1).(func(v1.AgentInstallDesc) error); ok { + r1 = rf(desc) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockInstallDescriptorSource_AddInstallDesc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddInstallDesc' +type mockInstallDescriptorSource_AddInstallDesc_Call struct { + *mock.Call +} + +// AddInstallDesc is a helper method to define mock.On call +// - desc v1.AgentInstallDesc +func (_e *mockInstallDescriptorSource_Expecter) AddInstallDesc(desc interface{}) *mockInstallDescriptorSource_AddInstallDesc_Call { + return &mockInstallDescriptorSource_AddInstallDesc_Call{Call: _e.mock.On("AddInstallDesc", desc)} +} + +func (_c *mockInstallDescriptorSource_AddInstallDesc_Call) Run(run func(desc v1.AgentInstallDesc)) *mockInstallDescriptorSource_AddInstallDesc_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(v1.AgentInstallDesc)) + }) + return _c +} + +func (_c *mockInstallDescriptorSource_AddInstallDesc_Call) Return(_a0 *v1.InstallDescriptor, _a1 error) *mockInstallDescriptorSource_AddInstallDesc_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockInstallDescriptorSource_AddInstallDesc_Call) RunAndReturn(run func(v1.AgentInstallDesc) (*v1.InstallDescriptor, error)) *mockInstallDescriptorSource_AddInstallDesc_Call { + _c.Call.Return(run) + return _c +} + +// ModifyInstallDesc provides a mock function with given fields: modifierFunc +func (_m *mockInstallDescriptorSource) ModifyInstallDesc(modifierFunc func(*v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error) { + ret := _m.Called(modifierFunc) + + if len(ret) == 0 { + panic("no return value specified for ModifyInstallDesc") + } + + var r0 *v1.InstallDescriptor + var r1 error + if rf, ok := ret.Get(0).(func(func(*v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error)); ok { + return rf(modifierFunc) + } + if rf, ok := ret.Get(0).(func(func(*v1.AgentInstallDesc) error) *v1.InstallDescriptor); ok { + r0 = rf(modifierFunc) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.InstallDescriptor) + } + } + + if rf, ok := ret.Get(1).(func(func(*v1.AgentInstallDesc) error) error); ok { + r1 = rf(modifierFunc) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockInstallDescriptorSource_ModifyInstallDesc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ModifyInstallDesc' +type mockInstallDescriptorSource_ModifyInstallDesc_Call struct { + *mock.Call +} + +// ModifyInstallDesc is a helper method to define mock.On call +// - modifierFunc func(*v1.AgentInstallDesc) error +func (_e *mockInstallDescriptorSource_Expecter) ModifyInstallDesc(modifierFunc interface{}) *mockInstallDescriptorSource_ModifyInstallDesc_Call { + return &mockInstallDescriptorSource_ModifyInstallDesc_Call{Call: _e.mock.On("ModifyInstallDesc", modifierFunc)} +} + +func (_c *mockInstallDescriptorSource_ModifyInstallDesc_Call) Run(run func(modifierFunc func(*v1.AgentInstallDesc) error)) *mockInstallDescriptorSource_ModifyInstallDesc_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(func(*v1.AgentInstallDesc) error)) + }) + return _c +} + +func (_c *mockInstallDescriptorSource_ModifyInstallDesc_Call) Return(_a0 *v1.InstallDescriptor, _a1 error) *mockInstallDescriptorSource_ModifyInstallDesc_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockInstallDescriptorSource_ModifyInstallDesc_Call) RunAndReturn(run func(func(*v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error)) *mockInstallDescriptorSource_ModifyInstallDesc_Call { + _c.Call.Return(run) + return _c +} + +// RemoveAgentInstallDesc provides a mock function with given fields: versionedHome +func (_m *mockInstallDescriptorSource) RemoveAgentInstallDesc(versionedHome string) (*v1.InstallDescriptor, error) { + ret := _m.Called(versionedHome) + + if len(ret) == 0 { + panic("no return value specified for RemoveAgentInstallDesc") + } + + var r0 *v1.InstallDescriptor + var r1 error + if rf, ok := ret.Get(0).(func(string) (*v1.InstallDescriptor, error)); ok { + return rf(versionedHome) + } + if rf, ok := ret.Get(0).(func(string) *v1.InstallDescriptor); ok { + r0 = rf(versionedHome) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.InstallDescriptor) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(versionedHome) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockInstallDescriptorSource_RemoveAgentInstallDesc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveAgentInstallDesc' +type mockInstallDescriptorSource_RemoveAgentInstallDesc_Call struct { + *mock.Call +} + +// RemoveAgentInstallDesc is a helper method to define mock.On call +// - versionedHome string +func (_e *mockInstallDescriptorSource_Expecter) RemoveAgentInstallDesc(versionedHome interface{}) *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call { + return &mockInstallDescriptorSource_RemoveAgentInstallDesc_Call{Call: _e.mock.On("RemoveAgentInstallDesc", versionedHome)} +} + +func (_c *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call) Run(run func(versionedHome string)) *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call) Return(_a0 *v1.InstallDescriptor, _a1 error) *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call) RunAndReturn(run func(string) (*v1.InstallDescriptor, error)) *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call { + _c.Call.Return(run) + return _c +} + +// newMockInstallDescriptorSource creates a new instance of mockInstallDescriptorSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockInstallDescriptorSource(t interface { + mock.TestingT + Cleanup(func()) +}) *mockInstallDescriptorSource { + mock := &mockInstallDescriptorSource{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index 9e280e8d9d9..934aecc9c03 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -93,7 +93,7 @@ type copyRunDirectoryFunc func(log *logger.Logger, oldRunPath, newRunPath string type fileDirCopyFunc func(from, to string, opts ...filecopy.Options) error type markUpgradeFunc func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, rollbackWindow time.Duration) error type changeSymlinkFunc func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error -type rollbackInstallFunc func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error +type rollbackInstallFunc func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error // Types used to abstract stdlib functions type mkdirAllFunc func(name string, perm fs.FileMode) error @@ -117,16 +117,23 @@ type WatcherHelper interface { TakeOverWatcher(ctx context.Context, log *logger.Logger, topDir string) (*filelock.AppLocker, error) } +type installDescriptorSource interface { + AddInstallDesc(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) + ModifyInstallDesc(modifierFunc func(desc *v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error) + RemoveAgentInstallDesc(versionedHome string) (*v1.InstallDescriptor, error) +} + // Upgrader performs an upgrade type Upgrader struct { - log *logger.Logger - settings *artifact.Config - upgradeSettings *configuration.UpgradeConfig - agentInfo info.Agent - upgradeable bool - fleetServerURI string - markerWatcher MarkerWatcher - watcherHelper WatcherHelper + log *logger.Logger + settings *artifact.Config + upgradeSettings *configuration.UpgradeConfig + agentInfo info.Agent + upgradeable bool + fleetServerURI string + markerWatcher MarkerWatcher + watcherHelper WatcherHelper + installDescriptorSource installDescriptorSource // The following are abstractions for testability artifactDownloader artifactDownloadHandler @@ -148,24 +155,25 @@ func IsUpgradeable() bool { } // NewUpgrader creates an upgrader which is capable of performing upgrade operation -func NewUpgrader(log *logger.Logger, settings *artifact.Config, upgradeConfig *configuration.UpgradeConfig, agentInfo info.Agent, watcherHelper WatcherHelper) (*Upgrader, error) { +func NewUpgrader(log *logger.Logger, settings *artifact.Config, upgradeConfig *configuration.UpgradeConfig, agentInfo info.Agent, watcherHelper WatcherHelper, ids installDescriptorSource) (*Upgrader, error) { return &Upgrader{ - log: log, - settings: settings, - upgradeSettings: upgradeConfig, - agentInfo: agentInfo, - upgradeable: IsUpgradeable(), - markerWatcher: newMarkerFileWatcher(markerFilePath(paths.Data()), log), - watcherHelper: watcherHelper, - artifactDownloader: newArtifactDownloader(settings, log), - unpacker: newUnpacker(log), - isDiskSpaceErrorFunc: upgradeErrors.IsDiskSpaceError, - extractAgentVersion: extractAgentVersion, - copyActionStore: copyActionStoreProvider(os.ReadFile, os.WriteFile), - copyRunDirectory: copyRunDirectoryProvider(os.MkdirAll, filecopy.Copy), - markUpgrade: markUpgradeProvider(UpdateActiveCommit, os.WriteFile), - changeSymlink: changeSymlink, - rollbackInstall: rollbackInstall, + log: log, + settings: settings, + upgradeSettings: upgradeConfig, + agentInfo: agentInfo, + upgradeable: IsUpgradeable(), + markerWatcher: newMarkerFileWatcher(markerFilePath(paths.Data()), log), + watcherHelper: watcherHelper, + installDescriptorSource: ids, + artifactDownloader: newArtifactDownloader(settings, log), + unpacker: newUnpacker(log), + isDiskSpaceErrorFunc: upgradeErrors.IsDiskSpaceError, + extractAgentVersion: extractAgentVersion, + copyActionStore: copyActionStoreProvider(os.ReadFile, os.WriteFile), + copyRunDirectory: copyRunDirectoryProvider(os.MkdirAll, filecopy.Copy), + markUpgrade: markUpgradeProvider(UpdateActiveCommit, os.WriteFile), + changeSymlink: changeSymlink, + rollbackInstall: rollbackInstall, }, nil } @@ -407,11 +415,13 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s } //FIXME make it nicer - err = addInstallDesc(version, unpackRes.VersionedHome, unpackRes.Hash, detectedFlavor, false) + _, err = u.installDescriptorSource.AddInstallDesc( + v1.AgentInstallDesc{Version: version, VersionedHome: unpackRes.VersionedHome, Hash: unpackRes.Hash, Flavor: detectedFlavor, Active: false}, + ) if err != nil { err = fmt.Errorf("error encountered when adding install description: %w", err) - rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), unpackRes.VersionedHome, currentVersionedHome) + rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), unpackRes.VersionedHome, currentVersionedHome, u.installDescriptorSource) if rollbackErr != nil { return nil, goerrors.Join(err, rollbackErr) } @@ -446,25 +456,27 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s if err := u.changeSymlink(u.log, paths.Top(), symlinkPath, newPath); err != nil { u.log.Errorw("Rolling back: changing symlink failed", "error.message", err) - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.installDescriptorSource) return nil, goerrors.Join(err, rollbackErr) } //FIXME make it nicer - err = modifyInstallDesc(func(desc *v1.AgentInstallDesc) error { - if desc.VersionedHome == unpackRes.VersionedHome { - desc.Active = true - return nil - } + _, err = u.installDescriptorSource.ModifyInstallDesc( + func(desc *v1.AgentInstallDesc) error { + if desc.VersionedHome == unpackRes.VersionedHome { + desc.Active = true + return nil + } - desc.Active = false - return nil - }) + desc.Active = false + return nil + }, + ) if err != nil { err = fmt.Errorf("error encountered when adding install description: %w", err) - rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), unpackRes.VersionedHome, currentVersionedHome) + rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), unpackRes.VersionedHome, currentVersionedHome, u.installDescriptorSource) if rollbackErr != nil { return nil, goerrors.Join(err, rollbackErr) } @@ -500,7 +512,7 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s previous, // old agent version data action, det, rollbackWindow); err != nil { u.log.Errorw("Rolling back: marking upgrade failed", "error.message", err) - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.installDescriptorSource) return nil, goerrors.Join(err, rollbackErr) } @@ -509,14 +521,14 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s var watcherCmd *exec.Cmd if watcherCmd, err = u.watcherHelper.InvokeWatcher(u.log, watcherExecutable); err != nil { u.log.Errorw("Rolling back: starting watcher failed", "error.message", err) - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.installDescriptorSource) return nil, goerrors.Join(err, rollbackErr) } watcherWaitErr := u.watcherHelper.WaitForWatcher(ctx, u.log, markerFilePath(paths.Data()), watcherMaxWaitTime) if watcherWaitErr != nil { killWatcherErr := watcherCmd.Process.Kill() - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.installDescriptorSource) return nil, goerrors.Join(watcherWaitErr, killWatcherErr, rollbackErr) } @@ -532,96 +544,6 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return cb, nil } -func addInstallDesc(version string, home string, hash string, flavor string, active bool) error { - installMarkerFilePath := filepath.Join(paths.Top(), paths.MarkerFileName) - installDescriptor, err := readInstallMarker(installMarkerFilePath) - if err != nil { - return err - } - - if installDescriptor == nil { - return fmt.Errorf("no install descriptor found at %q") - } - - existingInstalls := installDescriptor.AgentInstalls - installDescriptor.AgentInstalls = make([]v1.AgentInstallDesc, len(existingInstalls)+1) - newInstall := v1.AgentInstallDesc{ - Version: version, - Hash: hash, - VersionedHome: home, - Flavor: flavor, - Active: active, - } - installDescriptor.AgentInstalls[0] = newInstall - copied := copy(installDescriptor.AgentInstalls[1:], existingInstalls) - if copied != len(existingInstalls) { - return fmt.Errorf("error adding new install %v to existing installs %v", newInstall, existingInstalls) - } - - err = writeInstallMarker(installMarkerFilePath, installDescriptor) - if err != nil { - return fmt.Errorf("writing updated install marker: %w", err) - } - - return nil -} - -func modifyInstallDesc(modifierFunc func(desc *v1.AgentInstallDesc) error) error { - installMarkerFilePath := filepath.Join(paths.Top(), paths.MarkerFileName) - installDescriptor, err := readInstallMarker(installMarkerFilePath) - if err != nil { - return err - } - - if installDescriptor == nil { - return fmt.Errorf("no install descriptor found at %q") - } - - for i := range installDescriptor.AgentInstalls { - err = modifierFunc(&installDescriptor.AgentInstalls[i]) - if err != nil { - return fmt.Errorf("modifying agent install %s: %w", installDescriptor.AgentInstalls[i].VersionedHome, err) - } - } - - err = writeInstallMarker(installMarkerFilePath, installDescriptor) - if err != nil { - return fmt.Errorf("writing updated install marker: %w", err) - } - - return nil -} - -func removeAgentInstallDesc(versionedHome string) error { - -} - -func writeInstallMarker(markerFilePath string, descriptor *v1.InstallDescriptor) error { - installMarkerFile, err := os.Create(markerFilePath) - if err != nil { - return fmt.Errorf("opening install marker file: %w", err) - } - defer func(installMarkerFile *os.File) { - _ = installMarkerFile.Close() - }(installMarkerFile) - return v1.WriteInstallDescriptor(installMarkerFile, descriptor) -} - -func readInstallMarker(markerFilePath string) (*v1.InstallDescriptor, error) { - installMarkerFile, err := os.Open(markerFilePath) - if err != nil { - return nil, fmt.Errorf("opening install marker file: %w", err) - } - defer func(installMarkerFile *os.File) { - _ = installMarkerFile.Close() - }(installMarkerFile) - installDescriptor, err := v1.ParseInstallDescriptor(installMarkerFile) - if err != nil { - return nil, fmt.Errorf("parsing install marker file: %w", err) - } - return installDescriptor, nil -} - func (u *Upgrader) rollbackToPreviousVersion(ctx context.Context, topDir string, now time.Time, version string, action *fleetapi.ActionUpgrade) (reexec.ShutdownCallbackFn, error) { if version == "" { return nil, ErrEmptyRollbackVersion @@ -816,7 +738,7 @@ func isSameVersion(log *logger.Logger, current agentVersion, newVersion agentVer return current == newVersion } -func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error { +func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error { oldAgentPath := paths.BinaryPath(filepath.Join(topDirPath, oldVersionedHome), agentName) err := changeSymlink(log, topDirPath, filepath.Join(topDirPath, agentName), oldAgentPath) if err != nil && !errors.Is(err, fs.ErrNotExist) { @@ -828,7 +750,7 @@ func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versio if err != nil && !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("rolling back install: removing new agent install at %q failed: %w", newAgentInstallPath, err) } - err = removeAgentInstallDesc(versionedHome) + _, err = ids.RemoveAgentInstallDesc(versionedHome) if err != nil && !errors.Is(err, ErrAgentInstallNotFound) { return fmt.Errorf("rolling back install: removing agent install descriptor at %q failed: %w", versionedHome, err) } diff --git a/internal/pkg/agent/application/upgrade/upgrade_test.go b/internal/pkg/agent/application/upgrade/upgrade_test.go index 5bc96564cb6..b7385f938d2 100644 --- a/internal/pkg/agent/application/upgrade/upgrade_test.go +++ b/internal/pkg/agent/application/upgrade/upgrade_test.go @@ -1292,6 +1292,7 @@ func TestManualRollback(t *testing.T) { log, _ := loggertest.New(t.Name()) mockAgentInfo := info.NewMockAgent(t) mockWatcherHelper := NewMockWatcherHelper(t) + mockInstallSource := newMockInstallDescriptorSource(t) topDir := t.TempDir() err := os.MkdirAll(paths.DataFrom(topDir), 0777) require.NoError(t, err, "error creating data directory in topDir %q", topDir) @@ -1300,7 +1301,7 @@ func TestManualRollback(t *testing.T) { tc.setup(t, topDir, mockAgentInfo, mockWatcherHelper) } - upgrader, err := NewUpgrader(log, tc.artifactSettings, tc.upgradeSettings, mockAgentInfo, mockWatcherHelper) + upgrader, err := NewUpgrader(log, tc.artifactSettings, tc.upgradeSettings, mockAgentInfo, mockWatcherHelper, mockInstallSource) require.NoError(t, err, "error instantiating upgrader") _, err = upgrader.rollbackToPreviousVersion(t.Context(), topDir, tc.now, tc.version, nil) tc.wantErr(t, err, "unexpected error returned by rollbackToPreviousVersion()") @@ -1352,6 +1353,7 @@ func TestUpgradeErrorHandling(t *testing.T) { upgraderMocker upgraderMocker checkArchiveCleanup bool checkVersionedHomeCleanup bool + setupMocks func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) } testCases := map[string]testCase{ @@ -1365,6 +1367,9 @@ func TestUpgradeErrorHandling(t *testing.T) { } }, checkArchiveCleanup: true, + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + mockAgentInfo.EXPECT().Version().Return("9.0.0") + }, }, "should return error if getPackageMetadata fails": { isDiskSpaceErrorResult: false, @@ -1378,6 +1383,9 @@ func TestUpgradeErrorHandling(t *testing.T) { } }, checkArchiveCleanup: true, + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + mockAgentInfo.EXPECT().Version().Return("9.0.0") + }, }, "should return error and cleanup downloaded archive if unpack fails before extracting": { isDiskSpaceErrorResult: false, @@ -1402,6 +1410,9 @@ func TestUpgradeErrorHandling(t *testing.T) { } }, checkArchiveCleanup: true, + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + mockAgentInfo.EXPECT().Version().Return("9.0.0") + }, }, "should return error and cleanup downloaded archive if unpack fails after extracting": { isDiskSpaceErrorResult: false, @@ -1431,6 +1442,9 @@ func TestUpgradeErrorHandling(t *testing.T) { }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + mockAgentInfo.EXPECT().Version().Return("9.0.0") + }, }, "should return error and cleanup downloaded artifact and extracted archive if copyActionStore fails": { isDiskSpaceErrorResult: false, @@ -1462,6 +1476,14 @@ func TestUpgradeErrorHandling(t *testing.T) { }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + mockAgentInfo.EXPECT().Version().Return("9.0.0") + mockInstallSrc.EXPECT().AddInstallDesc(mock.AnythingOfType("v1.AgentInstallDesc")).RunAndReturn( + func(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) { + return &v1.InstallDescriptor{}, nil + }, + ) + }, }, "should return error and cleanup downloaded artifact and extracted archive if copyRunDirectory fails": { isDiskSpaceErrorResult: false, @@ -1497,6 +1519,15 @@ func TestUpgradeErrorHandling(t *testing.T) { }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + mockAgentInfo.EXPECT().Version().Return("9.0.0") + + mockInstallSrc.EXPECT().AddInstallDesc(mock.AnythingOfType("v1.AgentInstallDesc")).RunAndReturn( + func(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) { + return &v1.InstallDescriptor{}, nil + }, + ) + }, }, "should return error and cleanup downloaded artifact and extracted archive if changeSymlink fails": { isDiskSpaceErrorResult: false, @@ -1528,7 +1559,7 @@ func TestUpgradeErrorHandling(t *testing.T) { upgrader.copyRunDirectory = func(log *logger.Logger, oldRunPath, newRunPath string) error { return nil } - upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error { + upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error { return nil } upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error { @@ -1537,6 +1568,10 @@ func TestUpgradeErrorHandling(t *testing.T) { }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + mockAgentInfo.EXPECT().Version().Return("9.0.0") + mockInstallSrc.EXPECT().AddInstallDesc(mock.AnythingOfType("v1.AgentInstallDesc")).Return(&v1.InstallDescriptor{}, nil) + }, }, "should return error and cleanup downloaded artifact and extracted archive if markUpgrade fails": { isDiskSpaceErrorResult: false, @@ -1571,7 +1606,7 @@ func TestUpgradeErrorHandling(t *testing.T) { upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error { return nil } - upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error { + upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error { return nil } upgrader.markUpgrade = func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, rollbackWindow time.Duration) error { @@ -1580,6 +1615,11 @@ func TestUpgradeErrorHandling(t *testing.T) { }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + mockAgentInfo.EXPECT().Version().Return("9.0.0") + mockInstallSrc.EXPECT().AddInstallDesc(mock.AnythingOfType("v1.AgentInstallDesc")).Return(&v1.InstallDescriptor{}, nil) + mockInstallSrc.EXPECT().ModifyInstallDesc(mock.Anything).Return(&v1.InstallDescriptor{}, nil) + }, }, "should add disk space error to the error chain if downloadArtifact fails with disk space error": { isDiskSpaceErrorResult: true, @@ -1589,19 +1629,29 @@ func TestUpgradeErrorHandling(t *testing.T) { returnError: testError, } }, + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + mockAgentInfo.EXPECT().Version().Return("9.0.0") + }, }, } - mockAgentInfo := info.NewMockAgent(t) - mockAgentInfo.On("Version").Return("9.0.0") - for name, tc := range testCases { t.Run(name, func(t *testing.T) { baseDir := t.TempDir() paths.SetTop(baseDir) + mockAgentInfo := info.NewMockAgent(t) + mockInstallSource := newMockInstallDescriptorSource(t) mockWatcherHelper := NewMockWatcherHelper(t) - upgrader, err := NewUpgrader(log, &artifact.Config{}, nil, mockAgentInfo, mockWatcherHelper) + + if tc.setupMocks != nil { + // setup mocks + tc.setupMocks(t, mockAgentInfo, mockInstallSource, mockWatcherHelper) + } else { + t.Log("skipping mocks setup as the testcase does not define a setupMocks()") + } + + upgrader, err := NewUpgrader(log, &artifact.Config{}, nil, mockAgentInfo, mockWatcherHelper, mockInstallSource) require.NoError(t, err) tc.upgraderMocker(upgrader, filepath.Join(baseDir, "mockArchive"), "versionedHome") diff --git a/internal/pkg/agent/install/install.go b/internal/pkg/agent/install/install.go index b9c39d29e4a..c2c5fa8184c 100644 --- a/internal/pkg/agent/install/install.go +++ b/internal/pkg/agent/install/install.go @@ -17,7 +17,6 @@ import ( "github.com/kardianos/service" "github.com/otiai10/copy" "github.com/schollz/progressbar/v3" - "gopkg.in/yaml.v3" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" @@ -26,6 +25,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/cli" v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/utils" + "github.com/elastic/elastic-agent/pkg/utils/install" manifestutils "github.com/elastic/elastic-agent/pkg/utils/manifest" ) @@ -523,30 +523,16 @@ func hasAllSSDs(block ghw.BlockInfo) bool { // and then calls fixInstallMarkerPermissions to set the ownership provided by `ownership` func CreateInstallMarker(topPath string, ownership utils.FileOwner, home string, version string, flavor string) error { markerFilePath := filepath.Join(topPath, paths.MarkerFileName) - err := createInstallMarkerFile(markerFilePath, version, home, flavor) + installDescProvider := install.NewFileDescriptorSource(markerFilePath) + installDesc := v1.AgentInstallDesc{Version: version, VersionedHome: home, Flavor: flavor, Active: true} + _, err := installDescProvider.AddInstallDesc(installDesc) + if err != nil { return fmt.Errorf("creating install marker: %w", err) } return fixInstallMarkerPermissions(markerFilePath, ownership) } -func createInstallMarkerFile(markerFilePath string, version string, home string, flavor string) error { - handle, err := os.Create(markerFilePath) - if err != nil { - return fmt.Errorf("creating destination file %q : %w", markerFilePath, err) - } - defer func() { - _ = handle.Close() - }() - installDescriptor := v1.NewInstallDescriptor() - installDescriptor.AgentInstalls = []v1.AgentInstallDesc{{Version: version, VersionedHome: home, Flavor: flavor, Active: true}} - err = yaml.NewEncoder(handle).Encode(installDescriptor) - if err != nil { - return fmt.Errorf("writing install descriptor: %w", err) - } - return nil -} - func UnprivilegedUser(username, password string) (string, string) { if username != "" { return username, password diff --git a/internal/pkg/agent/install/install_test.go b/internal/pkg/agent/install/install_test.go index df35fac1096..6a2616f99c8 100644 --- a/internal/pkg/agent/install/install_test.go +++ b/internal/pkg/agent/install/install_test.go @@ -224,7 +224,7 @@ func TestSetupInstallPath(t *testing.T) { tmpdir := t.TempDir() ownership, err := utils.CurrentFileOwner() require.NoError(t, err) - err = setupInstallPath(tmpdir, ownership, "data/elastic-agent-1.2.3-SNAPSHOT", "1.2.3-SNAPSHOT", "") + err = setupInstallPath(tmpdir, ownership, "data/elastic-agent-1.2.3-SNAPSHOT", "1.2.3-SNAPSHOT", "flavor") require.NoError(t, err) markerFilePath := filepath.Join(tmpdir, paths.MarkerFileName) require.FileExists(t, markerFilePath) @@ -234,7 +234,9 @@ func TestSetupInstallPath(t *testing.T) { kind: InstallDescriptor agentInstalls: - version: 1.2.3-SNAPSHOT - versioned-home: data/elastic-agent-1.2.3-SNAPSHOT + versionedHome: data/elastic-agent-1.2.3-SNAPSHOT + active: true + flavor: flavor ` actualInstallDescriptorBytes, err := os.ReadFile(markerFilePath) require.NoError(t, err, "error reading actual install descriptor") diff --git a/pkg/utils/install/file_source.go b/pkg/utils/install/file_source.go new file mode 100644 index 00000000000..f2a8e4e8f11 --- /dev/null +++ b/pkg/utils/install/file_source.go @@ -0,0 +1,123 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package install + +import ( + "errors" + "fmt" + "io" + "os" + "slices" + + v1 "github.com/elastic/elastic-agent/pkg/api/v1" +) + +type FileDescriptorSource struct { + descriptorFile string +} + +func NewFileDescriptorSource(descriptorFile string) *FileDescriptorSource { + return &FileDescriptorSource{descriptorFile: descriptorFile} +} + +func (dp *FileDescriptorSource) AddInstallDesc(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) { + installDescriptor, err := readInstallMarkerFile(dp.descriptorFile) + // not existing or empty files are tolerated, since we would be writing a new descriptor, return any other error + if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, io.EOF) { + return nil, err + } + + if installDescriptor == nil { + installDescriptor = v1.NewInstallDescriptor() + } + + existingInstalls := installDescriptor.AgentInstalls + installDescriptor.AgentInstalls = make([]v1.AgentInstallDesc, len(existingInstalls)+1) + installDescriptor.AgentInstalls[0] = desc + copied := copy(installDescriptor.AgentInstalls[1:], existingInstalls) + if copied != len(existingInstalls) { + return nil, fmt.Errorf("error adding new install %v to existing installs %v", desc, existingInstalls) + } + + err = writeInstallMarkerFile(dp.descriptorFile, installDescriptor) + if err != nil { + return nil, fmt.Errorf("writing updated install marker: %w", err) + } + + return installDescriptor, nil +} + +func (dp *FileDescriptorSource) ModifyInstallDesc(modifierFunc func(desc *v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error) { + installDescriptor, err := readInstallMarkerFile(dp.descriptorFile) + if err != nil { + return nil, err + } + + if installDescriptor == nil { + return nil, fmt.Errorf("no install descriptor found at %q", dp.descriptorFile) + } + + for i := range installDescriptor.AgentInstalls { + err = modifierFunc(&installDescriptor.AgentInstalls[i]) + if err != nil { + return nil, fmt.Errorf("modifying agent install %s: %w", installDescriptor.AgentInstalls[i].VersionedHome, err) + } + } + + err = writeInstallMarkerFile(dp.descriptorFile, installDescriptor) + if err != nil { + return nil, fmt.Errorf("writing updated install marker: %w", err) + } + + return installDescriptor, nil +} + +func (dp *FileDescriptorSource) RemoveAgentInstallDesc(versionedHome string) (*v1.InstallDescriptor, error) { + installDescriptor, err := readInstallMarkerFile(dp.descriptorFile) + if err != nil { + return nil, err + } + + if installDescriptor == nil { + return nil, fmt.Errorf("no install descriptor found at %q", dp.descriptorFile) + } + + installDescriptor.AgentInstalls = slices.DeleteFunc(installDescriptor.AgentInstalls, func(installDesc v1.AgentInstallDesc) bool { + return installDesc.VersionedHome == versionedHome + }) + + err = writeInstallMarkerFile(dp.descriptorFile, installDescriptor) + if err != nil { + return nil, fmt.Errorf("writing updated install marker: %w", err) + } + + return installDescriptor, nil +} + +func writeInstallMarkerFile(markerFilePath string, descriptor *v1.InstallDescriptor) error { + installMarkerFile, err := os.Create(markerFilePath) + if err != nil { + return fmt.Errorf("opening install marker file: %w", err) + } + defer func(installMarkerFile *os.File) { + _ = installMarkerFile.Close() + }(installMarkerFile) + return v1.WriteInstallDescriptor(installMarkerFile, descriptor) +} + +func readInstallMarkerFile(markerFilePath string) (*v1.InstallDescriptor, error) { + installMarkerFile, err := os.Open(markerFilePath) + if err != nil { + return nil, fmt.Errorf("opening install marker file: %w", err) + } + defer func(installMarkerFile *os.File) { + _ = installMarkerFile.Close() + }(installMarkerFile) + installDescriptor, err := v1.ParseInstallDescriptor(installMarkerFile) + if err != nil { + return nil, fmt.Errorf("parsing install marker file: %w", err) + } + return installDescriptor, nil +} diff --git a/pkg/utils/install/file_source_test.go b/pkg/utils/install/file_source_test.go new file mode 100644 index 00000000000..16813489745 --- /dev/null +++ b/pkg/utils/install/file_source_test.go @@ -0,0 +1,583 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package install + +import ( + "bytes" + "errors" + "io" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" + v1 "github.com/elastic/elastic-agent/pkg/api/v1" +) + +func TestFileDescriptorSource_AddInstallDesc(t *testing.T) { + testcases := []struct { + name string + setupDir func(t *testing.T, tmpDir string) string + arg v1.AgentInstallDesc + expected *v1.InstallDescriptor + wantErr assert.ErrorAssertionFunc + postOpAssertions func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) + }{ + { + name: "no existing file, adding a descriptor creates the file and returns the updated descriptor", + arg: v1.AgentInstallDesc{ + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + expected: createInstallDescriptor([]v1.AgentInstallDesc{ + { + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + }), + wantErr: assert.NoError, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) + }, + }, + { + name: "existing empty file, adding a descriptor updates the file and returns the updated descriptor", + setupDir: func(t *testing.T, tmpDir string) string { + markerFileName := "emptydescriptor.yaml" + err := os.WriteFile(filepath.Join(tmpDir, markerFileName), nil, 0o644) + require.NoError(t, err) + return markerFileName + }, + arg: v1.AgentInstallDesc{ + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + expected: createInstallDescriptor([]v1.AgentInstallDesc{ + { + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + }), + wantErr: assert.NoError, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) + }, + }, + { + name: "existing file with another install descriptor, adding a descriptor updates the file and the descriptor", + setupDir: func(t *testing.T, tmpDir string) string { + markerFileName := "filleddescriptor.yaml" + descriptor := v1.NewInstallDescriptor() + descriptor.AgentInstalls = []v1.AgentInstallDesc{ + { + OptionalTTLItem: v1.OptionalTTLItem{}, + Version: "0.0.0", + Hash: "oooooo", + VersionedHome: "date/elastic-agent-0.0.0-oooooo", + Flavor: "oooo", + Active: false, + }, + } + + buf := new(bytes.Buffer) + err := v1.WriteInstallDescriptor(buf, descriptor) + require.NoError(t, err, "error writing install descriptor during setup") + + outfilePath := filepath.Join(tmpDir, markerFileName) + err = os.WriteFile(outfilePath, buf.Bytes(), 0o644) + require.NoError(t, err, "error writing output file %s", markerFileName) + + return markerFileName + }, + arg: v1.AgentInstallDesc{ + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + expected: createInstallDescriptor([]v1.AgentInstallDesc{ + { + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + { + Version: "0.0.0", + Hash: "oooooo", + VersionedHome: "date/elastic-agent-0.0.0-oooooo", + Flavor: "oooo", + Active: false, + }, + }), + wantErr: assert.NoError, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) + }, + }, + { + name: "existing malformed install descriptor, adding a descriptor returns error", + setupDir: func(t *testing.T, tmpDir string) string { + + markerFileName := "malformeddescriptor" + outfilePath := filepath.Join(tmpDir, markerFileName) + err := os.WriteFile(outfilePath, []byte("malformed (non-YAML) content"), 0o644) + require.NoError(t, err, "error creating output file %s", markerFileName) + + return markerFileName + }, + arg: v1.AgentInstallDesc{ + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + expected: nil, + wantErr: assert.Error, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + markerAbsPath := filepath.Join(tmpDir, installMarker) + assert.FileExists(t, markerAbsPath, "install descriptor exists at %s", installMarker) + fileContent, err := os.ReadFile(markerAbsPath) + require.NoError(t, err, "error reading file %s", markerAbsPath) + assert.Equal(t, []byte("malformed (non-YAML) content"), fileContent, "install descriptor content should be left untouched") + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tmpDir := t.TempDir() + installMarkerFile := paths.MarkerFileName + if tc.setupDir != nil { + installMarkerFile = tc.setupDir(t, tmpDir) + } + + src := NewFileDescriptorSource(filepath.Join(tmpDir, installMarkerFile)) + + installDescriptor, err := src.AddInstallDesc(tc.arg) + tc.wantErr(t, err) + assert.Equal(t, tc.expected, installDescriptor) + + if tc.postOpAssertions != nil { + tc.postOpAssertions(t, tmpDir, installMarkerFile, installDescriptor) + } + }) + } +} + +func TestFileDescriptorSource_ModifyInstallDesc(t *testing.T) { + // useful variables for testcases + aMomentInTime := time.Now() + modifierFunctionError := errors.New("whoops! don't trust modifier functions") + calledModifierFunctionError := errors.New("this should not have been invoked") + + testcases := []struct { + name string + setupDir func(t *testing.T, tmpDir string) string + arg func(desc *v1.AgentInstallDesc) error + expected *v1.InstallDescriptor + wantErr assert.ErrorAssertionFunc + postOpAssertions func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) + }{ + { + name: "no existing file, modifying a descriptor returns error", + arg: func(desc *v1.AgentInstallDesc) error { + return calledModifierFunctionError + }, + expected: nil, + wantErr: assert.Error, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + assert.NoFileExists(t, filepath.Join(tmpDir, installMarker), "install descriptor should not exist at %s", installMarker) + }, + }, + { + name: "empty file, modifying a descriptor returns error", + setupDir: func(t *testing.T, tmpDir string) string { + markerFileName := "emptydescriptor.yaml" + err := os.WriteFile(filepath.Join(tmpDir, markerFileName), nil, 0o644) + require.NoError(t, err, "error creating output file %s", markerFileName) + return markerFileName + }, + arg: func(desc *v1.AgentInstallDesc) error { + return calledModifierFunctionError + }, + expected: nil, + wantErr: assert.Error, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + assert.FileExists(t, filepath.Join(tmpDir, installMarker), "install descriptor should not exist at %s", installMarker) + fileContent, err := os.ReadFile(filepath.Join(tmpDir, installMarker)) + assert.NoError(t, err, "error reading file %s", installMarker) + assert.Empty(t, fileContent, "install descriptor content should be empty") + }, + }, + { + name: "empty descriptor (not file), modifying a descriptor does not call modifier function", + setupDir: func(t *testing.T, tmpDir string) string { + installDescriptor := v1.NewInstallDescriptor() + buf := new(bytes.Buffer) + err := v1.WriteInstallDescriptor(buf, installDescriptor) + require.NoError(t, err, "error writing install descriptor during setup") + + markerFileName := "zerodescriptor.yaml" + err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) + require.NoError(t, err, "error creating output file %s", markerFileName) + return markerFileName + }, + arg: func(desc *v1.AgentInstallDesc) error { + return calledModifierFunctionError + }, + expected: createInstallDescriptor(nil), + wantErr: assert.NoError, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) + }, + }, + { + name: " valid descriptor with multiple installs, modifying a descriptor call modifier function on all installs", + setupDir: func(t *testing.T, tmpDir string) string { + markerFileName := "descriptor.yaml" + installDescriptor := createInstallDescriptor([]v1.AgentInstallDesc{ + { + Version: "4.5.6", + Hash: "ghijkl", + VersionedHome: "date/elastic-agent-4.5.6-ghijkl", + Flavor: "basic", + Active: false, + }, + { + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + { + Version: "0.0.0", + Hash: "oooooo", + VersionedHome: "date/elastic-agent-0.0.0-oooooo", + Flavor: "oooo", + Active: false, + }, + }) + + buf := new(bytes.Buffer) + err := v1.WriteInstallDescriptor(buf, installDescriptor) + require.NoError(t, err, "error writing install descriptor during setup") + + err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) + require.NoError(t, err, "error creating output file %s", markerFileName) + + return markerFileName + }, + arg: func(desc *v1.AgentInstallDesc) error { + // make version 4.5.6 active and all others inactive + if desc.Version == "4.5.6" { + desc.Active = true + } else { + desc.Active = false + desc.TTL = &aMomentInTime + } + + return nil + }, + expected: createInstallDescriptor([]v1.AgentInstallDesc{ + { + Version: "4.5.6", + Hash: "ghijkl", + VersionedHome: "date/elastic-agent-4.5.6-ghijkl", + Flavor: "basic", + Active: true, + }, + { + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: false, + OptionalTTLItem: v1.OptionalTTLItem{ + TTL: &aMomentInTime, + }, + }, + { + Version: "0.0.0", + Hash: "oooooo", + VersionedHome: "date/elastic-agent-0.0.0-oooooo", + Flavor: "oooo", + Active: false, + OptionalTTLItem: v1.OptionalTTLItem{ + TTL: &aMomentInTime, + }, + }, + }), + wantErr: assert.NoError, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) + }, + }, + { + name: " valid descriptor with installs, returns error if modifier function errors out and leaves the file untouched", + setupDir: func(t *testing.T, tmpDir string) string { + markerFileName := "descriptor.yaml" + installDescriptor := createInstallDescriptor([]v1.AgentInstallDesc{ + { + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + { + Version: "0.0.0", + Hash: "oooooo", + VersionedHome: "date/elastic-agent-0.0.0-oooooo", + Flavor: "oooo", + Active: false, + }, + }) + + buf := new(bytes.Buffer) + err := v1.WriteInstallDescriptor(buf, installDescriptor) + require.NoError(t, err, "error writing install descriptor during setup") + + err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) + require.NoError(t, err, "error creating output file %s", markerFileName) + return markerFileName + }, + arg: func(desc *v1.AgentInstallDesc) error { + // modify flavor and then return error + desc.Flavor = "touched" + return modifierFunctionError + }, + expected: nil, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.ErrorIs(t, err, modifierFunctionError, i) + }, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + untouchedDescriptor := createInstallDescriptor([]v1.AgentInstallDesc{ + { + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + { + Version: "0.0.0", + Hash: "oooooo", + VersionedHome: "date/elastic-agent-0.0.0-oooooo", + Flavor: "oooo", + Active: false, + }, + }) + checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), untouchedDescriptor) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tmpDir := t.TempDir() + installMarkerFile := paths.MarkerFileName + if tc.setupDir != nil { + installMarkerFile = tc.setupDir(t, tmpDir) + } + + src := NewFileDescriptorSource(filepath.Join(tmpDir, installMarkerFile)) + + installDescriptor, err := src.ModifyInstallDesc(tc.arg) + tc.wantErr(t, err) + assert.Equal(t, tc.expected, installDescriptor) + + if tc.postOpAssertions != nil { + tc.postOpAssertions(t, tmpDir, installMarkerFile, installDescriptor) + } + }) + } +} + +func TestFileDescriptorSource_RemoveAgentInstallDesc(t *testing.T) { + testcases := []struct { + name string + setupDir func(t *testing.T, tmpDir string) string + arg string + expected *v1.InstallDescriptor + wantErr assert.ErrorAssertionFunc + postOpAssertions func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) + }{ + { + name: "no existing file, removing an agent install descriptor returns error", + arg: "data/elastic-agent-1.2.3-abcdef", + expected: nil, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.ErrorIs(t, err, os.ErrNotExist) + }, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + assert.NoFileExists(t, filepath.Join(tmpDir, installMarker), "install descriptor should not exist at %s", installMarker) + }, + }, + { + name: "empty file, removing an agent install descriptor returns error", + setupDir: func(t *testing.T, tmpDir string) string { + markerFileName := "emptydescriptor.yaml" + err := os.WriteFile(filepath.Join(tmpDir, markerFileName), nil, 0o644) + require.NoError(t, err, "error creating output file %s", markerFileName) + return markerFileName + }, + arg: "data/elastic-agent-1.2.3-abcdef", + expected: nil, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.ErrorIs(t, err, io.EOF) + }, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + assert.FileExists(t, filepath.Join(tmpDir, installMarker), "install descriptor should not exist at %s", installMarker) + fileContent, err := os.ReadFile(filepath.Join(tmpDir, installMarker)) + assert.NoError(t, err, "error reading file %s", installMarker) + assert.Empty(t, fileContent, "install descriptor content should be empty") + }, + }, + { + name: "empty descriptor (not file), removing an agent install descriptor should not return error", + setupDir: func(t *testing.T, tmpDir string) string { + installDescriptor := v1.NewInstallDescriptor() + buf := new(bytes.Buffer) + err := v1.WriteInstallDescriptor(buf, installDescriptor) + require.NoError(t, err, "error writing install descriptor during setup") + + markerFileName := "zerodescriptor.yaml" + err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) + require.NoError(t, err, "error creating output file %s", markerFileName) + return markerFileName + }, + arg: "data/elastic-agent-1.2.3-abcdef", + expected: createInstallDescriptor(nil), + wantErr: assert.NoError, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) + }, + }, + { + name: " valid descriptor with multiple installs, removing a descriptor will delete the entries matching the versionedHome", + setupDir: func(t *testing.T, tmpDir string) string { + markerFileName := "descriptor.yaml" + installDescriptor := createInstallDescriptor([]v1.AgentInstallDesc{ + { + Version: "4.5.6", + Hash: "ghijkl", + VersionedHome: "date/elastic-agent-4.5.6-ghijkl", + Flavor: "basic", + Active: false, + }, + { + Version: "1.2.3", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: true, + }, + { + Version: "0.0.0", + Hash: "oooooo", + VersionedHome: "date/elastic-agent-0.0.0-oooooo", + Flavor: "oooo", + Active: false, + }, + { + Version: "1.2.3 x2", + Hash: "abcdef", + VersionedHome: "date/elastic-agent-1.2.3-abcdef", + Flavor: "basic", + Active: false, + }, + }) + + buf := new(bytes.Buffer) + err := v1.WriteInstallDescriptor(buf, installDescriptor) + require.NoError(t, err, "error writing install descriptor during setup") + + err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) + require.NoError(t, err, "error creating output file %s", markerFileName) + + return markerFileName + }, + arg: "date/elastic-agent-1.2.3-abcdef", + expected: createInstallDescriptor([]v1.AgentInstallDesc{ + { + Version: "4.5.6", + Hash: "ghijkl", + VersionedHome: "date/elastic-agent-4.5.6-ghijkl", + Flavor: "basic", + Active: false, + }, + { + Version: "0.0.0", + Hash: "oooooo", + VersionedHome: "date/elastic-agent-0.0.0-oooooo", + Flavor: "oooo", + Active: false, + }, + }), + wantErr: assert.NoError, + postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { + checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tmpDir := t.TempDir() + installMarkerFile := paths.MarkerFileName + if tc.setupDir != nil { + installMarkerFile = tc.setupDir(t, tmpDir) + } + + src := NewFileDescriptorSource(filepath.Join(tmpDir, installMarkerFile)) + + installDescriptor, err := src.RemoveAgentInstallDesc(tc.arg) + tc.wantErr(t, err) + assert.Equal(t, tc.expected, installDescriptor) + + if tc.postOpAssertions != nil { + tc.postOpAssertions(t, tmpDir, installMarkerFile, installDescriptor) + } + }) + } +} + +func createInstallDescriptor(agentInstalls []v1.AgentInstallDesc) *v1.InstallDescriptor { + descriptor := v1.NewInstallDescriptor() + descriptor.AgentInstalls = agentInstalls + return descriptor +} + +func checkInstallDescriptorMatches(t *testing.T, markerFile string, descriptor *v1.InstallDescriptor) { + require.FileExists(t, markerFile, "install marker file should exist") + buf := new(bytes.Buffer) + err := v1.WriteInstallDescriptor(buf, descriptor) + require.NoError(t, err, "error marshaling install descriptor") + fileRawData, err := os.ReadFile(markerFile) + require.NoError(t, err, "error marshaling install descriptor") + + assert.YAMLEq(t, buf.String(), string(fileRawData), "install marker file should match marshalled install descriptor") +} From d48d8ad9f020b1dc6a68024729d8afa1ad5b8831 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Wed, 24 Sep 2025 17:46:51 +0200 Subject: [PATCH 06/18] wire agent install source at startup --- internal/pkg/agent/application/application.go | 41 ++++++++++++++++++- .../pkg/agent/application/upgrade/upgrade.go | 24 ++++++++--- internal/pkg/agent/cmd/run.go | 16 ++++---- internal/pkg/agent/install/install.go | 2 +- pkg/utils/manifest/version.go | 15 +++++++ 5 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 pkg/utils/manifest/version.go diff --git a/internal/pkg/agent/application/application.go b/internal/pkg/agent/application/application.go index 373c1257758..a8d726a02a8 100644 --- a/internal/pkg/agent/application/application.go +++ b/internal/pkg/agent/application/application.go @@ -14,6 +14,7 @@ import ( componentmonitoring "github.com/elastic/elastic-agent/internal/pkg/agent/application/monitoring/component" + v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/utils/install" "github.com/elastic/go-ucfg" @@ -66,7 +67,7 @@ func New( fleetInitTimeout time.Duration, disableMonitoring bool, override CfgOverrider, - initialUpgradeDetails *details.Details, + initialUpdateMarker *upgrade.UpdateMarker, modifiers ...component.PlatformModifier, ) (*coordinator.Coordinator, coordinator.ConfigManager, composable.Controller, error) { @@ -131,7 +132,37 @@ func New( // monitoring is not supported in bootstrap mode https://github.com/elastic/elastic-agent/issues/1761 isMonitoringSupported := !disableMonitoring && cfg.Settings.V1MonitoringEnabled - upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, cfg.Settings.Upgrade, agentInfo, new(upgrade.AgentWatcherHelper), install.NewFileDescriptorSource(filepath.Join(paths.Top(), paths.MarkerFileName))) + + var installDescriptorSource *install.FileDescriptorSource = nil + + installDescriptorSource = install.NewFileDescriptorSource(filepath.Join(paths.Top(), paths.MarkerFileName)) + if platform.OS != component.Container { + if initialUpdateMarker != nil && initialUpdateMarker.Details != nil && initialUpdateMarker.Details.State == details.StateRollback { + // Take the versionedHome of the version we rolledback from and remove it from the installation lists + _, removeInstallDescErr := installDescriptorSource.RemoveAgentInstallDesc(initialUpdateMarker.VersionedHome /* there should be the versionedHome from the upgrade marker here*/) + if removeInstallDescErr != nil { + log.Warnf("Error removing rolled back version %s installed in %s: %v", initialUpdateMarker.VersionedHome, initialUpdateMarker.VersionedHome, removeInstallDescErr) + } + + currentVersionedHome, _ := filepath.Rel(paths.Top(), paths.Home()) + // Set the current version as active and all the others as inactive + _, updateInstallDescErr := installDescriptorSource.ModifyInstallDesc(func(desc *v1.AgentInstallDesc) error { + if desc.VersionedHome == currentVersionedHome { + // set the current version as active and make sure it doesn't have a TTL + desc.Active = true + desc.TTL = nil + } else { + // any other install is not the active one + desc.Active = false + } + return nil + }) + if updateInstallDescErr != nil { + log.Warnf("Error activating current version installed in %s", currentVersionedHome) + } + } + } + upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, cfg.Settings.Upgrade, agentInfo, new(upgrade.AgentWatcherHelper), installDescriptorSource) if err != nil { return nil, nil, nil, fmt.Errorf("failed to create upgrader: %w", err) } @@ -155,6 +186,12 @@ func New( return nil, nil, nil, fmt.Errorf("failed to initialize runtime manager: %w", err) } + // prepare initialUpgradeDetails for injecting it in coordinator later on + var initialUpgradeDetails *details.Details + if initialUpdateMarker != nil && initialUpdateMarker.Details != nil { + initialUpgradeDetails = initialUpdateMarker.Details + } + var configMgr coordinator.ConfigManager var managed *managedConfigManager var compModifiers = []coordinator.ComponentsModifier{InjectAPMConfig} diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index 934aecc9c03..dbbf5a0195b 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -460,15 +460,30 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return nil, goerrors.Join(err, rollbackErr) } - //FIXME make it nicer + rollbackWindow := time.Duration(0) + if u.upgradeSettings != nil && u.upgradeSettings.Rollback != nil { + rollbackWindow = u.upgradeSettings.Rollback.Window + } + + var currentInstallTTL *time.Time = nil + if rollbackWindow > 0 { + currentInstallTTLVar := time.Now().Add(rollbackWindow) + currentInstallTTL = ¤tInstallTTLVar + } + _, err = u.installDescriptorSource.ModifyInstallDesc( func(desc *v1.AgentInstallDesc) error { if desc.VersionedHome == unpackRes.VersionedHome { desc.Active = true return nil + } else { + desc.Active = false + } + + if desc.VersionedHome == currentVersionedHome { + desc.TTL = currentInstallTTL } - desc.Active = false return nil }, ) @@ -501,10 +516,7 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s hash: release.Commit(), versionedHome: currentVersionedHome, } - rollbackWindow := time.Duration(0) - if u.upgradeSettings != nil && u.upgradeSettings.Rollback != nil { - rollbackWindow = u.upgradeSettings.Rollback.Window - } + if err := u.markUpgrade(u.log, paths.Data(), // data dir to place the marker in time.Now(), diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index e140b162a6a..496806e9ea3 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -31,7 +31,6 @@ import ( "github.com/elastic/elastic-agent-libs/service" "github.com/elastic/elastic-agent-system-metrics/report" "github.com/elastic/elastic-agent/internal/pkg/agent/vault" - "github.com/elastic/elastic-agent/internal/pkg/diagnostics" "github.com/elastic/elastic-agent/internal/pkg/agent/application" "github.com/elastic/elastic-agent/internal/pkg/agent/application/coordinator" @@ -43,7 +42,6 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/reexec" "github.com/elastic/elastic-agent/internal/pkg/agent/application/secret" "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade" - "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details" "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/agent/install" @@ -52,6 +50,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/cli" "github.com/elastic/elastic-agent/internal/pkg/config" monitoringCfg "github.com/elastic/elastic-agent/internal/pkg/core/monitoring/config" + "github.com/elastic/elastic-agent/internal/pkg/diagnostics" "github.com/elastic/elastic-agent/internal/pkg/release" "github.com/elastic/elastic-agent/pkg/component" "github.com/elastic/elastic-agent/pkg/control/v2/server" @@ -89,6 +88,7 @@ func newRunCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { testingMode, _ := cmd.Flags().GetBool("testing-mode") if err := run(nil, testingMode, fleetInitTimeout); err != nil && !errors.Is(err, context.Canceled) { fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage) + logExternal(fmt.Sprintf("%s run failed: %s", paths.BinaryName, err)) return err } return nil @@ -166,7 +166,7 @@ func runElasticAgentCritical( var errs []error // early handleUpgrade, but don't error yet - upgradeDetailsFromMarker, err := handleUpgrade() + initialUpdateMarker, err := handleUpgrade() if err != nil { errs = append(errs, fmt.Errorf("failed to handle upgrade: %w", err)) } @@ -232,7 +232,7 @@ func runElasticAgentCritical( } // actually run the agent now - err = runElasticAgent(ctx, cancel, baseLogger, l, cfg, override, stop, testingMode, fleetInitTimeout, upgradeDetailsFromMarker, modifiers...) + err = runElasticAgent(ctx, cancel, baseLogger, l, cfg, override, stop, testingMode, fleetInitTimeout, initialUpdateMarker, modifiers...) return logReturn(l, err) } @@ -247,7 +247,7 @@ func runElasticAgent( stop chan bool, testingMode bool, fleetInitTimeout time.Duration, - upgradeDetailsFromMarker *details.Details, + initialUpgradeMarker *upgrade.UpdateMarker, modifiers ...component.PlatformModifier, ) error { logLvl := logger.DefaultLogLevel @@ -351,7 +351,7 @@ func runElasticAgent( isBootstrap := configuration.IsFleetServerBootstrap(cfg.Fleet) coord, configMgr, _, err := application.New(ctx, l, baseLogger, logLvl, agentInfo, rex, tracer, testingMode, - fleetInitTimeout, isBootstrap, override, upgradeDetailsFromMarker, modifiers...) + fleetInitTimeout, isBootstrap, override, initialUpgradeMarker, modifiers...) if err != nil { return err } @@ -734,7 +734,7 @@ func setupMetrics( // handleUpgrade checks if agent is being run as part of an // ongoing upgrade operation, i.e. being re-exec'd and performs // any upgrade-specific work, if needed. -func handleUpgrade() (*details.Details, error) { +func handleUpgrade() (*upgrade.UpdateMarker, error) { upgradeMarker, err := upgrade.LoadMarker(paths.Data()) if err != nil { return nil, fmt.Errorf("unable to load upgrade marker to check if Agent is being upgraded: %w", err) @@ -753,7 +753,7 @@ func handleUpgrade() (*details.Details, error) { return nil, err } - return upgradeMarker.Details, nil + return upgradeMarker, nil } func ensureInstallMarkerPresent() error { diff --git a/internal/pkg/agent/install/install.go b/internal/pkg/agent/install/install.go index c2c5fa8184c..7a5734f5efc 100644 --- a/internal/pkg/agent/install/install.go +++ b/internal/pkg/agent/install/install.go @@ -73,7 +73,7 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p targetVersionedHome := filepath.FromSlash(pathMapper.Map(manifest.Package.VersionedHome)) - err = setupInstallPath(topPath, ownership, targetVersionedHome, manifest.Package.Version, flavor) + err = setupInstallPath(topPath, ownership, targetVersionedHome, manifestutils.GetFullVersion(manifest), flavor) if err != nil { return utils.FileOwner{}, fmt.Errorf("error setting up install path: %w", err) } diff --git a/pkg/utils/manifest/version.go b/pkg/utils/manifest/version.go new file mode 100644 index 00000000000..06a009b8be3 --- /dev/null +++ b/pkg/utils/manifest/version.go @@ -0,0 +1,15 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package manifest + +import v1 "github.com/elastic/elastic-agent/pkg/api/v1" + +func GetFullVersion(manifest *v1.PackageManifest) string { + version := manifest.Package.Version + if manifest.Package.Snapshot { + version += "-SNAPSHOT" + } + return version +} From f3d39b035b53e958105b825af8117d035d70d520 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Thu, 25 Sep 2025 14:24:09 +0200 Subject: [PATCH 07/18] Mark installs and available rollbacks correctly during upgrade --- internal/pkg/agent/application/application.go | 3 +- .../application/upgrade/rollback_test.go | 2 +- .../agent/application/upgrade/step_mark.go | 22 ++++--- .../application/upgrade/step_mark_test.go | 64 +++++++++++++------ .../pkg/agent/application/upgrade/upgrade.go | 48 ++++++++++---- .../agent/application/upgrade/upgrade_test.go | 2 +- 6 files changed, 98 insertions(+), 43 deletions(-) diff --git a/internal/pkg/agent/application/application.go b/internal/pkg/agent/application/application.go index a8d726a02a8..01605e382cf 100644 --- a/internal/pkg/agent/application/application.go +++ b/internal/pkg/agent/application/application.go @@ -158,9 +158,10 @@ func New( return nil }) if updateInstallDescErr != nil { - log.Warnf("Error activating current version installed in %s", currentVersionedHome) + log.Warnf("Error setting current version as active in installDescriptor: %s", updateInstallDescErr) } } + } upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, cfg.Settings.Upgrade, agentInfo, new(upgrade.AgentWatcherHelper), installDescriptorSource) if err != nil { diff --git a/internal/pkg/agent/application/upgrade/rollback_test.go b/internal/pkg/agent/application/upgrade/rollback_test.go index 9bb2f113a66..eab955d663a 100644 --- a/internal/pkg/agent/application/upgrade/rollback_test.go +++ b/internal/pkg/agent/application/upgrade/rollback_test.go @@ -769,6 +769,6 @@ func createUpdateMarker(t *testing.T, log *logger.Logger, topDir, newAgentVersio time.Now(), newAgentInstall, oldAgentInstall, - nil, nil, disableRollbackWindow) + nil, nil, nil) require.NoError(t, err, "error writing fake update marker") } diff --git a/internal/pkg/agent/application/upgrade/step_mark.go b/internal/pkg/agent/application/upgrade/step_mark.go index 3a279a631e9..2945daa9a1b 100644 --- a/internal/pkg/agent/application/upgrade/step_mark.go +++ b/internal/pkg/agent/application/upgrade/step_mark.go @@ -16,6 +16,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/fleetapi" + v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/core/logger" "github.com/elastic/elastic-agent/pkg/version" ) @@ -142,7 +143,7 @@ type updateActiveCommitFunc func(log *logger.Logger, topDirPath, hash string, wr // markUpgrade marks update happened so we can handle grace period func markUpgradeProvider(updateActiveCommit updateActiveCommitFunc, writeFile writeFileFunc) markUpgradeFunc { - return func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, rollbackWindow time.Duration) error { + return func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks []v1.AgentInstallDesc) error { if len(previousAgent.hash) > hashLen { previousAgent.hash = previousAgent.hash[:hashLen] @@ -160,16 +161,21 @@ func markUpgradeProvider(updateActiveCommit updateActiveCommitFunc, writeFile wr Details: upgradeDetails, } - if rollbackWindow > disableRollbackWindow && agent.parsedVersion != nil && !agent.parsedVersion.Less(*Version_9_2_0_SNAPSHOT) { + if agent.parsedVersion != nil && !agent.parsedVersion.Less(*Version_9_2_0_SNAPSHOT) { // if we have a not empty rollback window, write the prev version in the rollbacks_available field // we also need to check the destination version because the manual rollback and delayed cleanup will be // handled by that version of agent, so it needs to be recent enough - marker.RollbacksAvailable = []RollbackAvailable{ - { - Version: previousAgent.version, - Home: previousAgent.versionedHome, - ValidUntil: updatedOn.Add(rollbackWindow), - }, + for _, rollback := range availableRollbacks { + rollbackAvailable := RollbackAvailable{ + Version: rollback.Version, + Home: rollback.VersionedHome, + } + + if rollback.TTL != nil { + rollbackAvailable.ValidUntil = *rollback.TTL + } + + marker.RollbacksAvailable = append(marker.RollbacksAvailable, rollbackAvailable) } } diff --git a/internal/pkg/agent/application/upgrade/step_mark_test.go b/internal/pkg/agent/application/upgrade/step_mark_test.go index 6d53d75330b..440f79821e1 100644 --- a/internal/pkg/agent/application/upgrade/step_mark_test.go +++ b/internal/pkg/agent/application/upgrade/step_mark_test.go @@ -16,6 +16,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details" "github.com/elastic/elastic-agent/internal/pkg/fleetapi" + v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/core/logger" "github.com/elastic/elastic-agent/pkg/core/logger/loggertest" agtversion "github.com/elastic/elastic-agent/pkg/version" @@ -86,14 +87,15 @@ func TestMarkUpgrade(t *testing.T) { var parsed920SNAPSHOT = agtversion.NewParsedSemVer(9, 2, 0, "SNAPSHOT", "") // fix a timestamp (truncated to the second because of loss of precision during marshalling/unmarshalling) updatedOnNow := time.Now().UTC().Truncate(time.Second) + twentyFourHoursFromNow := updatedOnNow.Add(24 * time.Hour) type args struct { - updatedOn time.Time - currentAgent agentInstall - previousAgent agentInstall - action *fleetapi.ActionUpgrade - details *details.Details - rollbackWindow time.Duration + updatedOn time.Time + currentAgent agentInstall + previousAgent agentInstall + action *fleetapi.ActionUpgrade + details *details.Details + availableRollbacks []v1.AgentInstallDesc } type workingDirHook func(t *testing.T, dataDir string) @@ -130,9 +132,9 @@ func TestMarkUpgrade(t *testing.T) { hash: "prvagt", versionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), }, - action: nil, - details: details.NewDetails("4.5.6-SNAPSHOT", details.StateReplacing, ""), - rollbackWindow: 0, + action: nil, + details: details.NewDetails("4.5.6-SNAPSHOT", details.StateReplacing, ""), + availableRollbacks: nil, }, wantErr: assert.Error, }, @@ -152,9 +154,9 @@ func TestMarkUpgrade(t *testing.T) { hash: "prvagt", versionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), }, - action: nil, - details: details.NewDetails("4.5.6-SNAPSHOT", details.StateReplacing, ""), - rollbackWindow: 0, + action: nil, + details: details.NewDetails("4.5.6-SNAPSHOT", details.StateReplacing, ""), + availableRollbacks: nil, }, wantErr: assert.NoError, assertAfterMark: func(t *testing.T, dataDir string) { @@ -197,9 +199,20 @@ func TestMarkUpgrade(t *testing.T) { hash: "prvagt", versionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), }, - action: nil, - details: details.NewDetails("4.5.6-SNAPSHOT", details.StateReplacing, ""), - rollbackWindow: 7 * 24 * time.Hour, + action: nil, + details: details.NewDetails("4.5.6-SNAPSHOT", details.StateReplacing, ""), + availableRollbacks: []v1.AgentInstallDesc{ + { + OptionalTTLItem: v1.OptionalTTLItem{ + TTL: &twentyFourHoursFromNow, + }, + Version: "1.2.3-SNAPSHOT", + Hash: "prvagt", + VersionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), + Flavor: "basic", + Active: false, + }, + }, }, wantErr: assert.NoError, assertAfterMark: func(t *testing.T, dataDir string) { @@ -241,9 +254,20 @@ func TestMarkUpgrade(t *testing.T) { hash: "prvagt", versionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), }, - action: nil, - details: details.NewDetails("9.2.0-SNAPSHOT", details.StateReplacing, ""), - rollbackWindow: 7 * 24 * time.Hour, + action: nil, + details: details.NewDetails("9.2.0-SNAPSHOT", details.StateReplacing, ""), + availableRollbacks: []v1.AgentInstallDesc{ + { + OptionalTTLItem: v1.OptionalTTLItem{ + TTL: &twentyFourHoursFromNow, + }, + Version: "1.2.3-SNAPSHOT", + Hash: "prvagt", + VersionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), + Flavor: "basic", + Active: false, + }, + }, }, wantErr: assert.NoError, assertAfterMark: func(t *testing.T, dataDir string) { @@ -270,7 +294,7 @@ func TestMarkUpgrade(t *testing.T) { { Version: "1.2.3-SNAPSHOT", Home: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), - ValidUntil: updatedOnNow.Add(7 * 24 * time.Hour), + ValidUntil: twentyFourHoursFromNow, }, }, } @@ -295,7 +319,7 @@ func TestMarkUpgrade(t *testing.T) { tc.setupBeforeMark(t, dataDir) } - err := markUpgrade(log, dataDir, tc.args.updatedOn, tc.args.currentAgent, tc.args.previousAgent, tc.args.action, tc.args.details, tc.args.rollbackWindow) + err := markUpgrade(log, dataDir, tc.args.updatedOn, tc.args.currentAgent, tc.args.previousAgent, tc.args.action, tc.args.details, tc.args.availableRollbacks) tc.wantErr(t, err) if tc.assertAfterMark != nil { tc.assertAfterMark(t, dataDir) diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index dbbf5a0195b..c6786177fd1 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -91,7 +91,7 @@ type unpackHandler interface { type copyActionStoreFunc func(log *logger.Logger, newHome string) error type copyRunDirectoryFunc func(log *logger.Logger, oldRunPath, newRunPath string) error type fileDirCopyFunc func(from, to string, opts ...filecopy.Options) error -type markUpgradeFunc func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, rollbackWindow time.Duration) error +type markUpgradeFunc func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks []v1.AgentInstallDesc) error type changeSymlinkFunc func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error type rollbackInstallFunc func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error @@ -414,7 +414,6 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return nil, fmt.Errorf("calculating home path relative to top, home: %q top: %q : %w", paths.Home(), paths.Top(), err) } - //FIXME make it nicer _, err = u.installDescriptorSource.AddInstallDesc( v1.AgentInstallDesc{Version: version, VersionedHome: unpackRes.VersionedHome, Hash: unpackRes.Hash, Flavor: detectedFlavor, Active: false}, ) @@ -465,13 +464,9 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s rollbackWindow = u.upgradeSettings.Rollback.Window } - var currentInstallTTL *time.Time = nil - if rollbackWindow > 0 { - currentInstallTTLVar := time.Now().Add(rollbackWindow) - currentInstallTTL = ¤tInstallTTLVar - } - - _, err = u.installDescriptorSource.ModifyInstallDesc( + // timestamp marking the moment the links have been rotated. It will be used for TTL calculations of pre-existing elastic-agent installs + rotationTimestamp := time.Now() + modifiedInstallDescriptor, err := u.installDescriptorSource.ModifyInstallDesc( func(desc *v1.AgentInstallDesc) error { if desc.VersionedHome == unpackRes.VersionedHome { desc.Active = true @@ -480,8 +475,9 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s desc.Active = false } + // set the TTL only for the current install if desc.VersionedHome == currentVersionedHome { - desc.TTL = currentInstallTTL + desc.TTL = getCurrentInstallTTL(rollbackWindow, rotationTimestamp) } return nil @@ -489,7 +485,7 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s ) if err != nil { - err = fmt.Errorf("error encountered when adding install description: %w", err) + err = fmt.Errorf("error encountered when setting new install description as active: %w", err) rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), unpackRes.VersionedHome, currentVersionedHome, u.installDescriptorSource) if rollbackErr != nil { @@ -517,12 +513,14 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s versionedHome: currentVersionedHome, } + availableRollbacks := getAvailableRollbacks(rollbackWindow, rotationTimestamp, unpackRes.VersionedHome, modifiedInstallDescriptor) + if err := u.markUpgrade(u.log, paths.Data(), // data dir to place the marker in time.Now(), current, // new agent version data previous, // old agent version data - action, det, rollbackWindow); err != nil { + action, det, availableRollbacks); err != nil { u.log.Errorw("Rolling back: marking upgrade failed", "error.message", err) rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.installDescriptorSource) return nil, goerrors.Join(err, rollbackErr) @@ -556,6 +554,32 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return cb, nil } +func getAvailableRollbacks(rollbackWindow time.Duration, now time.Time, newVersionedHome string, descriptor *v1.InstallDescriptor) []v1.AgentInstallDesc { + if rollbackWindow == 0 { + // if there's no rollback window it means that no rollback should survive the watcher cleanup at the end of the grace period. + return nil + } + + res := make([]v1.AgentInstallDesc, 0, len(descriptor.AgentInstalls)) + for _, installDesc := range descriptor.AgentInstalls { + if installDesc.VersionedHome != newVersionedHome && (installDesc.TTL == nil || now.Before(*installDesc.TTL)) { + // this is a valid possible rollback target, so we have to keep it available beyond the end of the grace period + res = append(res, installDesc) + } + } + return res +} + +func getCurrentInstallTTL(rollbackWindow time.Duration, now time.Time) *time.Time { + if rollbackWindow == 0 { + // no rollback window, no TTL + return nil + } + + currentInstallTTLVar := now.Add(rollbackWindow) + return ¤tInstallTTLVar +} + func (u *Upgrader) rollbackToPreviousVersion(ctx context.Context, topDir string, now time.Time, version string, action *fleetapi.ActionUpgrade) (reexec.ShutdownCallbackFn, error) { if version == "" { return nil, ErrEmptyRollbackVersion diff --git a/internal/pkg/agent/application/upgrade/upgrade_test.go b/internal/pkg/agent/application/upgrade/upgrade_test.go index b7385f938d2..55ede8e217f 100644 --- a/internal/pkg/agent/application/upgrade/upgrade_test.go +++ b/internal/pkg/agent/application/upgrade/upgrade_test.go @@ -1609,7 +1609,7 @@ func TestUpgradeErrorHandling(t *testing.T) { upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error { return nil } - upgrader.markUpgrade = func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, rollbackWindow time.Duration) error { + upgrader.markUpgrade = func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks []v1.AgentInstallDesc) error { return testError } }, From 3a4c3b6fc0ce670e9ab773d61943340087847f27 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Tue, 7 Oct 2025 12:08:47 +0200 Subject: [PATCH 08/18] Treat install registry errors as non-fatal during upgrade --- .../pkg/agent/application/upgrade/upgrade.go | 76 +++++++++---------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index c6786177fd1..d4fe4b2d89b 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -414,18 +414,7 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return nil, fmt.Errorf("calculating home path relative to top, home: %q top: %q : %w", paths.Home(), paths.Top(), err) } - _, err = u.installDescriptorSource.AddInstallDesc( - v1.AgentInstallDesc{Version: version, VersionedHome: unpackRes.VersionedHome, Hash: unpackRes.Hash, Flavor: detectedFlavor, Active: false}, - ) - if err != nil { - err = fmt.Errorf("error encountered when adding install description: %w", err) - - rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), unpackRes.VersionedHome, currentVersionedHome, u.installDescriptorSource) - if rollbackErr != nil { - return nil, goerrors.Join(err, rollbackErr) - } - return nil, err - } + u.addNewInstallToRegistry(version, unpackRes, detectedFlavor) newHash := unpackRes.Hash if newHash == "" { @@ -459,40 +448,14 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return nil, goerrors.Join(err, rollbackErr) } - rollbackWindow := time.Duration(0) + rollbackWindow := disableRollbackWindow if u.upgradeSettings != nil && u.upgradeSettings.Rollback != nil { rollbackWindow = u.upgradeSettings.Rollback.Window } // timestamp marking the moment the links have been rotated. It will be used for TTL calculations of pre-existing elastic-agent installs rotationTimestamp := time.Now() - modifiedInstallDescriptor, err := u.installDescriptorSource.ModifyInstallDesc( - func(desc *v1.AgentInstallDesc) error { - if desc.VersionedHome == unpackRes.VersionedHome { - desc.Active = true - return nil - } else { - desc.Active = false - } - - // set the TTL only for the current install - if desc.VersionedHome == currentVersionedHome { - desc.TTL = getCurrentInstallTTL(rollbackWindow, rotationTimestamp) - } - - return nil - }, - ) - - if err != nil { - err = fmt.Errorf("error encountered when setting new install description as active: %w", err) - - rollbackErr := rollbackInstall(ctx, u.log, paths.Top(), unpackRes.VersionedHome, currentVersionedHome, u.installDescriptorSource) - if rollbackErr != nil { - return nil, goerrors.Join(err, rollbackErr) - } - return nil, err - } + modifiedInstallDescriptor := u.activateInstallInRegistry(unpackRes.VersionedHome, currentVersionedHome, rollbackWindow, rotationTimestamp) // We rotated the symlink successfully: prepare the current and previous agent installation details for the update marker // In update marker the `current` agent install is the one where the symlink is pointing (the new one we didn't start yet) @@ -554,6 +517,39 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return cb, nil } +func (u *Upgrader) addNewInstallToRegistry(version string, unpackRes UnpackResult, detectedFlavor string) { + _, err := u.installDescriptorSource.AddInstallDesc( + v1.AgentInstallDesc{Version: version, VersionedHome: unpackRes.VersionedHome, Hash: unpackRes.Hash, Flavor: detectedFlavor, Active: false}, + ) + if err != nil { + u.log.Warnf("error encountered when adding install description of new agent version: %s", err.Error()) + } +} + +func (u *Upgrader) activateInstallInRegistry(newVersionedHome, currentVersionedHome string, rollbackWindow time.Duration, rotationTimestamp time.Time) *v1.InstallDescriptor { + modifiedInstallDescriptor, err := u.installDescriptorSource.ModifyInstallDesc( + func(desc *v1.AgentInstallDesc) error { + if desc.VersionedHome == newVersionedHome { + desc.Active = true + return nil + } else { + desc.Active = false + } + + // set the TTL only for the current install + if desc.VersionedHome == currentVersionedHome { + desc.TTL = getCurrentInstallTTL(rollbackWindow, rotationTimestamp) + } + + return nil + }, + ) + if err != nil { + u.log.Warnf("error encountered when setting new install description as active: %s", err.Error()) + } + return modifiedInstallDescriptor +} + func getAvailableRollbacks(rollbackWindow time.Duration, now time.Time, newVersionedHome string, descriptor *v1.InstallDescriptor) []v1.AgentInstallDesc { if rollbackWindow == 0 { // if there's no rollback window it means that no rollback should survive the watcher cleanup at the end of the grace period. From d4cc617fceca97994a1f51e6d32bf24606a795f0 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Thu, 9 Oct 2025 09:13:42 +0200 Subject: [PATCH 09/18] Remove install registry and add available rollbacks list under /data remove InstallRegistry in favor of .ttl marker files --- .mockery.yaml | 2 +- internal/pkg/agent/application/application.go | 36 +- .../coordinator/coordinator_unit_test.go | 3 +- .../mock_availablerollbackssource_test.go | 139 +++++ .../mock_installdescriptorsource_test.go | 214 ------- .../agent/application/upgrade/step_mark.go | 49 +- .../application/upgrade/step_mark_test.go | 81 +-- .../application/upgrade/ttl_marker_source.go | 118 ++++ .../upgrade/ttl_marker_source_test.go | 335 ++++++++++ .../pkg/agent/application/upgrade/upgrade.go | 158 ++--- .../agent/application/upgrade/upgrade_test.go | 121 ++-- internal/pkg/agent/cmd/run.go | 2 +- internal/pkg/agent/install/install.go | 29 +- internal/pkg/agent/install/install_test.go | 15 +- pkg/api/v1/install.go | 53 -- pkg/utils/install/file_source.go | 123 ---- pkg/utils/install/file_source_test.go | 583 ------------------ 17 files changed, 743 insertions(+), 1318 deletions(-) create mode 100644 internal/pkg/agent/application/upgrade/mock_availablerollbackssource_test.go delete mode 100644 internal/pkg/agent/application/upgrade/mock_installdescriptorsource_test.go create mode 100644 internal/pkg/agent/application/upgrade/ttl_marker_source.go create mode 100644 internal/pkg/agent/application/upgrade/ttl_marker_source_test.go delete mode 100644 pkg/api/v1/install.go delete mode 100644 pkg/utils/install/file_source.go delete mode 100644 pkg/utils/install/file_source_test.go diff --git a/.mockery.yaml b/.mockery.yaml index a35fa8ae490..8d8a896feb3 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -18,7 +18,7 @@ packages: interfaces: WatcherHelper: {} watcherGrappler: {} - installDescriptorSource: {} + availableRollbacksSource: {} github.com/elastic/elastic-agent/internal/pkg/agent/cmd: interfaces: agentWatcher: {} diff --git a/internal/pkg/agent/application/application.go b/internal/pkg/agent/application/application.go index 01605e382cf..949f4f02e64 100644 --- a/internal/pkg/agent/application/application.go +++ b/internal/pkg/agent/application/application.go @@ -7,15 +7,12 @@ package application import ( "context" "fmt" - "path/filepath" "time" "go.elastic.co/apm/v2" componentmonitoring "github.com/elastic/elastic-agent/internal/pkg/agent/application/monitoring/component" - v1 "github.com/elastic/elastic-agent/pkg/api/v1" - "github.com/elastic/elastic-agent/pkg/utils/install" "github.com/elastic/go-ucfg" "github.com/elastic/elastic-agent-libs/logp" @@ -133,37 +130,8 @@ func New( // monitoring is not supported in bootstrap mode https://github.com/elastic/elastic-agent/issues/1761 isMonitoringSupported := !disableMonitoring && cfg.Settings.V1MonitoringEnabled - var installDescriptorSource *install.FileDescriptorSource = nil - - installDescriptorSource = install.NewFileDescriptorSource(filepath.Join(paths.Top(), paths.MarkerFileName)) - if platform.OS != component.Container { - if initialUpdateMarker != nil && initialUpdateMarker.Details != nil && initialUpdateMarker.Details.State == details.StateRollback { - // Take the versionedHome of the version we rolledback from and remove it from the installation lists - _, removeInstallDescErr := installDescriptorSource.RemoveAgentInstallDesc(initialUpdateMarker.VersionedHome /* there should be the versionedHome from the upgrade marker here*/) - if removeInstallDescErr != nil { - log.Warnf("Error removing rolled back version %s installed in %s: %v", initialUpdateMarker.VersionedHome, initialUpdateMarker.VersionedHome, removeInstallDescErr) - } - - currentVersionedHome, _ := filepath.Rel(paths.Top(), paths.Home()) - // Set the current version as active and all the others as inactive - _, updateInstallDescErr := installDescriptorSource.ModifyInstallDesc(func(desc *v1.AgentInstallDesc) error { - if desc.VersionedHome == currentVersionedHome { - // set the current version as active and make sure it doesn't have a TTL - desc.Active = true - desc.TTL = nil - } else { - // any other install is not the active one - desc.Active = false - } - return nil - }) - if updateInstallDescErr != nil { - log.Warnf("Error setting current version as active in installDescriptor: %s", updateInstallDescErr) - } - } - - } - upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, cfg.Settings.Upgrade, agentInfo, new(upgrade.AgentWatcherHelper), installDescriptorSource) + availableRollbacksSource := upgrade.NewTTLMarkerRegistry(paths.Top()) + upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, cfg.Settings.Upgrade, agentInfo, new(upgrade.AgentWatcherHelper), availableRollbacksSource) if err != nil { return nil, nil, nil, fmt.Errorf("failed to create upgrader: %w", err) } diff --git a/internal/pkg/agent/application/coordinator/coordinator_unit_test.go b/internal/pkg/agent/application/coordinator/coordinator_unit_test.go index 92b11e3e2d1..e9c661d6f57 100644 --- a/internal/pkg/agent/application/coordinator/coordinator_unit_test.go +++ b/internal/pkg/agent/application/coordinator/coordinator_unit_test.go @@ -29,7 +29,6 @@ import ( "github.com/elastic/elastic-agent-client/v7/pkg/proto" "github.com/elastic/elastic-agent/internal/pkg/fleetapi/acker" "github.com/elastic/elastic-agent/internal/pkg/testutils" - "github.com/elastic/elastic-agent/pkg/utils/install" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/status" "go.opentelemetry.io/collector/component/componentstatus" @@ -469,7 +468,7 @@ func TestCoordinatorReportsInvalidPolicy(t *testing.T) { }() tmpDir := t.TempDir() - upgradeMgr, err := upgrade.NewUpgrader(log, &artifact.Config{}, nil, &info.AgentInfo{}, new(upgrade.AgentWatcherHelper), install.NewFileDescriptorSource(filepath.Join(tmpDir, paths.MarkerFileName))) + upgradeMgr, err := upgrade.NewUpgrader(log, &artifact.Config{}, nil, &info.AgentInfo{}, new(upgrade.AgentWatcherHelper), upgrade.NewTTLMarkerRegistry(tmpDir)) require.NoError(t, err, "errored when creating a new upgrader") // Channels have buffer length 1, so we don't have to run on multiple diff --git a/internal/pkg/agent/application/upgrade/mock_availablerollbackssource_test.go b/internal/pkg/agent/application/upgrade/mock_availablerollbackssource_test.go new file mode 100644 index 00000000000..e4a383e3576 --- /dev/null +++ b/internal/pkg/agent/application/upgrade/mock_availablerollbackssource_test.go @@ -0,0 +1,139 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package upgrade + +import mock "github.com/stretchr/testify/mock" + +// mockAvailableRollbacksSource is an autogenerated mock type for the availableRollbacksSource type +type mockAvailableRollbacksSource struct { + mock.Mock +} + +type mockAvailableRollbacksSource_Expecter struct { + mock *mock.Mock +} + +func (_m *mockAvailableRollbacksSource) EXPECT() *mockAvailableRollbacksSource_Expecter { + return &mockAvailableRollbacksSource_Expecter{mock: &_m.Mock} +} + +// Get provides a mock function with no fields +func (_m *mockAvailableRollbacksSource) Get() (map[string]TTLMarker, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 map[string]TTLMarker + var r1 error + if rf, ok := ret.Get(0).(func() (map[string]TTLMarker, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[string]TTLMarker); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]TTLMarker) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockAvailableRollbacksSource_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type mockAvailableRollbacksSource_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +func (_e *mockAvailableRollbacksSource_Expecter) Get() *mockAvailableRollbacksSource_Get_Call { + return &mockAvailableRollbacksSource_Get_Call{Call: _e.mock.On("Get")} +} + +func (_c *mockAvailableRollbacksSource_Get_Call) Run(run func()) *mockAvailableRollbacksSource_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockAvailableRollbacksSource_Get_Call) Return(_a0 map[string]TTLMarker, _a1 error) *mockAvailableRollbacksSource_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockAvailableRollbacksSource_Get_Call) RunAndReturn(run func() (map[string]TTLMarker, error)) *mockAvailableRollbacksSource_Get_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function with given fields: _a0 +func (_m *mockAvailableRollbacksSource) Set(_a0 map[string]TTLMarker) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 error + if rf, ok := ret.Get(0).(func(map[string]TTLMarker) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockAvailableRollbacksSource_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type mockAvailableRollbacksSource_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - _a0 map[string]TTLMarker +func (_e *mockAvailableRollbacksSource_Expecter) Set(_a0 interface{}) *mockAvailableRollbacksSource_Set_Call { + return &mockAvailableRollbacksSource_Set_Call{Call: _e.mock.On("Set", _a0)} +} + +func (_c *mockAvailableRollbacksSource_Set_Call) Run(run func(_a0 map[string]TTLMarker)) *mockAvailableRollbacksSource_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]TTLMarker)) + }) + return _c +} + +func (_c *mockAvailableRollbacksSource_Set_Call) Return(_a0 error) *mockAvailableRollbacksSource_Set_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockAvailableRollbacksSource_Set_Call) RunAndReturn(run func(map[string]TTLMarker) error) *mockAvailableRollbacksSource_Set_Call { + _c.Call.Return(run) + return _c +} + +// newMockAvailableRollbacksSource creates a new instance of mockAvailableRollbacksSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockAvailableRollbacksSource(t interface { + mock.TestingT + Cleanup(func()) +}) *mockAvailableRollbacksSource { + mock := &mockAvailableRollbacksSource{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/pkg/agent/application/upgrade/mock_installdescriptorsource_test.go b/internal/pkg/agent/application/upgrade/mock_installdescriptorsource_test.go deleted file mode 100644 index 4d23507749d..00000000000 --- a/internal/pkg/agent/application/upgrade/mock_installdescriptorsource_test.go +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License 2.0; -// you may not use this file except in compliance with the Elastic License 2.0. - -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package upgrade - -import ( - mock "github.com/stretchr/testify/mock" - - v1 "github.com/elastic/elastic-agent/pkg/api/v1" -) - -// mockInstallDescriptorSource is an autogenerated mock type for the installDescriptorSource type -type mockInstallDescriptorSource struct { - mock.Mock -} - -type mockInstallDescriptorSource_Expecter struct { - mock *mock.Mock -} - -func (_m *mockInstallDescriptorSource) EXPECT() *mockInstallDescriptorSource_Expecter { - return &mockInstallDescriptorSource_Expecter{mock: &_m.Mock} -} - -// AddInstallDesc provides a mock function with given fields: desc -func (_m *mockInstallDescriptorSource) AddInstallDesc(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) { - ret := _m.Called(desc) - - if len(ret) == 0 { - panic("no return value specified for AddInstallDesc") - } - - var r0 *v1.InstallDescriptor - var r1 error - if rf, ok := ret.Get(0).(func(v1.AgentInstallDesc) (*v1.InstallDescriptor, error)); ok { - return rf(desc) - } - if rf, ok := ret.Get(0).(func(v1.AgentInstallDesc) *v1.InstallDescriptor); ok { - r0 = rf(desc) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.InstallDescriptor) - } - } - - if rf, ok := ret.Get(1).(func(v1.AgentInstallDesc) error); ok { - r1 = rf(desc) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockInstallDescriptorSource_AddInstallDesc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddInstallDesc' -type mockInstallDescriptorSource_AddInstallDesc_Call struct { - *mock.Call -} - -// AddInstallDesc is a helper method to define mock.On call -// - desc v1.AgentInstallDesc -func (_e *mockInstallDescriptorSource_Expecter) AddInstallDesc(desc interface{}) *mockInstallDescriptorSource_AddInstallDesc_Call { - return &mockInstallDescriptorSource_AddInstallDesc_Call{Call: _e.mock.On("AddInstallDesc", desc)} -} - -func (_c *mockInstallDescriptorSource_AddInstallDesc_Call) Run(run func(desc v1.AgentInstallDesc)) *mockInstallDescriptorSource_AddInstallDesc_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(v1.AgentInstallDesc)) - }) - return _c -} - -func (_c *mockInstallDescriptorSource_AddInstallDesc_Call) Return(_a0 *v1.InstallDescriptor, _a1 error) *mockInstallDescriptorSource_AddInstallDesc_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockInstallDescriptorSource_AddInstallDesc_Call) RunAndReturn(run func(v1.AgentInstallDesc) (*v1.InstallDescriptor, error)) *mockInstallDescriptorSource_AddInstallDesc_Call { - _c.Call.Return(run) - return _c -} - -// ModifyInstallDesc provides a mock function with given fields: modifierFunc -func (_m *mockInstallDescriptorSource) ModifyInstallDesc(modifierFunc func(*v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error) { - ret := _m.Called(modifierFunc) - - if len(ret) == 0 { - panic("no return value specified for ModifyInstallDesc") - } - - var r0 *v1.InstallDescriptor - var r1 error - if rf, ok := ret.Get(0).(func(func(*v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error)); ok { - return rf(modifierFunc) - } - if rf, ok := ret.Get(0).(func(func(*v1.AgentInstallDesc) error) *v1.InstallDescriptor); ok { - r0 = rf(modifierFunc) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.InstallDescriptor) - } - } - - if rf, ok := ret.Get(1).(func(func(*v1.AgentInstallDesc) error) error); ok { - r1 = rf(modifierFunc) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockInstallDescriptorSource_ModifyInstallDesc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ModifyInstallDesc' -type mockInstallDescriptorSource_ModifyInstallDesc_Call struct { - *mock.Call -} - -// ModifyInstallDesc is a helper method to define mock.On call -// - modifierFunc func(*v1.AgentInstallDesc) error -func (_e *mockInstallDescriptorSource_Expecter) ModifyInstallDesc(modifierFunc interface{}) *mockInstallDescriptorSource_ModifyInstallDesc_Call { - return &mockInstallDescriptorSource_ModifyInstallDesc_Call{Call: _e.mock.On("ModifyInstallDesc", modifierFunc)} -} - -func (_c *mockInstallDescriptorSource_ModifyInstallDesc_Call) Run(run func(modifierFunc func(*v1.AgentInstallDesc) error)) *mockInstallDescriptorSource_ModifyInstallDesc_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(func(*v1.AgentInstallDesc) error)) - }) - return _c -} - -func (_c *mockInstallDescriptorSource_ModifyInstallDesc_Call) Return(_a0 *v1.InstallDescriptor, _a1 error) *mockInstallDescriptorSource_ModifyInstallDesc_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockInstallDescriptorSource_ModifyInstallDesc_Call) RunAndReturn(run func(func(*v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error)) *mockInstallDescriptorSource_ModifyInstallDesc_Call { - _c.Call.Return(run) - return _c -} - -// RemoveAgentInstallDesc provides a mock function with given fields: versionedHome -func (_m *mockInstallDescriptorSource) RemoveAgentInstallDesc(versionedHome string) (*v1.InstallDescriptor, error) { - ret := _m.Called(versionedHome) - - if len(ret) == 0 { - panic("no return value specified for RemoveAgentInstallDesc") - } - - var r0 *v1.InstallDescriptor - var r1 error - if rf, ok := ret.Get(0).(func(string) (*v1.InstallDescriptor, error)); ok { - return rf(versionedHome) - } - if rf, ok := ret.Get(0).(func(string) *v1.InstallDescriptor); ok { - r0 = rf(versionedHome) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.InstallDescriptor) - } - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(versionedHome) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockInstallDescriptorSource_RemoveAgentInstallDesc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveAgentInstallDesc' -type mockInstallDescriptorSource_RemoveAgentInstallDesc_Call struct { - *mock.Call -} - -// RemoveAgentInstallDesc is a helper method to define mock.On call -// - versionedHome string -func (_e *mockInstallDescriptorSource_Expecter) RemoveAgentInstallDesc(versionedHome interface{}) *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call { - return &mockInstallDescriptorSource_RemoveAgentInstallDesc_Call{Call: _e.mock.On("RemoveAgentInstallDesc", versionedHome)} -} - -func (_c *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call) Run(run func(versionedHome string)) *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) - }) - return _c -} - -func (_c *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call) Return(_a0 *v1.InstallDescriptor, _a1 error) *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call) RunAndReturn(run func(string) (*v1.InstallDescriptor, error)) *mockInstallDescriptorSource_RemoveAgentInstallDesc_Call { - _c.Call.Return(run) - return _c -} - -// newMockInstallDescriptorSource creates a new instance of mockInstallDescriptorSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockInstallDescriptorSource(t interface { - mock.TestingT - Cleanup(func()) -}) *mockInstallDescriptorSource { - mock := &mockInstallDescriptorSource{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/pkg/agent/application/upgrade/step_mark.go b/internal/pkg/agent/application/upgrade/step_mark.go index 2945daa9a1b..7f93ff43a27 100644 --- a/internal/pkg/agent/application/upgrade/step_mark.go +++ b/internal/pkg/agent/application/upgrade/step_mark.go @@ -16,7 +16,6 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/fleetapi" - v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/core/logger" "github.com/elastic/elastic-agent/pkg/version" ) @@ -24,10 +23,9 @@ import ( const markerFilename = ".update-marker" const disableRollbackWindow = time.Duration(0) -// RollbackAvailable identifies an elastic-agent install available for rollback -type RollbackAvailable struct { +// TTLMarker marks an elastic-agent install available for rollback +type TTLMarker struct { Version string `json:"version" yaml:"version"` - Home string `json:"home" yaml:"home"` ValidUntil time.Time `json:"valid_until" yaml:"valid_until"` } @@ -56,7 +54,7 @@ type UpdateMarker struct { Details *details.Details `json:"details,omitempty" yaml:"details,omitempty"` - RollbacksAvailable []RollbackAvailable `json:"rollbacks_available,omitempty" yaml:"rollbacks_available,omitempty"` + RollbacksAvailable map[string]TTLMarker `json:"rollbacks_available,omitempty" yaml:"rollbacks_available,omitempty"` } // GetActionID returns the Fleet Action ID associated with the @@ -113,7 +111,7 @@ type updateMarkerSerializer struct { Acked bool `yaml:"acked"` Action *MarkerActionUpgrade `yaml:"action"` Details *details.Details `yaml:"details"` - RollbacksAvailable []RollbackAvailable `yaml:"rollbacks_available,omitempty"` + RollbacksAvailable map[string]TTLMarker `yaml:"rollbacks_available,omitempty"` } func newMarkerSerializer(m *UpdateMarker) *updateMarkerSerializer { @@ -143,40 +141,23 @@ type updateActiveCommitFunc func(log *logger.Logger, topDirPath, hash string, wr // markUpgrade marks update happened so we can handle grace period func markUpgradeProvider(updateActiveCommit updateActiveCommitFunc, writeFile writeFileFunc) markUpgradeFunc { - return func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks []v1.AgentInstallDesc) error { + return func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks map[string]TTLMarker) error { if len(previousAgent.hash) > hashLen { previousAgent.hash = previousAgent.hash[:hashLen] } marker := &UpdateMarker{ - Version: agent.version, - Hash: agent.hash, - VersionedHome: agent.versionedHome, - UpdatedOn: updatedOn, - PrevVersion: previousAgent.version, - PrevHash: previousAgent.hash, - PrevVersionedHome: previousAgent.versionedHome, - Action: action, - Details: upgradeDetails, - } - - if agent.parsedVersion != nil && !agent.parsedVersion.Less(*Version_9_2_0_SNAPSHOT) { - // if we have a not empty rollback window, write the prev version in the rollbacks_available field - // we also need to check the destination version because the manual rollback and delayed cleanup will be - // handled by that version of agent, so it needs to be recent enough - for _, rollback := range availableRollbacks { - rollbackAvailable := RollbackAvailable{ - Version: rollback.Version, - Home: rollback.VersionedHome, - } - - if rollback.TTL != nil { - rollbackAvailable.ValidUntil = *rollback.TTL - } - - marker.RollbacksAvailable = append(marker.RollbacksAvailable, rollbackAvailable) - } + Version: agent.version, + Hash: agent.hash, + VersionedHome: agent.versionedHome, + UpdatedOn: updatedOn, + PrevVersion: previousAgent.version, + PrevHash: previousAgent.hash, + PrevVersionedHome: previousAgent.versionedHome, + Action: action, + Details: upgradeDetails, + RollbacksAvailable: availableRollbacks, } markerBytes, err := yaml.Marshal(newMarkerSerializer(marker)) diff --git a/internal/pkg/agent/application/upgrade/step_mark_test.go b/internal/pkg/agent/application/upgrade/step_mark_test.go index 440f79821e1..9daed169e58 100644 --- a/internal/pkg/agent/application/upgrade/step_mark_test.go +++ b/internal/pkg/agent/application/upgrade/step_mark_test.go @@ -16,7 +16,6 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade/details" "github.com/elastic/elastic-agent/internal/pkg/fleetapi" - v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/core/logger" "github.com/elastic/elastic-agent/pkg/core/logger/loggertest" agtversion "github.com/elastic/elastic-agent/pkg/version" @@ -95,7 +94,7 @@ func TestMarkUpgrade(t *testing.T) { previousAgent agentInstall action *fleetapi.ActionUpgrade details *details.Details - availableRollbacks []v1.AgentInstallDesc + availableRollbacks map[string]TTLMarker } type workingDirHook func(t *testing.T, dataDir string) @@ -139,7 +138,7 @@ func TestMarkUpgrade(t *testing.T) { wantErr: assert.Error, }, { - name: "no rollback window specified - no available rollbacks", + name: "no rollbacks specified in input - no available rollbacks in marker", args: args{ updatedOn: updatedOnNow, currentAgent: agentInstall{ @@ -184,62 +183,7 @@ func TestMarkUpgrade(t *testing.T) { }, }, { - name: "rollback window specified but new version is too low - no rollbacks", - args: args{ - updatedOn: updatedOnNow, - currentAgent: agentInstall{ - parsedVersion: parsed456SNAPSHOT, - version: "4.5.6-SNAPSHOT", - hash: "curagt", - versionedHome: filepath.Join("data", "elastic-agent-4.5.6-SNAPSHOT-curagt"), - }, - previousAgent: agentInstall{ - parsedVersion: parsed123SNAPSHOT, - version: "1.2.3-SNAPSHOT", - hash: "prvagt", - versionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), - }, - action: nil, - details: details.NewDetails("4.5.6-SNAPSHOT", details.StateReplacing, ""), - availableRollbacks: []v1.AgentInstallDesc{ - { - OptionalTTLItem: v1.OptionalTTLItem{ - TTL: &twentyFourHoursFromNow, - }, - Version: "1.2.3-SNAPSHOT", - Hash: "prvagt", - VersionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), - Flavor: "basic", - Active: false, - }, - }, - }, - wantErr: assert.NoError, - assertAfterMark: func(t *testing.T, dataDir string) { - actualMarker, err := LoadMarker(dataDir) - require.NoError(t, err, "error reading actualMarker content after writing") - - expectedMarker := &UpdateMarker{ - Version: "4.5.6-SNAPSHOT", - Hash: "curagt", - VersionedHome: filepath.Join("data", "elastic-agent-4.5.6-SNAPSHOT-curagt"), - UpdatedOn: updatedOnNow, - PrevVersion: "1.2.3-SNAPSHOT", - PrevHash: "prvagt", - PrevVersionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), - Acked: false, - Action: nil, - Details: &details.Details{ - TargetVersion: "4.5.6-SNAPSHOT", - State: "UPG_REPLACING", - ActionID: "", - }, - } - assert.Equal(t, expectedMarker, actualMarker) - }, - }, - { - name: "rollback window specified and new version is at least 9.2.0-SNAPSHOT - available rollbacks must be present", + name: "available rollbacks passed in - available rollbacks must be present in upgrade marker", args: args{ updatedOn: updatedOnNow, currentAgent: agentInstall{ @@ -256,16 +200,10 @@ func TestMarkUpgrade(t *testing.T) { }, action: nil, details: details.NewDetails("9.2.0-SNAPSHOT", details.StateReplacing, ""), - availableRollbacks: []v1.AgentInstallDesc{ - { - OptionalTTLItem: v1.OptionalTTLItem{ - TTL: &twentyFourHoursFromNow, - }, - Version: "1.2.3-SNAPSHOT", - Hash: "prvagt", - VersionedHome: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), - Flavor: "basic", - Active: false, + availableRollbacks: map[string]TTLMarker{ + filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"): { + Version: "1.2.3-SNAPSHOT", + ValidUntil: twentyFourHoursFromNow, }, }, }, @@ -290,10 +228,9 @@ func TestMarkUpgrade(t *testing.T) { ActionID: "", Metadata: details.Metadata{}, }, - RollbacksAvailable: []RollbackAvailable{ - { + RollbacksAvailable: map[string]TTLMarker{ + filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"): { Version: "1.2.3-SNAPSHOT", - Home: filepath.Join("data", "elastic-agent-1.2.3-SNAPSHOT-prvagt"), ValidUntil: twentyFourHoursFromNow, }, }, diff --git a/internal/pkg/agent/application/upgrade/ttl_marker_source.go b/internal/pkg/agent/application/upgrade/ttl_marker_source.go new file mode 100644 index 00000000000..d095a128392 --- /dev/null +++ b/internal/pkg/agent/application/upgrade/ttl_marker_source.go @@ -0,0 +1,118 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package upgrade + +import ( + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +const ttlMarkerName = ".ttl" + +var defaultMarkerGlobPattern = filepath.Join("data", "elastic-agent-*", ttlMarkerName) + +type TTLMarkerRegistry struct { + baseDir string + markerFileGlobPattern string +} + +func (T TTLMarkerRegistry) AddOrReplace(m map[string]TTLMarker) error { + for versionedHome, marker := range m { + dstFilePath := filepath.Join(T.baseDir, versionedHome, ttlMarkerName) + err := writeTTLMarker(dstFilePath, marker) + if err != nil { + return fmt.Errorf("writing marker %q: %w", dstFilePath, err) + } + } + + return nil +} + +func (T TTLMarkerRegistry) Set(m map[string]TTLMarker) error { + // identify the marker files to be deleted first + existingMarkers, err := T.Get() + if err != nil { + return fmt.Errorf("accessing existing markers: %w", err) + } + + for versionedHome, _ := range existingMarkers { + _, ok := m[versionedHome] + if !ok { + // the existing marker should not be in the final state + err = os.Remove(filepath.Join(T.baseDir, versionedHome, ttlMarkerName)) + if err != nil { + return fmt.Errorf("removing ttl marker for %q: %w", versionedHome, err) + } + } + } + + // create all the remaining markers + return T.AddOrReplace(m) +} + +func (T TTLMarkerRegistry) Get() (map[string]TTLMarker, error) { + matches, err := filepath.Glob(filepath.Join(T.baseDir, T.markerFileGlobPattern)) + if err != nil { + return nil, fmt.Errorf("failed to glob files using %q: %w", T.markerFileGlobPattern, err) + } + ttlMarkers := make(map[string]TTLMarker, len(matches)) + for _, match := range matches { + relPath, err := filepath.Rel(T.baseDir, filepath.Dir(match)) + if err != nil { + return nil, fmt.Errorf("failed to determine path for %q relative to %q : %w", match, T.baseDir, err) + } + marker, err := readTTLMarker(match) + if err != nil { + return nil, fmt.Errorf("failed to read marker from file %q: %w", match, err) + } + ttlMarkers[relPath] = marker + } + + return ttlMarkers, nil +} + +func NewTTLMarkerRegistry(baseDir string) *TTLMarkerRegistry { + return &TTLMarkerRegistry{ + baseDir: baseDir, + markerFileGlobPattern: defaultMarkerGlobPattern, + } +} + +func readTTLMarker(filePath string) (TTLMarker, error) { + file, err := os.Open(filePath) + if err != nil { + return TTLMarker{}, fmt.Errorf("failed to open %q: %w", filePath, err) + } + defer func(file *os.File) { + _ = file.Close() + }(file) + ttlMarker := TTLMarker{} + err = yaml.NewDecoder(file).Decode(&ttlMarker) + if err != nil { + return TTLMarker{}, fmt.Errorf("failed to decode %q: %w", filePath, err) + } + + return ttlMarker, nil +} + +func writeTTLMarker(filePath string, marker TTLMarker) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to open %q: %w", filePath, err) + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + err = yaml.NewEncoder(file).Encode(marker) + if err != nil { + return fmt.Errorf("failed to encode %q: %w", filePath, err) + } + + return nil +} diff --git a/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go b/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go new file mode 100644 index 00000000000..53e94236315 --- /dev/null +++ b/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go @@ -0,0 +1,335 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package upgrade + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "text/template" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTTLMarkerRegistry_AddOrReplace(t *testing.T) { + const TTLMarkerYAMLTemplate = ` + version: {{ .Version }} + valid_until: {{ .ValidUntil }}` + + expectedMarkerContentTemplate, err := template.New("expected marker").Parse(TTLMarkerYAMLTemplate) + require.NoError(t, err) + + now := time.Now() + nowString := now.Format(time.RFC3339) + // re-parse now to account for loss of fidelity due to marshal/unmarshal + now, _ = time.Parse(time.RFC3339, nowString) + + yesterday := now.Add(-24 * time.Hour) + yesterdayString := yesterday.Format(time.RFC3339) + yesterday, _ = time.Parse(time.RFC3339, yesterdayString) + + tomorrow := now.Add(24 * time.Hour) + tomorrowString := tomorrow.Format(time.RFC3339) + tomorrow, _ = time.Parse(time.RFC3339, tomorrowString) + + versions := []string{"1.2.3", "4.5.6"} + versionedHomes := []string{"elastic-agent-1.2.3-past", "elastic-agent-4.5.6-present"} + + type args struct { + m map[string]TTLMarker + } + tests := []struct { + name string + setup func(t *testing.T, tmpDir string) + args args + wantErr assert.ErrorAssertionFunc + postAssertions func(t *testing.T, tmpDir string) + }{ + { + name: "ttl are present in one install - all get created/replaced", + setup: func(t *testing.T, tmpDir string) { + for i, versionedHome := range versionedHomes { + err := os.MkdirAll(filepath.Join(tmpDir, "data", versionedHome), 0755) + require.NoError(t, err, "error setting up fake agent install directory") + + if i < 1 { + // add only 1 ttl marker as part of setup + buf := bytes.Buffer{} + err = expectedMarkerContentTemplate.Execute(&buf, map[string]string{"Version": versions[i], "ValidUntil": yesterdayString}) + require.NoError(t, err, "error executing ttl marker template") + err = os.WriteFile(filepath.Join(tmpDir, "data", versionedHome, ttlMarkerName), buf.Bytes(), 0644) + require.NoError(t, err, "error setting up fake agent ttl marker") + } + } + }, + args: args{ + map[string]TTLMarker{ + filepath.Join("data", versionedHomes[0]): { + Version: versions[0], + ValidUntil: tomorrow, + }, + filepath.Join("data", versionedHomes[1]): { + Version: versions[1], + ValidUntil: tomorrow, + }, + }, + }, + wantErr: assert.NoError, + postAssertions: func(t *testing.T, tmpDir string) { + for i, versionedHome := range versionedHomes { + expectedTTLMarkerFilePath := filepath.Join(tmpDir, "data", versionedHome, ttlMarkerName) + if assert.FileExists(t, expectedTTLMarkerFilePath, "TTL marker should have been created/replaced") { + b := new(strings.Builder) + err = expectedMarkerContentTemplate.Execute(b, map[string]string{"Version": versions[i], "ValidUntil": tomorrowString}) + require.NoError(t, err) + actualMarkerContent, err := os.ReadFile(expectedTTLMarkerFilePath) + require.NoError(t, err) + assert.YAMLEq(t, b.String(), string(actualMarkerContent)) + } + } + + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + if tt.setup != nil { + tt.setup(t, tmpDir) + } + T := NewTTLMarkerRegistry(tmpDir) + err := T.AddOrReplace(tt.args.m) + if !tt.wantErr(t, err, fmt.Sprintf("AddOrReplace(%v)", tt.args.m)) { + return + } + if tt.postAssertions != nil { + tt.postAssertions(t, tmpDir) + } + }) + } +} + +func TestTTLMarkerRegistry_Get(t *testing.T) { + const TTLMarkerYAMLTemplate = ` + version: {{ .Version }} + valid_until: {{ .ValidUntil }}` + + parsedTemplate, err := template.New("ttlMarker").Parse(TTLMarkerYAMLTemplate) + require.NoError(t, err, "error parsing ttl marker template") + + now := time.Now() + nowString := now.Format(time.RFC3339) + // re-parse now to account for loss of fidelity due to marshal/unmarshal + now, _ = time.Parse(time.RFC3339, nowString) + + yesterday := now.Add(-24 * time.Hour) + yesterdayString := yesterday.Format(time.RFC3339) + + tomorrow := now.Add(24 * time.Hour) + tomorrowString := tomorrow.Format(time.RFC3339) + + versions := []string{"1.2.3", "4.5.6", "7.8.9-SNAPSHOT"} + versionedHomes := []string{"elastic-agent-1.2.3-past", "elastic-agent-4.5.6-present", "elastic-agent-7.8.9-SNAPSHOT-future"} + ttls := []string{yesterdayString, nowString, tomorrowString} + + tests := []struct { + name string + setup func(t *testing.T, tmpDir string) + want map[string]TTLMarker + wantErr assert.ErrorAssertionFunc + }{ + { + name: "Empty directory - empty map", + setup: func(t *testing.T, tmpDir string) { + // nothing to do here + }, + want: map[string]TTLMarker{}, + wantErr: assert.NoError, + }, + { + name: "multiple directories, no marker - empty map", + setup: func(t *testing.T, tmpDir string) { + for _, versionedHome := range versionedHomes { + err := os.MkdirAll(filepath.Join(tmpDir, "data", versionedHome), 0755) + require.NoError(t, err, "error setting up fake agent install directory") + } + }, + want: map[string]TTLMarker{}, + wantErr: assert.NoError, + }, + { + name: "multiple directories, ttl on past and present marker - return value", + setup: func(t *testing.T, tmpDir string) { + + for i, versionedHome := range versionedHomes { + err := os.MkdirAll(filepath.Join(tmpDir, "data", versionedHome), 0755) + require.NoError(t, err, "error setting up fake agent install directory") + + if i < 2 { + buf := bytes.Buffer{} + err = parsedTemplate.Execute(&buf, map[string]string{"Version": versions[i], "ValidUntil": ttls[i]}) + require.NoError(t, err, "error executing ttl marker template") + err = os.WriteFile(filepath.Join(tmpDir, "data", versionedHome, ttlMarkerName), buf.Bytes(), 0644) + require.NoError(t, err, "error setting up fake agent ttl marker") + } + } + }, + want: map[string]TTLMarker{ + filepath.Join("data", "elastic-agent-1.2.3-past"): { + Version: "1.2.3", + ValidUntil: yesterday, + }, + filepath.Join("data", "elastic-agent-4.5.6-present"): { + Version: "4.5.6", + ValidUntil: now, + }, + }, + wantErr: assert.NoError, + }, + { + name: "empty marker - error", + setup: func(t *testing.T, tmpDir string) { + for _, versionedHome := range versionedHomes { + err := os.MkdirAll(filepath.Join(tmpDir, "data", versionedHome), 0755) + require.NoError(t, err, "error setting up fake agent install directory") + err = os.WriteFile(filepath.Join(tmpDir, "data", versionedHome, ttlMarkerName), nil, 0644) + require.NoError(t, err, "error setting up fake agent ttl marker") + } + }, + want: nil, + wantErr: assert.Error, + }, + { + name: "ttl content is not yaml - error", + setup: func(t *testing.T, tmpDir string) { + err := os.MkdirAll(filepath.Join(tmpDir, "data", versionedHomes[0]), 0755) + require.NoError(t, err, "error setting up fake agent install directory") + err = os.WriteFile(filepath.Join(tmpDir, "data", versionedHomes[0], ttlMarkerName), []byte("this is not yaml"), 0644) + require.NoError(t, err, "error setting up fake agent ttl marker") + }, + want: nil, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + tt.setup(t, tmpDir) + T := NewTTLMarkerRegistry(tmpDir) + got, err := T.Get() + if !tt.wantErr(t, err, fmt.Sprintf("Get()")) { + return + } + assert.Equalf(t, tt.want, got, "Get()") + }) + } +} + +func TestTTLMarkerRegistry_Set(t *testing.T) { + const TTLMarkerYAMLTemplate = ` + version: {{ .Version }} + valid_until: {{ .ValidUntil }}` + + expectedMarkerContentTemplate, err := template.New("expected marker").Parse(TTLMarkerYAMLTemplate) + require.NoError(t, err) + + now := time.Now() + nowString := now.Format(time.RFC3339) + // re-parse now to account for loss of fidelity due to marshal/unmarshal + now, _ = time.Parse(time.RFC3339, nowString) + + tomorrow := now.Add(24 * time.Hour) + tomorrowString := tomorrow.Format(time.RFC3339) + tomorrow, _ = time.Parse(time.RFC3339, tomorrowString) + + versions := []string{"1.2.3", "4.5.6"} + versionedHomes := []string{"elastic-agent-1.2.3-past", "elastic-agent-4.5.6-present"} + ttls := []string{tomorrowString, ""} + + type args struct { + m map[string]TTLMarker + } + tests := []struct { + name string + setup func(t *testing.T, tmpDir string) + args args + wantErr assert.ErrorAssertionFunc + postAssertions func(t *testing.T, tmpDir string) + }{ + { + name: "no ttl are present - all get created", + setup: func(t *testing.T, tmpDir string) { + for _, versionedHome := range versionedHomes { + err := os.MkdirAll(filepath.Join(tmpDir, "data", versionedHome), 0755) + require.NoError(t, err, "error setting up fake agent install directory") + } + }, + args: args{ + map[string]TTLMarker{ + filepath.Join("data", versionedHomes[0]): { + Version: versions[0], + ValidUntil: tomorrow, + }, + }, + }, + wantErr: assert.NoError, + postAssertions: func(t *testing.T, tmpDir string) { + notExistingTTLMarkerFilePath := filepath.Join(tmpDir, "data", versionedHomes[1], ttlMarkerName) + assert.NoFileExists(t, notExistingTTLMarkerFilePath) + expectedTTLMarkerFilePath := filepath.Join(tmpDir, "data", versionedHomes[0], ttlMarkerName) + if assert.FileExists(t, expectedTTLMarkerFilePath, "new TTL marker should have been created") { + + b := new(strings.Builder) + err = expectedMarkerContentTemplate.Execute(b, map[string]string{"Version": versions[0], "ValidUntil": ttls[0]}) + require.NoError(t, err) + actualMarkerContent, err := os.ReadFile(expectedTTLMarkerFilePath) + require.NoError(t, err) + assert.YAMLEq(t, b.String(), string(actualMarkerContent)) + } + }, + }, + { + name: "ttls are present, none are specified - all deleted", + setup: func(t *testing.T, tmpDir string) { + for i, versionedHome := range versionedHomes { + err = os.MkdirAll(filepath.Join(tmpDir, "data", versionedHome), 0755) + require.NoError(t, err, "error setting up fake agent install directory") + b := new(strings.Builder) + err = expectedMarkerContentTemplate.Execute(b, map[string]string{"Version": versions[i], "ValidUntil": ttls[i]}) + require.NoError(t, err, "error setting up ttl marker") + err = os.WriteFile(filepath.Join(tmpDir, "data", versionedHomes[i], ttlMarkerName), []byte(b.String()), 0644) + } + }, + args: args{ + nil, + }, + wantErr: assert.NoError, + postAssertions: func(t *testing.T, tmpDir string) { + for _, versionedHome := range versionedHomes { + notExistingTTLMarkerFilePath := filepath.Join(tmpDir, "data", versionedHome, ttlMarkerName) + assert.NoFileExists(t, notExistingTTLMarkerFilePath) + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + if tt.setup != nil { + tt.setup(t, tmpDir) + } + T := NewTTLMarkerRegistry(tmpDir) + tt.wantErr(t, T.Set(tt.args.m), fmt.Sprintf("Set(%v)", tt.args.m)) + if tt.postAssertions != nil { + tt.postAssertions(t, tmpDir) + } + }) + } +} diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index d4fe4b2d89b..c7fe76f23e0 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -35,7 +35,6 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/fleetapi/acker" fleetclient "github.com/elastic/elastic-agent/internal/pkg/fleetapi/client" "github.com/elastic/elastic-agent/internal/pkg/release" - v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/control/v2/client" "github.com/elastic/elastic-agent/pkg/control/v2/cproto" "github.com/elastic/elastic-agent/pkg/core/logger" @@ -68,8 +67,8 @@ var ( ErrEmptyRollbackVersion = errors.New("rollback version is empty") ErrNoRollbacksAvailable = errors.New("no rollbacks available") ErrAgentInstallNotFound = errors.New("agent install descriptor not found") - // Version_9_2_0_SNAPSHOT is the minimum version for manual rollback and rollback reason - Version_9_2_0_SNAPSHOT = agtversion.NewParsedSemVer(9, 2, 0, "SNAPSHOT", "") + // Version_9_3_0_SNAPSHOT is the minimum version for manual rollback and rollback reason + Version_9_3_0_SNAPSHOT = agtversion.NewParsedSemVer(9, 3, 0, "SNAPSHOT", "") ) func init() { @@ -91,9 +90,9 @@ type unpackHandler interface { type copyActionStoreFunc func(log *logger.Logger, newHome string) error type copyRunDirectoryFunc func(log *logger.Logger, oldRunPath, newRunPath string) error type fileDirCopyFunc func(from, to string, opts ...filecopy.Options) error -type markUpgradeFunc func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks []v1.AgentInstallDesc) error +type markUpgradeFunc func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks map[string]TTLMarker) error type changeSymlinkFunc func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error -type rollbackInstallFunc func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error +type rollbackInstallFunc func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error // Types used to abstract stdlib functions type mkdirAllFunc func(name string, perm fs.FileMode) error @@ -117,23 +116,22 @@ type WatcherHelper interface { TakeOverWatcher(ctx context.Context, log *logger.Logger, topDir string) (*filelock.AppLocker, error) } -type installDescriptorSource interface { - AddInstallDesc(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) - ModifyInstallDesc(modifierFunc func(desc *v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error) - RemoveAgentInstallDesc(versionedHome string) (*v1.InstallDescriptor, error) +type availableRollbacksSource interface { + Set(map[string]TTLMarker) error + Get() (map[string]TTLMarker, error) } // Upgrader performs an upgrade type Upgrader struct { - log *logger.Logger - settings *artifact.Config - upgradeSettings *configuration.UpgradeConfig - agentInfo info.Agent - upgradeable bool - fleetServerURI string - markerWatcher MarkerWatcher - watcherHelper WatcherHelper - installDescriptorSource installDescriptorSource + log *logger.Logger + settings *artifact.Config + upgradeSettings *configuration.UpgradeConfig + agentInfo info.Agent + upgradeable bool + fleetServerURI string + markerWatcher MarkerWatcher + watcherHelper WatcherHelper + availableRollbacksSource availableRollbacksSource // The following are abstractions for testability artifactDownloader artifactDownloadHandler @@ -155,25 +153,25 @@ func IsUpgradeable() bool { } // NewUpgrader creates an upgrader which is capable of performing upgrade operation -func NewUpgrader(log *logger.Logger, settings *artifact.Config, upgradeConfig *configuration.UpgradeConfig, agentInfo info.Agent, watcherHelper WatcherHelper, ids installDescriptorSource) (*Upgrader, error) { +func NewUpgrader(log *logger.Logger, settings *artifact.Config, upgradeConfig *configuration.UpgradeConfig, agentInfo info.Agent, watcherHelper WatcherHelper, ars availableRollbacksSource) (*Upgrader, error) { return &Upgrader{ - log: log, - settings: settings, - upgradeSettings: upgradeConfig, - agentInfo: agentInfo, - upgradeable: IsUpgradeable(), - markerWatcher: newMarkerFileWatcher(markerFilePath(paths.Data()), log), - watcherHelper: watcherHelper, - installDescriptorSource: ids, - artifactDownloader: newArtifactDownloader(settings, log), - unpacker: newUnpacker(log), - isDiskSpaceErrorFunc: upgradeErrors.IsDiskSpaceError, - extractAgentVersion: extractAgentVersion, - copyActionStore: copyActionStoreProvider(os.ReadFile, os.WriteFile), - copyRunDirectory: copyRunDirectoryProvider(os.MkdirAll, filecopy.Copy), - markUpgrade: markUpgradeProvider(UpdateActiveCommit, os.WriteFile), - changeSymlink: changeSymlink, - rollbackInstall: rollbackInstall, + log: log, + settings: settings, + upgradeSettings: upgradeConfig, + agentInfo: agentInfo, + upgradeable: IsUpgradeable(), + markerWatcher: newMarkerFileWatcher(markerFilePath(paths.Data()), log), + watcherHelper: watcherHelper, + availableRollbacksSource: ars, + artifactDownloader: newArtifactDownloader(settings, log), + unpacker: newUnpacker(log), + isDiskSpaceErrorFunc: upgradeErrors.IsDiskSpaceError, + extractAgentVersion: extractAgentVersion, + copyActionStore: copyActionStoreProvider(os.ReadFile, os.WriteFile), + copyRunDirectory: copyRunDirectoryProvider(os.MkdirAll, filecopy.Copy), + markUpgrade: markUpgradeProvider(UpdateActiveCommit, os.WriteFile), + changeSymlink: changeSymlink, + rollbackInstall: rollbackInstall, }, nil } @@ -414,8 +412,6 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return nil, fmt.Errorf("calculating home path relative to top, home: %q top: %q : %w", paths.Home(), paths.Top(), err) } - u.addNewInstallToRegistry(version, unpackRes, detectedFlavor) - newHash := unpackRes.Hash if newHash == "" { return nil, errors.New("unknown hash") @@ -444,7 +440,7 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s if err := u.changeSymlink(u.log, paths.Top(), symlinkPath, newPath); err != nil { u.log.Errorw("Rolling back: changing symlink failed", "error.message", err) - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.installDescriptorSource) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) return nil, goerrors.Join(err, rollbackErr) } @@ -453,10 +449,6 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s rollbackWindow = u.upgradeSettings.Rollback.Window } - // timestamp marking the moment the links have been rotated. It will be used for TTL calculations of pre-existing elastic-agent installs - rotationTimestamp := time.Now() - modifiedInstallDescriptor := u.activateInstallInRegistry(unpackRes.VersionedHome, currentVersionedHome, rollbackWindow, rotationTimestamp) - // We rotated the symlink successfully: prepare the current and previous agent installation details for the update marker // In update marker the `current` agent install is the one where the symlink is pointing (the new one we didn't start yet) // while the `previous` install is the currently executing elastic-agent that is no longer reachable via the symlink. @@ -476,7 +468,7 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s versionedHome: currentVersionedHome, } - availableRollbacks := getAvailableRollbacks(rollbackWindow, rotationTimestamp, unpackRes.VersionedHome, modifiedInstallDescriptor) + availableRollbacks := getAvailableRollbacks(rollbackWindow, time.Now(), release.VersionWithSnapshot(), previousParsedVersion, currentVersionedHome) if err := u.markUpgrade(u.log, paths.Data(), // data dir to place the marker in @@ -485,23 +477,27 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s previous, // old agent version data action, det, availableRollbacks); err != nil { u.log.Errorw("Rolling back: marking upgrade failed", "error.message", err) - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.installDescriptorSource) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) return nil, goerrors.Join(err, rollbackErr) } + if err = u.availableRollbacksSource.Set(availableRollbacks); err != nil { + u.log.Warnw("Setting rollback targets failed. Manual rollback may not be available beyond grace period", "error.message", err) + } + watcherExecutable := u.watcherHelper.SelectWatcherExecutable(paths.Top(), previous, current) var watcherCmd *exec.Cmd if watcherCmd, err = u.watcherHelper.InvokeWatcher(u.log, watcherExecutable); err != nil { u.log.Errorw("Rolling back: starting watcher failed", "error.message", err) - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.installDescriptorSource) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) return nil, goerrors.Join(err, rollbackErr) } watcherWaitErr := u.watcherHelper.WaitForWatcher(ctx, u.log, markerFilePath(paths.Data()), watcherMaxWaitTime) if watcherWaitErr != nil { killWatcherErr := watcherCmd.Process.Kill() - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.installDescriptorSource) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) return nil, goerrors.Join(watcherWaitErr, killWatcherErr, rollbackErr) } @@ -517,63 +513,26 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return cb, nil } -func (u *Upgrader) addNewInstallToRegistry(version string, unpackRes UnpackResult, detectedFlavor string) { - _, err := u.installDescriptorSource.AddInstallDesc( - v1.AgentInstallDesc{Version: version, VersionedHome: unpackRes.VersionedHome, Hash: unpackRes.Hash, Flavor: detectedFlavor, Active: false}, - ) - if err != nil { - u.log.Warnf("error encountered when adding install description of new agent version: %s", err.Error()) - } -} - -func (u *Upgrader) activateInstallInRegistry(newVersionedHome, currentVersionedHome string, rollbackWindow time.Duration, rotationTimestamp time.Time) *v1.InstallDescriptor { - modifiedInstallDescriptor, err := u.installDescriptorSource.ModifyInstallDesc( - func(desc *v1.AgentInstallDesc) error { - if desc.VersionedHome == newVersionedHome { - desc.Active = true - return nil - } else { - desc.Active = false - } - - // set the TTL only for the current install - if desc.VersionedHome == currentVersionedHome { - desc.TTL = getCurrentInstallTTL(rollbackWindow, rotationTimestamp) - } - - return nil - }, - ) - if err != nil { - u.log.Warnf("error encountered when setting new install description as active: %s", err.Error()) - } - return modifiedInstallDescriptor -} - -func getAvailableRollbacks(rollbackWindow time.Duration, now time.Time, newVersionedHome string, descriptor *v1.InstallDescriptor) []v1.AgentInstallDesc { +func getAvailableRollbacks(rollbackWindow time.Duration, now time.Time, currentVersion string, parsedCurrentVersion *agtversion.ParsedSemVer, currentVersionedHome string) map[string]TTLMarker { if rollbackWindow == 0 { // if there's no rollback window it means that no rollback should survive the watcher cleanup at the end of the grace period. return nil } - res := make([]v1.AgentInstallDesc, 0, len(descriptor.AgentInstalls)) - for _, installDesc := range descriptor.AgentInstalls { - if installDesc.VersionedHome != newVersionedHome && (installDesc.TTL == nil || now.Before(*installDesc.TTL)) { - // this is a valid possible rollback target, so we have to keep it available beyond the end of the grace period - res = append(res, installDesc) - } + if parsedCurrentVersion == nil || parsedCurrentVersion.Less(*Version_9_3_0_SNAPSHOT) { + // the version we are upgrading to does not support manual rollbacks + return nil } - return res -} -func getCurrentInstallTTL(rollbackWindow time.Duration, now time.Time) *time.Time { - if rollbackWindow == 0 { - // no rollback window, no TTL - return nil + // when multiple rollbacks will be supported, read the existing descriptor + // at this stage we can get by with a single rollback + res := make(map[string]TTLMarker, 1) + res[currentVersionedHome] = TTLMarker{ + Version: currentVersion, + ValidUntil: now.Add(rollbackWindow), } - currentInstallTTLVar := now.Add(rollbackWindow) - return ¤tInstallTTLVar + return res } func (u *Upgrader) rollbackToPreviousVersion(ctx context.Context, topDir string, now time.Time, version string, action *fleetapi.ActionUpgrade) (reexec.ShutdownCallbackFn, error) { @@ -619,7 +578,7 @@ func (u *Upgrader) rollbackToPreviousVersion(ctx context.Context, topDir string, if len(updateMarker.RollbacksAvailable) == 0 { return ErrNoRollbacksAvailable } - var selectedRollback *RollbackAvailable + var selectedRollback *TTLMarker for _, rollback := range updateMarker.RollbacksAvailable { if rollback.Version == version && now.Before(rollback.ValidUntil) { selectedRollback = &rollback @@ -770,7 +729,7 @@ func isSameVersion(log *logger.Logger, current agentVersion, newVersion agentVer return current == newVersion } -func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error { +func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error { oldAgentPath := paths.BinaryPath(filepath.Join(topDirPath, oldVersionedHome), agentName) err := changeSymlink(log, topDirPath, filepath.Join(topDirPath, agentName), oldAgentPath) if err != nil && !errors.Is(err, fs.ErrNotExist) { @@ -782,10 +741,7 @@ func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versio if err != nil && !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("rolling back install: removing new agent install at %q failed: %w", newAgentInstallPath, err) } - _, err = ids.RemoveAgentInstallDesc(versionedHome) - if err != nil && !errors.Is(err, ErrAgentInstallNotFound) { - return fmt.Errorf("rolling back install: removing agent install descriptor at %q failed: %w", versionedHome, err) - } + return nil } diff --git a/internal/pkg/agent/application/upgrade/upgrade_test.go b/internal/pkg/agent/application/upgrade/upgrade_test.go index 55ede8e217f..f7eb6ac3a45 100644 --- a/internal/pkg/agent/application/upgrade/upgrade_test.go +++ b/internal/pkg/agent/application/upgrade/upgrade_test.go @@ -1044,43 +1044,42 @@ func TestIsSameReleaseVersion(t *testing.T) { func TestManualRollback(t *testing.T) { const updatemarkerwatching456NoRollbackAvailable = ` - version: 4.5.6 - hash: newver - versioned_home: data/elastic-agent-4.5.6-newver - updated_on: 2025-07-11T10:11:12.131415Z - prev_version: 1.2.3 - prev_hash: oldver - prev_versioned_home: data/elastic-agent-1.2.3-oldver - acked: false - action: null - details: - target_version: 4.5.6 - state: UPG_WATCHING - metadata: - retry_until: null - desired_outcome: UPGRADE - ` + version: 4.5.6 + hash: newver + versioned_home: data/elastic-agent-4.5.6-newver + updated_on: 2025-07-11T10:11:12.131415Z + prev_version: 1.2.3 + prev_hash: oldver + prev_versioned_home: data/elastic-agent-1.2.3-oldver + acked: false + action: null + details: + target_version: 4.5.6 + state: UPG_WATCHING + metadata: + retry_until: null + ` const updatemarkerwatching456 = ` - version: 4.5.6 - hash: newver - versioned_home: data/elastic-agent-4.5.6-newver - updated_on: 2025-07-11T10:11:12.131415Z - prev_version: 1.2.3 - prev_hash: oldver - prev_versioned_home: data/elastic-agent-1.2.3-oldver - acked: false - action: null - details: - target_version: 4.5.6 - state: UPG_WATCHING - metadata: - retry_until: null - desired_outcome: UPGRADE - rollbacks_available: - - version: 1.2.3 - home: data/elastic-agent-1.2.3-oldver - valid_until: 2025-07-18T10:11:12.131415Z - ` + version: 4.5.6 + hash: newver + versioned_home: data/elastic-agent-4.5.6-newver + updated_on: 2025-07-11T10:11:12.131415Z + prev_version: 1.2.3 + prev_hash: oldver + prev_versioned_home: data/elastic-agent-1.2.3-oldver + acked: false + action: null + details: + target_version: 4.5.6 + state: UPG_WATCHING + metadata: + retry_until: null + desired_outcome: UPGRADE + rollbacks_available: + "data/elastic-agent-1.2.3-oldver": + version: 1.2.3 + valid_until: 2025-07-18T10:11:12.131415Z + ` parsed123Version, err := agtversion.ParseVersion("1.2.3") require.NoError(t, err) @@ -1292,7 +1291,7 @@ func TestManualRollback(t *testing.T) { log, _ := loggertest.New(t.Name()) mockAgentInfo := info.NewMockAgent(t) mockWatcherHelper := NewMockWatcherHelper(t) - mockInstallSource := newMockInstallDescriptorSource(t) + mockRollbacksSource := newMockAvailableRollbacksSource(t) topDir := t.TempDir() err := os.MkdirAll(paths.DataFrom(topDir), 0777) require.NoError(t, err, "error creating data directory in topDir %q", topDir) @@ -1301,7 +1300,7 @@ func TestManualRollback(t *testing.T) { tc.setup(t, topDir, mockAgentInfo, mockWatcherHelper) } - upgrader, err := NewUpgrader(log, tc.artifactSettings, tc.upgradeSettings, mockAgentInfo, mockWatcherHelper, mockInstallSource) + upgrader, err := NewUpgrader(log, tc.artifactSettings, tc.upgradeSettings, mockAgentInfo, mockWatcherHelper, mockRollbacksSource) require.NoError(t, err, "error instantiating upgrader") _, err = upgrader.rollbackToPreviousVersion(t.Context(), topDir, tc.now, tc.version, nil) tc.wantErr(t, err, "unexpected error returned by rollbackToPreviousVersion()") @@ -1353,7 +1352,7 @@ func TestUpgradeErrorHandling(t *testing.T) { upgraderMocker upgraderMocker checkArchiveCleanup bool checkVersionedHomeCleanup bool - setupMocks func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) + setupMocks func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) } testCases := map[string]testCase{ @@ -1367,7 +1366,7 @@ func TestUpgradeErrorHandling(t *testing.T) { } }, checkArchiveCleanup: true, - setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") }, }, @@ -1383,7 +1382,7 @@ func TestUpgradeErrorHandling(t *testing.T) { } }, checkArchiveCleanup: true, - setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") }, }, @@ -1410,7 +1409,7 @@ func TestUpgradeErrorHandling(t *testing.T) { } }, checkArchiveCleanup: true, - setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") }, }, @@ -1442,7 +1441,7 @@ func TestUpgradeErrorHandling(t *testing.T) { }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, - setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") }, }, @@ -1476,13 +1475,8 @@ func TestUpgradeErrorHandling(t *testing.T) { }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, - setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") - mockInstallSrc.EXPECT().AddInstallDesc(mock.AnythingOfType("v1.AgentInstallDesc")).RunAndReturn( - func(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) { - return &v1.InstallDescriptor{}, nil - }, - ) }, }, "should return error and cleanup downloaded artifact and extracted archive if copyRunDirectory fails": { @@ -1519,14 +1513,8 @@ func TestUpgradeErrorHandling(t *testing.T) { }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, - setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") - - mockInstallSrc.EXPECT().AddInstallDesc(mock.AnythingOfType("v1.AgentInstallDesc")).RunAndReturn( - func(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) { - return &v1.InstallDescriptor{}, nil - }, - ) }, }, "should return error and cleanup downloaded artifact and extracted archive if changeSymlink fails": { @@ -1559,7 +1547,7 @@ func TestUpgradeErrorHandling(t *testing.T) { upgrader.copyRunDirectory = func(log *logger.Logger, oldRunPath, newRunPath string) error { return nil } - upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error { + upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error { return nil } upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error { @@ -1568,9 +1556,8 @@ func TestUpgradeErrorHandling(t *testing.T) { }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, - setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") - mockInstallSrc.EXPECT().AddInstallDesc(mock.AnythingOfType("v1.AgentInstallDesc")).Return(&v1.InstallDescriptor{}, nil) }, }, "should return error and cleanup downloaded artifact and extracted archive if markUpgrade fails": { @@ -1606,19 +1593,17 @@ func TestUpgradeErrorHandling(t *testing.T) { upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error { return nil } - upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, ids installDescriptorSource) error { + upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error { return nil } - upgrader.markUpgrade = func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks []v1.AgentInstallDesc) error { + upgrader.markUpgrade = func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks map[string]TTLMarker) error { return testError } }, checkArchiveCleanup: true, checkVersionedHomeCleanup: true, - setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") - mockInstallSrc.EXPECT().AddInstallDesc(mock.AnythingOfType("v1.AgentInstallDesc")).Return(&v1.InstallDescriptor{}, nil) - mockInstallSrc.EXPECT().ModifyInstallDesc(mock.Anything).Return(&v1.InstallDescriptor{}, nil) }, }, "should add disk space error to the error chain if downloadArtifact fails with disk space error": { @@ -1629,7 +1614,7 @@ func TestUpgradeErrorHandling(t *testing.T) { returnError: testError, } }, - setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockInstallSrc *mockInstallDescriptorSource, mockWatcherHelper *MockWatcherHelper) { + setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") }, }, @@ -1641,17 +1626,17 @@ func TestUpgradeErrorHandling(t *testing.T) { paths.SetTop(baseDir) mockAgentInfo := info.NewMockAgent(t) - mockInstallSource := newMockInstallDescriptorSource(t) + mockRollbackSource := newMockAvailableRollbacksSource(t) mockWatcherHelper := NewMockWatcherHelper(t) if tc.setupMocks != nil { // setup mocks - tc.setupMocks(t, mockAgentInfo, mockInstallSource, mockWatcherHelper) + tc.setupMocks(t, mockAgentInfo, mockRollbackSource, mockWatcherHelper) } else { t.Log("skipping mocks setup as the testcase does not define a setupMocks()") } - upgrader, err := NewUpgrader(log, &artifact.Config{}, nil, mockAgentInfo, mockWatcherHelper, mockInstallSource) + upgrader, err := NewUpgrader(log, &artifact.Config{}, nil, mockAgentInfo, mockWatcherHelper, mockRollbackSource) require.NoError(t, err) tc.upgraderMocker(upgrader, filepath.Join(baseDir, "mockArchive"), "versionedHome") diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index 496806e9ea3..077d948d318 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -777,7 +777,7 @@ func ensureInstallMarkerPresent() error { if err != nil { return fmt.Errorf("failed to get current file owner: %w", err) } - if err := install.CreateInstallMarker(paths.Top(), ownership, paths.Home(), version.GetAgentPackageVersion(), ""); err != nil { + if err := install.CreateInstallMarker(paths.Top(), ownership); err != nil { return fmt.Errorf("unable to create installation marker file during upgrade: %w", err) } diff --git a/internal/pkg/agent/install/install.go b/internal/pkg/agent/install/install.go index 7a5734f5efc..97ae6315c5f 100644 --- a/internal/pkg/agent/install/install.go +++ b/internal/pkg/agent/install/install.go @@ -25,8 +25,6 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/cli" v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/utils" - "github.com/elastic/elastic-agent/pkg/utils/install" - manifestutils "github.com/elastic/elastic-agent/pkg/utils/manifest" ) const ( @@ -63,20 +61,17 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p } } + err = setupInstallPath(topPath, ownership) + if err != nil { + return utils.FileOwner{}, fmt.Errorf("error setting up install path: %w", err) + } + manifest, err := readPackageManifest(dir) if err != nil { return utils.FileOwner{}, fmt.Errorf("reading package manifest: %w", err) } pathMappings := manifest.Package.PathMappings - pathMapper := manifestutils.NewPathMapper(pathMappings) - - targetVersionedHome := filepath.FromSlash(pathMapper.Map(manifest.Package.VersionedHome)) - - err = setupInstallPath(topPath, ownership, targetVersionedHome, manifestutils.GetFullVersion(manifest), flavor) - if err != nil { - return utils.FileOwner{}, fmt.Errorf("error setting up install path: %w", err) - } pt.Describe("Copying install files") copyConcurrency := calculateCopyConcurrency(streams) @@ -189,7 +184,7 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p } // setup the basic topPath, and the .installed file -func setupInstallPath(topPath string, ownership utils.FileOwner, versionedHome string, version string, flavor string) error { +func setupInstallPath(topPath string, ownership utils.FileOwner) error { // ensure parent directory exists err := os.MkdirAll(filepath.Dir(topPath), 0755) if err != nil { @@ -203,7 +198,7 @@ func setupInstallPath(topPath string, ownership utils.FileOwner, versionedHome s } // create the install marker - if err := CreateInstallMarker(topPath, ownership, versionedHome, version, flavor); err != nil { + if err := CreateInstallMarker(topPath, ownership); err != nil { return fmt.Errorf("failed to create install marker: %w", err) } return nil @@ -521,15 +516,13 @@ func hasAllSSDs(block ghw.BlockInfo) bool { // CreateInstallMarker creates a `.installed` file at the given install path, // and then calls fixInstallMarkerPermissions to set the ownership provided by `ownership` -func CreateInstallMarker(topPath string, ownership utils.FileOwner, home string, version string, flavor string) error { +func CreateInstallMarker(topPath string, ownership utils.FileOwner) error { markerFilePath := filepath.Join(topPath, paths.MarkerFileName) - installDescProvider := install.NewFileDescriptorSource(markerFilePath) - installDesc := v1.AgentInstallDesc{Version: version, VersionedHome: home, Flavor: flavor, Active: true} - _, err := installDescProvider.AddInstallDesc(installDesc) - + handle, err := os.Create(markerFilePath) if err != nil { - return fmt.Errorf("creating install marker: %w", err) + return err } + _ = handle.Close() return fixInstallMarkerPermissions(markerFilePath, ownership) } diff --git a/internal/pkg/agent/install/install_test.go b/internal/pkg/agent/install/install_test.go index 6a2616f99c8..795dcfb0416 100644 --- a/internal/pkg/agent/install/install_test.go +++ b/internal/pkg/agent/install/install_test.go @@ -224,21 +224,8 @@ func TestSetupInstallPath(t *testing.T) { tmpdir := t.TempDir() ownership, err := utils.CurrentFileOwner() require.NoError(t, err) - err = setupInstallPath(tmpdir, ownership, "data/elastic-agent-1.2.3-SNAPSHOT", "1.2.3-SNAPSHOT", "flavor") + err = setupInstallPath(tmpdir, ownership) require.NoError(t, err) markerFilePath := filepath.Join(tmpdir, paths.MarkerFileName) require.FileExists(t, markerFilePath) - - const expectedInstallDescriptor = ` - version: co.elastic.agent/v1 - kind: InstallDescriptor - agentInstalls: - - version: 1.2.3-SNAPSHOT - versionedHome: data/elastic-agent-1.2.3-SNAPSHOT - active: true - flavor: flavor - ` - actualInstallDescriptorBytes, err := os.ReadFile(markerFilePath) - require.NoError(t, err, "error reading actual install descriptor") - assert.YAMLEq(t, expectedInstallDescriptor, string(actualInstallDescriptorBytes), "expected and actual install descriptor do not match") } diff --git a/pkg/api/v1/install.go b/pkg/api/v1/install.go deleted file mode 100644 index e76ee5ba345..00000000000 --- a/pkg/api/v1/install.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License 2.0; -// you may not use this file except in compliance with the Elastic License 2.0. - -package v1 - -import ( - "io" - "time" - - "gopkg.in/yaml.v2" -) - -const ( - InstallDescriptorKind = "InstallDescriptor" -) - -type OptionalTTLItem struct { - TTL *time.Time `yaml:"ttl,omitempty" json:"ttl,omitempty"` -} - -type AgentInstallDesc struct { - OptionalTTLItem `yaml:",inline" json:",inline"` - Version string `yaml:"version,omitempty" json:"version,omitempty"` - Hash string `yaml:"hash,omitempty" json:"hash,omitempty"` - VersionedHome string `yaml:"versionedHome,omitempty" json:"versionedHome,omitempty"` - Flavor string `yaml:"flavor,omitempty" json:"flavor,omitempty"` - Active bool `yaml:"active,omitempty" json:"active,omitempty"` -} - -type InstallDescriptor struct { - apiObject `yaml:",inline"` - AgentInstalls []AgentInstallDesc `yaml:"agentInstalls,omitempty" json:"agentInstalls,omitempty"` -} - -func NewInstallDescriptor() *InstallDescriptor { - return &InstallDescriptor{ - apiObject: apiObject{ - Version: VERSION, - Kind: InstallDescriptorKind, - }, - } -} - -func ParseInstallDescriptor(r io.Reader) (*InstallDescriptor, error) { - id := NewInstallDescriptor() - err := yaml.NewDecoder(r).Decode(id) - return id, err -} - -func WriteInstallDescriptor(w io.Writer, id *InstallDescriptor) error { - return yaml.NewEncoder(w).Encode(id) -} diff --git a/pkg/utils/install/file_source.go b/pkg/utils/install/file_source.go deleted file mode 100644 index f2a8e4e8f11..00000000000 --- a/pkg/utils/install/file_source.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License 2.0; -// you may not use this file except in compliance with the Elastic License 2.0. - -package install - -import ( - "errors" - "fmt" - "io" - "os" - "slices" - - v1 "github.com/elastic/elastic-agent/pkg/api/v1" -) - -type FileDescriptorSource struct { - descriptorFile string -} - -func NewFileDescriptorSource(descriptorFile string) *FileDescriptorSource { - return &FileDescriptorSource{descriptorFile: descriptorFile} -} - -func (dp *FileDescriptorSource) AddInstallDesc(desc v1.AgentInstallDesc) (*v1.InstallDescriptor, error) { - installDescriptor, err := readInstallMarkerFile(dp.descriptorFile) - // not existing or empty files are tolerated, since we would be writing a new descriptor, return any other error - if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, io.EOF) { - return nil, err - } - - if installDescriptor == nil { - installDescriptor = v1.NewInstallDescriptor() - } - - existingInstalls := installDescriptor.AgentInstalls - installDescriptor.AgentInstalls = make([]v1.AgentInstallDesc, len(existingInstalls)+1) - installDescriptor.AgentInstalls[0] = desc - copied := copy(installDescriptor.AgentInstalls[1:], existingInstalls) - if copied != len(existingInstalls) { - return nil, fmt.Errorf("error adding new install %v to existing installs %v", desc, existingInstalls) - } - - err = writeInstallMarkerFile(dp.descriptorFile, installDescriptor) - if err != nil { - return nil, fmt.Errorf("writing updated install marker: %w", err) - } - - return installDescriptor, nil -} - -func (dp *FileDescriptorSource) ModifyInstallDesc(modifierFunc func(desc *v1.AgentInstallDesc) error) (*v1.InstallDescriptor, error) { - installDescriptor, err := readInstallMarkerFile(dp.descriptorFile) - if err != nil { - return nil, err - } - - if installDescriptor == nil { - return nil, fmt.Errorf("no install descriptor found at %q", dp.descriptorFile) - } - - for i := range installDescriptor.AgentInstalls { - err = modifierFunc(&installDescriptor.AgentInstalls[i]) - if err != nil { - return nil, fmt.Errorf("modifying agent install %s: %w", installDescriptor.AgentInstalls[i].VersionedHome, err) - } - } - - err = writeInstallMarkerFile(dp.descriptorFile, installDescriptor) - if err != nil { - return nil, fmt.Errorf("writing updated install marker: %w", err) - } - - return installDescriptor, nil -} - -func (dp *FileDescriptorSource) RemoveAgentInstallDesc(versionedHome string) (*v1.InstallDescriptor, error) { - installDescriptor, err := readInstallMarkerFile(dp.descriptorFile) - if err != nil { - return nil, err - } - - if installDescriptor == nil { - return nil, fmt.Errorf("no install descriptor found at %q", dp.descriptorFile) - } - - installDescriptor.AgentInstalls = slices.DeleteFunc(installDescriptor.AgentInstalls, func(installDesc v1.AgentInstallDesc) bool { - return installDesc.VersionedHome == versionedHome - }) - - err = writeInstallMarkerFile(dp.descriptorFile, installDescriptor) - if err != nil { - return nil, fmt.Errorf("writing updated install marker: %w", err) - } - - return installDescriptor, nil -} - -func writeInstallMarkerFile(markerFilePath string, descriptor *v1.InstallDescriptor) error { - installMarkerFile, err := os.Create(markerFilePath) - if err != nil { - return fmt.Errorf("opening install marker file: %w", err) - } - defer func(installMarkerFile *os.File) { - _ = installMarkerFile.Close() - }(installMarkerFile) - return v1.WriteInstallDescriptor(installMarkerFile, descriptor) -} - -func readInstallMarkerFile(markerFilePath string) (*v1.InstallDescriptor, error) { - installMarkerFile, err := os.Open(markerFilePath) - if err != nil { - return nil, fmt.Errorf("opening install marker file: %w", err) - } - defer func(installMarkerFile *os.File) { - _ = installMarkerFile.Close() - }(installMarkerFile) - installDescriptor, err := v1.ParseInstallDescriptor(installMarkerFile) - if err != nil { - return nil, fmt.Errorf("parsing install marker file: %w", err) - } - return installDescriptor, nil -} diff --git a/pkg/utils/install/file_source_test.go b/pkg/utils/install/file_source_test.go deleted file mode 100644 index 16813489745..00000000000 --- a/pkg/utils/install/file_source_test.go +++ /dev/null @@ -1,583 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License 2.0; -// you may not use this file except in compliance with the Elastic License 2.0. - -package install - -import ( - "bytes" - "errors" - "io" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" - v1 "github.com/elastic/elastic-agent/pkg/api/v1" -) - -func TestFileDescriptorSource_AddInstallDesc(t *testing.T) { - testcases := []struct { - name string - setupDir func(t *testing.T, tmpDir string) string - arg v1.AgentInstallDesc - expected *v1.InstallDescriptor - wantErr assert.ErrorAssertionFunc - postOpAssertions func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) - }{ - { - name: "no existing file, adding a descriptor creates the file and returns the updated descriptor", - arg: v1.AgentInstallDesc{ - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - expected: createInstallDescriptor([]v1.AgentInstallDesc{ - { - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - }), - wantErr: assert.NoError, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) - }, - }, - { - name: "existing empty file, adding a descriptor updates the file and returns the updated descriptor", - setupDir: func(t *testing.T, tmpDir string) string { - markerFileName := "emptydescriptor.yaml" - err := os.WriteFile(filepath.Join(tmpDir, markerFileName), nil, 0o644) - require.NoError(t, err) - return markerFileName - }, - arg: v1.AgentInstallDesc{ - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - expected: createInstallDescriptor([]v1.AgentInstallDesc{ - { - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - }), - wantErr: assert.NoError, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) - }, - }, - { - name: "existing file with another install descriptor, adding a descriptor updates the file and the descriptor", - setupDir: func(t *testing.T, tmpDir string) string { - markerFileName := "filleddescriptor.yaml" - descriptor := v1.NewInstallDescriptor() - descriptor.AgentInstalls = []v1.AgentInstallDesc{ - { - OptionalTTLItem: v1.OptionalTTLItem{}, - Version: "0.0.0", - Hash: "oooooo", - VersionedHome: "date/elastic-agent-0.0.0-oooooo", - Flavor: "oooo", - Active: false, - }, - } - - buf := new(bytes.Buffer) - err := v1.WriteInstallDescriptor(buf, descriptor) - require.NoError(t, err, "error writing install descriptor during setup") - - outfilePath := filepath.Join(tmpDir, markerFileName) - err = os.WriteFile(outfilePath, buf.Bytes(), 0o644) - require.NoError(t, err, "error writing output file %s", markerFileName) - - return markerFileName - }, - arg: v1.AgentInstallDesc{ - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - expected: createInstallDescriptor([]v1.AgentInstallDesc{ - { - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - { - Version: "0.0.0", - Hash: "oooooo", - VersionedHome: "date/elastic-agent-0.0.0-oooooo", - Flavor: "oooo", - Active: false, - }, - }), - wantErr: assert.NoError, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) - }, - }, - { - name: "existing malformed install descriptor, adding a descriptor returns error", - setupDir: func(t *testing.T, tmpDir string) string { - - markerFileName := "malformeddescriptor" - outfilePath := filepath.Join(tmpDir, markerFileName) - err := os.WriteFile(outfilePath, []byte("malformed (non-YAML) content"), 0o644) - require.NoError(t, err, "error creating output file %s", markerFileName) - - return markerFileName - }, - arg: v1.AgentInstallDesc{ - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - expected: nil, - wantErr: assert.Error, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - markerAbsPath := filepath.Join(tmpDir, installMarker) - assert.FileExists(t, markerAbsPath, "install descriptor exists at %s", installMarker) - fileContent, err := os.ReadFile(markerAbsPath) - require.NoError(t, err, "error reading file %s", markerAbsPath) - assert.Equal(t, []byte("malformed (non-YAML) content"), fileContent, "install descriptor content should be left untouched") - }, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - tmpDir := t.TempDir() - installMarkerFile := paths.MarkerFileName - if tc.setupDir != nil { - installMarkerFile = tc.setupDir(t, tmpDir) - } - - src := NewFileDescriptorSource(filepath.Join(tmpDir, installMarkerFile)) - - installDescriptor, err := src.AddInstallDesc(tc.arg) - tc.wantErr(t, err) - assert.Equal(t, tc.expected, installDescriptor) - - if tc.postOpAssertions != nil { - tc.postOpAssertions(t, tmpDir, installMarkerFile, installDescriptor) - } - }) - } -} - -func TestFileDescriptorSource_ModifyInstallDesc(t *testing.T) { - // useful variables for testcases - aMomentInTime := time.Now() - modifierFunctionError := errors.New("whoops! don't trust modifier functions") - calledModifierFunctionError := errors.New("this should not have been invoked") - - testcases := []struct { - name string - setupDir func(t *testing.T, tmpDir string) string - arg func(desc *v1.AgentInstallDesc) error - expected *v1.InstallDescriptor - wantErr assert.ErrorAssertionFunc - postOpAssertions func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) - }{ - { - name: "no existing file, modifying a descriptor returns error", - arg: func(desc *v1.AgentInstallDesc) error { - return calledModifierFunctionError - }, - expected: nil, - wantErr: assert.Error, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - assert.NoFileExists(t, filepath.Join(tmpDir, installMarker), "install descriptor should not exist at %s", installMarker) - }, - }, - { - name: "empty file, modifying a descriptor returns error", - setupDir: func(t *testing.T, tmpDir string) string { - markerFileName := "emptydescriptor.yaml" - err := os.WriteFile(filepath.Join(tmpDir, markerFileName), nil, 0o644) - require.NoError(t, err, "error creating output file %s", markerFileName) - return markerFileName - }, - arg: func(desc *v1.AgentInstallDesc) error { - return calledModifierFunctionError - }, - expected: nil, - wantErr: assert.Error, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - assert.FileExists(t, filepath.Join(tmpDir, installMarker), "install descriptor should not exist at %s", installMarker) - fileContent, err := os.ReadFile(filepath.Join(tmpDir, installMarker)) - assert.NoError(t, err, "error reading file %s", installMarker) - assert.Empty(t, fileContent, "install descriptor content should be empty") - }, - }, - { - name: "empty descriptor (not file), modifying a descriptor does not call modifier function", - setupDir: func(t *testing.T, tmpDir string) string { - installDescriptor := v1.NewInstallDescriptor() - buf := new(bytes.Buffer) - err := v1.WriteInstallDescriptor(buf, installDescriptor) - require.NoError(t, err, "error writing install descriptor during setup") - - markerFileName := "zerodescriptor.yaml" - err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) - require.NoError(t, err, "error creating output file %s", markerFileName) - return markerFileName - }, - arg: func(desc *v1.AgentInstallDesc) error { - return calledModifierFunctionError - }, - expected: createInstallDescriptor(nil), - wantErr: assert.NoError, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) - }, - }, - { - name: " valid descriptor with multiple installs, modifying a descriptor call modifier function on all installs", - setupDir: func(t *testing.T, tmpDir string) string { - markerFileName := "descriptor.yaml" - installDescriptor := createInstallDescriptor([]v1.AgentInstallDesc{ - { - Version: "4.5.6", - Hash: "ghijkl", - VersionedHome: "date/elastic-agent-4.5.6-ghijkl", - Flavor: "basic", - Active: false, - }, - { - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - { - Version: "0.0.0", - Hash: "oooooo", - VersionedHome: "date/elastic-agent-0.0.0-oooooo", - Flavor: "oooo", - Active: false, - }, - }) - - buf := new(bytes.Buffer) - err := v1.WriteInstallDescriptor(buf, installDescriptor) - require.NoError(t, err, "error writing install descriptor during setup") - - err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) - require.NoError(t, err, "error creating output file %s", markerFileName) - - return markerFileName - }, - arg: func(desc *v1.AgentInstallDesc) error { - // make version 4.5.6 active and all others inactive - if desc.Version == "4.5.6" { - desc.Active = true - } else { - desc.Active = false - desc.TTL = &aMomentInTime - } - - return nil - }, - expected: createInstallDescriptor([]v1.AgentInstallDesc{ - { - Version: "4.5.6", - Hash: "ghijkl", - VersionedHome: "date/elastic-agent-4.5.6-ghijkl", - Flavor: "basic", - Active: true, - }, - { - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: false, - OptionalTTLItem: v1.OptionalTTLItem{ - TTL: &aMomentInTime, - }, - }, - { - Version: "0.0.0", - Hash: "oooooo", - VersionedHome: "date/elastic-agent-0.0.0-oooooo", - Flavor: "oooo", - Active: false, - OptionalTTLItem: v1.OptionalTTLItem{ - TTL: &aMomentInTime, - }, - }, - }), - wantErr: assert.NoError, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) - }, - }, - { - name: " valid descriptor with installs, returns error if modifier function errors out and leaves the file untouched", - setupDir: func(t *testing.T, tmpDir string) string { - markerFileName := "descriptor.yaml" - installDescriptor := createInstallDescriptor([]v1.AgentInstallDesc{ - { - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - { - Version: "0.0.0", - Hash: "oooooo", - VersionedHome: "date/elastic-agent-0.0.0-oooooo", - Flavor: "oooo", - Active: false, - }, - }) - - buf := new(bytes.Buffer) - err := v1.WriteInstallDescriptor(buf, installDescriptor) - require.NoError(t, err, "error writing install descriptor during setup") - - err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) - require.NoError(t, err, "error creating output file %s", markerFileName) - return markerFileName - }, - arg: func(desc *v1.AgentInstallDesc) error { - // modify flavor and then return error - desc.Flavor = "touched" - return modifierFunctionError - }, - expected: nil, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorIs(t, err, modifierFunctionError, i) - }, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - untouchedDescriptor := createInstallDescriptor([]v1.AgentInstallDesc{ - { - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - { - Version: "0.0.0", - Hash: "oooooo", - VersionedHome: "date/elastic-agent-0.0.0-oooooo", - Flavor: "oooo", - Active: false, - }, - }) - checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), untouchedDescriptor) - }, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - tmpDir := t.TempDir() - installMarkerFile := paths.MarkerFileName - if tc.setupDir != nil { - installMarkerFile = tc.setupDir(t, tmpDir) - } - - src := NewFileDescriptorSource(filepath.Join(tmpDir, installMarkerFile)) - - installDescriptor, err := src.ModifyInstallDesc(tc.arg) - tc.wantErr(t, err) - assert.Equal(t, tc.expected, installDescriptor) - - if tc.postOpAssertions != nil { - tc.postOpAssertions(t, tmpDir, installMarkerFile, installDescriptor) - } - }) - } -} - -func TestFileDescriptorSource_RemoveAgentInstallDesc(t *testing.T) { - testcases := []struct { - name string - setupDir func(t *testing.T, tmpDir string) string - arg string - expected *v1.InstallDescriptor - wantErr assert.ErrorAssertionFunc - postOpAssertions func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) - }{ - { - name: "no existing file, removing an agent install descriptor returns error", - arg: "data/elastic-agent-1.2.3-abcdef", - expected: nil, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorIs(t, err, os.ErrNotExist) - }, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - assert.NoFileExists(t, filepath.Join(tmpDir, installMarker), "install descriptor should not exist at %s", installMarker) - }, - }, - { - name: "empty file, removing an agent install descriptor returns error", - setupDir: func(t *testing.T, tmpDir string) string { - markerFileName := "emptydescriptor.yaml" - err := os.WriteFile(filepath.Join(tmpDir, markerFileName), nil, 0o644) - require.NoError(t, err, "error creating output file %s", markerFileName) - return markerFileName - }, - arg: "data/elastic-agent-1.2.3-abcdef", - expected: nil, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorIs(t, err, io.EOF) - }, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - assert.FileExists(t, filepath.Join(tmpDir, installMarker), "install descriptor should not exist at %s", installMarker) - fileContent, err := os.ReadFile(filepath.Join(tmpDir, installMarker)) - assert.NoError(t, err, "error reading file %s", installMarker) - assert.Empty(t, fileContent, "install descriptor content should be empty") - }, - }, - { - name: "empty descriptor (not file), removing an agent install descriptor should not return error", - setupDir: func(t *testing.T, tmpDir string) string { - installDescriptor := v1.NewInstallDescriptor() - buf := new(bytes.Buffer) - err := v1.WriteInstallDescriptor(buf, installDescriptor) - require.NoError(t, err, "error writing install descriptor during setup") - - markerFileName := "zerodescriptor.yaml" - err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) - require.NoError(t, err, "error creating output file %s", markerFileName) - return markerFileName - }, - arg: "data/elastic-agent-1.2.3-abcdef", - expected: createInstallDescriptor(nil), - wantErr: assert.NoError, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) - }, - }, - { - name: " valid descriptor with multiple installs, removing a descriptor will delete the entries matching the versionedHome", - setupDir: func(t *testing.T, tmpDir string) string { - markerFileName := "descriptor.yaml" - installDescriptor := createInstallDescriptor([]v1.AgentInstallDesc{ - { - Version: "4.5.6", - Hash: "ghijkl", - VersionedHome: "date/elastic-agent-4.5.6-ghijkl", - Flavor: "basic", - Active: false, - }, - { - Version: "1.2.3", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: true, - }, - { - Version: "0.0.0", - Hash: "oooooo", - VersionedHome: "date/elastic-agent-0.0.0-oooooo", - Flavor: "oooo", - Active: false, - }, - { - Version: "1.2.3 x2", - Hash: "abcdef", - VersionedHome: "date/elastic-agent-1.2.3-abcdef", - Flavor: "basic", - Active: false, - }, - }) - - buf := new(bytes.Buffer) - err := v1.WriteInstallDescriptor(buf, installDescriptor) - require.NoError(t, err, "error writing install descriptor during setup") - - err = os.WriteFile(filepath.Join(tmpDir, markerFileName), buf.Bytes(), 0o644) - require.NoError(t, err, "error creating output file %s", markerFileName) - - return markerFileName - }, - arg: "date/elastic-agent-1.2.3-abcdef", - expected: createInstallDescriptor([]v1.AgentInstallDesc{ - { - Version: "4.5.6", - Hash: "ghijkl", - VersionedHome: "date/elastic-agent-4.5.6-ghijkl", - Flavor: "basic", - Active: false, - }, - { - Version: "0.0.0", - Hash: "oooooo", - VersionedHome: "date/elastic-agent-0.0.0-oooooo", - Flavor: "oooo", - Active: false, - }, - }), - wantErr: assert.NoError, - postOpAssertions: func(t *testing.T, tmpDir string, installMarker string, actual *v1.InstallDescriptor) { - checkInstallDescriptorMatches(t, filepath.Join(tmpDir, installMarker), actual) - }, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - tmpDir := t.TempDir() - installMarkerFile := paths.MarkerFileName - if tc.setupDir != nil { - installMarkerFile = tc.setupDir(t, tmpDir) - } - - src := NewFileDescriptorSource(filepath.Join(tmpDir, installMarkerFile)) - - installDescriptor, err := src.RemoveAgentInstallDesc(tc.arg) - tc.wantErr(t, err) - assert.Equal(t, tc.expected, installDescriptor) - - if tc.postOpAssertions != nil { - tc.postOpAssertions(t, tmpDir, installMarkerFile, installDescriptor) - } - }) - } -} - -func createInstallDescriptor(agentInstalls []v1.AgentInstallDesc) *v1.InstallDescriptor { - descriptor := v1.NewInstallDescriptor() - descriptor.AgentInstalls = agentInstalls - return descriptor -} - -func checkInstallDescriptorMatches(t *testing.T, markerFile string, descriptor *v1.InstallDescriptor) { - require.FileExists(t, markerFile, "install marker file should exist") - buf := new(bytes.Buffer) - err := v1.WriteInstallDescriptor(buf, descriptor) - require.NoError(t, err, "error marshaling install descriptor") - fileRawData, err := os.ReadFile(markerFile) - require.NoError(t, err, "error marshaling install descriptor") - - assert.YAMLEq(t, buf.String(), string(fileRawData), "install marker file should match marshalled install descriptor") -} From c81529cfeee70a49a4e2fa42e8459ab02425253a Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Thu, 16 Oct 2025 14:21:44 +0200 Subject: [PATCH 10/18] regenerate mocks --- .../mock_availablerollbackssource_test.go | 139 ------------------ .../pkg/agent/application/upgrade/mocks.go | 133 +++++++++++++++++ 2 files changed, 133 insertions(+), 139 deletions(-) delete mode 100644 internal/pkg/agent/application/upgrade/mock_availablerollbackssource_test.go diff --git a/internal/pkg/agent/application/upgrade/mock_availablerollbackssource_test.go b/internal/pkg/agent/application/upgrade/mock_availablerollbackssource_test.go deleted file mode 100644 index e4a383e3576..00000000000 --- a/internal/pkg/agent/application/upgrade/mock_availablerollbackssource_test.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License 2.0; -// you may not use this file except in compliance with the Elastic License 2.0. - -// Code generated by mockery v2.53.4. DO NOT EDIT. - -package upgrade - -import mock "github.com/stretchr/testify/mock" - -// mockAvailableRollbacksSource is an autogenerated mock type for the availableRollbacksSource type -type mockAvailableRollbacksSource struct { - mock.Mock -} - -type mockAvailableRollbacksSource_Expecter struct { - mock *mock.Mock -} - -func (_m *mockAvailableRollbacksSource) EXPECT() *mockAvailableRollbacksSource_Expecter { - return &mockAvailableRollbacksSource_Expecter{mock: &_m.Mock} -} - -// Get provides a mock function with no fields -func (_m *mockAvailableRollbacksSource) Get() (map[string]TTLMarker, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Get") - } - - var r0 map[string]TTLMarker - var r1 error - if rf, ok := ret.Get(0).(func() (map[string]TTLMarker, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() map[string]TTLMarker); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]TTLMarker) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockAvailableRollbacksSource_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' -type mockAvailableRollbacksSource_Get_Call struct { - *mock.Call -} - -// Get is a helper method to define mock.On call -func (_e *mockAvailableRollbacksSource_Expecter) Get() *mockAvailableRollbacksSource_Get_Call { - return &mockAvailableRollbacksSource_Get_Call{Call: _e.mock.On("Get")} -} - -func (_c *mockAvailableRollbacksSource_Get_Call) Run(run func()) *mockAvailableRollbacksSource_Get_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *mockAvailableRollbacksSource_Get_Call) Return(_a0 map[string]TTLMarker, _a1 error) *mockAvailableRollbacksSource_Get_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockAvailableRollbacksSource_Get_Call) RunAndReturn(run func() (map[string]TTLMarker, error)) *mockAvailableRollbacksSource_Get_Call { - _c.Call.Return(run) - return _c -} - -// Set provides a mock function with given fields: _a0 -func (_m *mockAvailableRollbacksSource) Set(_a0 map[string]TTLMarker) error { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for Set") - } - - var r0 error - if rf, ok := ret.Get(0).(func(map[string]TTLMarker) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockAvailableRollbacksSource_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' -type mockAvailableRollbacksSource_Set_Call struct { - *mock.Call -} - -// Set is a helper method to define mock.On call -// - _a0 map[string]TTLMarker -func (_e *mockAvailableRollbacksSource_Expecter) Set(_a0 interface{}) *mockAvailableRollbacksSource_Set_Call { - return &mockAvailableRollbacksSource_Set_Call{Call: _e.mock.On("Set", _a0)} -} - -func (_c *mockAvailableRollbacksSource_Set_Call) Run(run func(_a0 map[string]TTLMarker)) *mockAvailableRollbacksSource_Set_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(map[string]TTLMarker)) - }) - return _c -} - -func (_c *mockAvailableRollbacksSource_Set_Call) Return(_a0 error) *mockAvailableRollbacksSource_Set_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockAvailableRollbacksSource_Set_Call) RunAndReturn(run func(map[string]TTLMarker) error) *mockAvailableRollbacksSource_Set_Call { - _c.Call.Return(run) - return _c -} - -// newMockAvailableRollbacksSource creates a new instance of mockAvailableRollbacksSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockAvailableRollbacksSource(t interface { - mock.TestingT - Cleanup(func()) -}) *mockAvailableRollbacksSource { - mock := &mockAvailableRollbacksSource{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/pkg/agent/application/upgrade/mocks.go b/internal/pkg/agent/application/upgrade/mocks.go index 1820d1a3c90..5ef4993d084 100644 --- a/internal/pkg/agent/application/upgrade/mocks.go +++ b/internal/pkg/agent/application/upgrade/mocks.go @@ -339,6 +339,139 @@ func (_c *MockWatcherHelper_WaitForWatcher_Call) RunAndReturn(run func(ctx conte return _c } +// newMockAvailableRollbacksSource creates a new instance of mockAvailableRollbacksSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockAvailableRollbacksSource(t interface { + mock.TestingT + Cleanup(func()) +}) *mockAvailableRollbacksSource { + mock := &mockAvailableRollbacksSource{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// mockAvailableRollbacksSource is an autogenerated mock type for the availableRollbacksSource type +type mockAvailableRollbacksSource struct { + mock.Mock +} + +type mockAvailableRollbacksSource_Expecter struct { + mock *mock.Mock +} + +func (_m *mockAvailableRollbacksSource) EXPECT() *mockAvailableRollbacksSource_Expecter { + return &mockAvailableRollbacksSource_Expecter{mock: &_m.Mock} +} + +// Get provides a mock function for the type mockAvailableRollbacksSource +func (_mock *mockAvailableRollbacksSource) Get() (map[string]TTLMarker, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 map[string]TTLMarker + var r1 error + if returnFunc, ok := ret.Get(0).(func() (map[string]TTLMarker, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() map[string]TTLMarker); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]TTLMarker) + } + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// mockAvailableRollbacksSource_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type mockAvailableRollbacksSource_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +func (_e *mockAvailableRollbacksSource_Expecter) Get() *mockAvailableRollbacksSource_Get_Call { + return &mockAvailableRollbacksSource_Get_Call{Call: _e.mock.On("Get")} +} + +func (_c *mockAvailableRollbacksSource_Get_Call) Run(run func()) *mockAvailableRollbacksSource_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockAvailableRollbacksSource_Get_Call) Return(stringToTTLMarker map[string]TTLMarker, err error) *mockAvailableRollbacksSource_Get_Call { + _c.Call.Return(stringToTTLMarker, err) + return _c +} + +func (_c *mockAvailableRollbacksSource_Get_Call) RunAndReturn(run func() (map[string]TTLMarker, error)) *mockAvailableRollbacksSource_Get_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type mockAvailableRollbacksSource +func (_mock *mockAvailableRollbacksSource) Set(stringToTTLMarker map[string]TTLMarker) error { + ret := _mock.Called(stringToTTLMarker) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(map[string]TTLMarker) error); ok { + r0 = returnFunc(stringToTTLMarker) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// mockAvailableRollbacksSource_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type mockAvailableRollbacksSource_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - stringToTTLMarker map[string]TTLMarker +func (_e *mockAvailableRollbacksSource_Expecter) Set(stringToTTLMarker interface{}) *mockAvailableRollbacksSource_Set_Call { + return &mockAvailableRollbacksSource_Set_Call{Call: _e.mock.On("Set", stringToTTLMarker)} +} + +func (_c *mockAvailableRollbacksSource_Set_Call) Run(run func(stringToTTLMarker map[string]TTLMarker)) *mockAvailableRollbacksSource_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 map[string]TTLMarker + if args[0] != nil { + arg0 = args[0].(map[string]TTLMarker) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *mockAvailableRollbacksSource_Set_Call) Return(err error) *mockAvailableRollbacksSource_Set_Call { + _c.Call.Return(err) + return _c +} + +func (_c *mockAvailableRollbacksSource_Set_Call) RunAndReturn(run func(stringToTTLMarker map[string]TTLMarker) error) *mockAvailableRollbacksSource_Set_Call { + _c.Call.Return(run) + return _c +} + // newMockWatcherGrappler creates a new instance of mockWatcherGrappler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockWatcherGrappler(t interface { From 377f80d420dd3af595b41e358e79efd7648225d4 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Thu, 16 Oct 2025 14:24:53 +0200 Subject: [PATCH 11/18] fix linter errors --- internal/pkg/agent/application/upgrade/ttl_marker_source.go | 2 +- .../pkg/agent/application/upgrade/ttl_marker_source_test.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/pkg/agent/application/upgrade/ttl_marker_source.go b/internal/pkg/agent/application/upgrade/ttl_marker_source.go index d095a128392..23a52ed4e66 100644 --- a/internal/pkg/agent/application/upgrade/ttl_marker_source.go +++ b/internal/pkg/agent/application/upgrade/ttl_marker_source.go @@ -40,7 +40,7 @@ func (T TTLMarkerRegistry) Set(m map[string]TTLMarker) error { return fmt.Errorf("accessing existing markers: %w", err) } - for versionedHome, _ := range existingMarkers { + for versionedHome := range existingMarkers { _, ok := m[versionedHome] if !ok { // the existing marker should not be in the final state diff --git a/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go b/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go index 53e94236315..d3f4a86400d 100644 --- a/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go +++ b/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go @@ -33,7 +33,6 @@ func TestTTLMarkerRegistry_AddOrReplace(t *testing.T) { yesterday := now.Add(-24 * time.Hour) yesterdayString := yesterday.Format(time.RFC3339) - yesterday, _ = time.Parse(time.RFC3339, yesterdayString) tomorrow := now.Add(24 * time.Hour) tomorrowString := tomorrow.Format(time.RFC3339) @@ -224,10 +223,10 @@ func TestTTLMarkerRegistry_Get(t *testing.T) { tt.setup(t, tmpDir) T := NewTTLMarkerRegistry(tmpDir) got, err := T.Get() - if !tt.wantErr(t, err, fmt.Sprintf("Get()")) { + if !tt.wantErr(t, err, "Get()") { return } - assert.Equalf(t, tt.want, got, "Get()") + assert.Equal(t, tt.want, got, "Get()") }) } } From 508ecc0e1575987a41bd66ad32d24ef6a56f29c6 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Mon, 20 Oct 2025 08:32:08 +0200 Subject: [PATCH 12/18] make TTLMarkerRegistry.addOrReplace private --- internal/pkg/agent/application/upgrade/ttl_marker_source.go | 4 ++-- .../pkg/agent/application/upgrade/ttl_marker_source_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/pkg/agent/application/upgrade/ttl_marker_source.go b/internal/pkg/agent/application/upgrade/ttl_marker_source.go index 23a52ed4e66..5b9dec3f9af 100644 --- a/internal/pkg/agent/application/upgrade/ttl_marker_source.go +++ b/internal/pkg/agent/application/upgrade/ttl_marker_source.go @@ -21,7 +21,7 @@ type TTLMarkerRegistry struct { markerFileGlobPattern string } -func (T TTLMarkerRegistry) AddOrReplace(m map[string]TTLMarker) error { +func (T TTLMarkerRegistry) addOrReplace(m map[string]TTLMarker) error { for versionedHome, marker := range m { dstFilePath := filepath.Join(T.baseDir, versionedHome, ttlMarkerName) err := writeTTLMarker(dstFilePath, marker) @@ -52,7 +52,7 @@ func (T TTLMarkerRegistry) Set(m map[string]TTLMarker) error { } // create all the remaining markers - return T.AddOrReplace(m) + return T.addOrReplace(m) } func (T TTLMarkerRegistry) Get() (map[string]TTLMarker, error) { diff --git a/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go b/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go index d3f4a86400d..ccd95046cb6 100644 --- a/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go +++ b/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go @@ -104,8 +104,8 @@ func TestTTLMarkerRegistry_AddOrReplace(t *testing.T) { tt.setup(t, tmpDir) } T := NewTTLMarkerRegistry(tmpDir) - err := T.AddOrReplace(tt.args.m) - if !tt.wantErr(t, err, fmt.Sprintf("AddOrReplace(%v)", tt.args.m)) { + err := T.addOrReplace(tt.args.m) + if !tt.wantErr(t, err, fmt.Sprintf("addOrReplace(%v)", tt.args.m)) { return } if tt.postAssertions != nil { From 11a5a70bc9bd48cb9f8a94fa88bd543525111475 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Mon, 20 Oct 2025 08:52:45 +0200 Subject: [PATCH 13/18] remove GetFullVersion utility function --- pkg/utils/manifest/version.go | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 pkg/utils/manifest/version.go diff --git a/pkg/utils/manifest/version.go b/pkg/utils/manifest/version.go deleted file mode 100644 index 06a009b8be3..00000000000 --- a/pkg/utils/manifest/version.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License 2.0; -// you may not use this file except in compliance with the Elastic License 2.0. - -package manifest - -import v1 "github.com/elastic/elastic-agent/pkg/api/v1" - -func GetFullVersion(manifest *v1.PackageManifest) string { - version := manifest.Package.Version - if manifest.Package.Snapshot { - version += "-SNAPSHOT" - } - return version -} From eaa7582807d2534879032323322481c35a1b78dc Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Mon, 20 Oct 2025 14:15:02 +0200 Subject: [PATCH 14/18] Add logger to ttl marker source --- internal/pkg/agent/application/application.go | 2 +- .../coordinator/coordinator_unit_test.go | 2 +- .../application/upgrade/ttl_marker_source.go | 33 +++--- .../upgrade/ttl_marker_source_test.go | 105 +----------------- 4 files changed, 28 insertions(+), 114 deletions(-) diff --git a/internal/pkg/agent/application/application.go b/internal/pkg/agent/application/application.go index 949f4f02e64..825b078cceb 100644 --- a/internal/pkg/agent/application/application.go +++ b/internal/pkg/agent/application/application.go @@ -130,7 +130,7 @@ func New( // monitoring is not supported in bootstrap mode https://github.com/elastic/elastic-agent/issues/1761 isMonitoringSupported := !disableMonitoring && cfg.Settings.V1MonitoringEnabled - availableRollbacksSource := upgrade.NewTTLMarkerRegistry(paths.Top()) + availableRollbacksSource := upgrade.NewTTLMarkerRegistry(log, paths.Top()) upgrader, err := upgrade.NewUpgrader(log, cfg.Settings.DownloadConfig, cfg.Settings.Upgrade, agentInfo, new(upgrade.AgentWatcherHelper), availableRollbacksSource) if err != nil { return nil, nil, nil, fmt.Errorf("failed to create upgrader: %w", err) diff --git a/internal/pkg/agent/application/coordinator/coordinator_unit_test.go b/internal/pkg/agent/application/coordinator/coordinator_unit_test.go index e9c661d6f57..559ad802c7f 100644 --- a/internal/pkg/agent/application/coordinator/coordinator_unit_test.go +++ b/internal/pkg/agent/application/coordinator/coordinator_unit_test.go @@ -468,7 +468,7 @@ func TestCoordinatorReportsInvalidPolicy(t *testing.T) { }() tmpDir := t.TempDir() - upgradeMgr, err := upgrade.NewUpgrader(log, &artifact.Config{}, nil, &info.AgentInfo{}, new(upgrade.AgentWatcherHelper), upgrade.NewTTLMarkerRegistry(tmpDir)) + upgradeMgr, err := upgrade.NewUpgrader(log, &artifact.Config{}, nil, &info.AgentInfo{}, new(upgrade.AgentWatcherHelper), upgrade.NewTTLMarkerRegistry(nil, tmpDir)) require.NoError(t, err, "errored when creating a new upgrader") // Channels have buffer length 1, so we don't have to run on multiple diff --git a/internal/pkg/agent/application/upgrade/ttl_marker_source.go b/internal/pkg/agent/application/upgrade/ttl_marker_source.go index 5b9dec3f9af..4445ff4526e 100644 --- a/internal/pkg/agent/application/upgrade/ttl_marker_source.go +++ b/internal/pkg/agent/application/upgrade/ttl_marker_source.go @@ -10,6 +10,8 @@ import ( "path/filepath" "gopkg.in/yaml.v2" + + "github.com/elastic/elastic-agent/pkg/core/logger" ) const ttlMarkerName = ".ttl" @@ -19,18 +21,15 @@ var defaultMarkerGlobPattern = filepath.Join("data", "elastic-agent-*", ttlMarke type TTLMarkerRegistry struct { baseDir string markerFileGlobPattern string + log *logger.Logger } -func (T TTLMarkerRegistry) addOrReplace(m map[string]TTLMarker) error { - for versionedHome, marker := range m { - dstFilePath := filepath.Join(T.baseDir, versionedHome, ttlMarkerName) - err := writeTTLMarker(dstFilePath, marker) - if err != nil { - return fmt.Errorf("writing marker %q: %w", dstFilePath, err) - } +func NewTTLMarkerRegistry(log *logger.Logger, baseDir string) *TTLMarkerRegistry { + return &TTLMarkerRegistry{ + baseDir: baseDir, + markerFileGlobPattern: defaultMarkerGlobPattern, + log: log, } - - return nil } func (T TTLMarkerRegistry) Set(m map[string]TTLMarker) error { @@ -44,6 +43,7 @@ func (T TTLMarkerRegistry) Set(m map[string]TTLMarker) error { _, ok := m[versionedHome] if !ok { // the existing marker should not be in the final state + T.log.Debugf("Removing marker for versionedHome: %s", versionedHome) err = os.Remove(filepath.Join(T.baseDir, versionedHome, ttlMarkerName)) if err != nil { return fmt.Errorf("removing ttl marker for %q: %w", versionedHome, err) @@ -60,8 +60,10 @@ func (T TTLMarkerRegistry) Get() (map[string]TTLMarker, error) { if err != nil { return nil, fmt.Errorf("failed to glob files using %q: %w", T.markerFileGlobPattern, err) } + T.log.Debugf("Found matching versionedHomes: %v", matches) ttlMarkers := make(map[string]TTLMarker, len(matches)) for _, match := range matches { + T.log.Debugf("Reading marker from versionedHome: %s", match) relPath, err := filepath.Rel(T.baseDir, filepath.Dir(match)) if err != nil { return nil, fmt.Errorf("failed to determine path for %q relative to %q : %w", match, T.baseDir, err) @@ -76,11 +78,16 @@ func (T TTLMarkerRegistry) Get() (map[string]TTLMarker, error) { return ttlMarkers, nil } -func NewTTLMarkerRegistry(baseDir string) *TTLMarkerRegistry { - return &TTLMarkerRegistry{ - baseDir: baseDir, - markerFileGlobPattern: defaultMarkerGlobPattern, +func (T TTLMarkerRegistry) addOrReplace(m map[string]TTLMarker) error { + for versionedHome, marker := range m { + dstFilePath := filepath.Join(T.baseDir, versionedHome, ttlMarkerName) + err := writeTTLMarker(dstFilePath, marker) + if err != nil { + return fmt.Errorf("writing marker %q: %w", dstFilePath, err) + } } + + return nil } func readTTLMarker(filePath string) (TTLMarker, error) { diff --git a/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go b/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go index ccd95046cb6..3ac95e1769d 100644 --- a/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go +++ b/internal/pkg/agent/application/upgrade/ttl_marker_source_test.go @@ -16,104 +16,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" -) - -func TestTTLMarkerRegistry_AddOrReplace(t *testing.T) { - const TTLMarkerYAMLTemplate = ` - version: {{ .Version }} - valid_until: {{ .ValidUntil }}` - - expectedMarkerContentTemplate, err := template.New("expected marker").Parse(TTLMarkerYAMLTemplate) - require.NoError(t, err) - - now := time.Now() - nowString := now.Format(time.RFC3339) - // re-parse now to account for loss of fidelity due to marshal/unmarshal - now, _ = time.Parse(time.RFC3339, nowString) - - yesterday := now.Add(-24 * time.Hour) - yesterdayString := yesterday.Format(time.RFC3339) - - tomorrow := now.Add(24 * time.Hour) - tomorrowString := tomorrow.Format(time.RFC3339) - tomorrow, _ = time.Parse(time.RFC3339, tomorrowString) - - versions := []string{"1.2.3", "4.5.6"} - versionedHomes := []string{"elastic-agent-1.2.3-past", "elastic-agent-4.5.6-present"} - - type args struct { - m map[string]TTLMarker - } - tests := []struct { - name string - setup func(t *testing.T, tmpDir string) - args args - wantErr assert.ErrorAssertionFunc - postAssertions func(t *testing.T, tmpDir string) - }{ - { - name: "ttl are present in one install - all get created/replaced", - setup: func(t *testing.T, tmpDir string) { - for i, versionedHome := range versionedHomes { - err := os.MkdirAll(filepath.Join(tmpDir, "data", versionedHome), 0755) - require.NoError(t, err, "error setting up fake agent install directory") - - if i < 1 { - // add only 1 ttl marker as part of setup - buf := bytes.Buffer{} - err = expectedMarkerContentTemplate.Execute(&buf, map[string]string{"Version": versions[i], "ValidUntil": yesterdayString}) - require.NoError(t, err, "error executing ttl marker template") - err = os.WriteFile(filepath.Join(tmpDir, "data", versionedHome, ttlMarkerName), buf.Bytes(), 0644) - require.NoError(t, err, "error setting up fake agent ttl marker") - } - } - }, - args: args{ - map[string]TTLMarker{ - filepath.Join("data", versionedHomes[0]): { - Version: versions[0], - ValidUntil: tomorrow, - }, - filepath.Join("data", versionedHomes[1]): { - Version: versions[1], - ValidUntil: tomorrow, - }, - }, - }, - wantErr: assert.NoError, - postAssertions: func(t *testing.T, tmpDir string) { - for i, versionedHome := range versionedHomes { - expectedTTLMarkerFilePath := filepath.Join(tmpDir, "data", versionedHome, ttlMarkerName) - if assert.FileExists(t, expectedTTLMarkerFilePath, "TTL marker should have been created/replaced") { - b := new(strings.Builder) - err = expectedMarkerContentTemplate.Execute(b, map[string]string{"Version": versions[i], "ValidUntil": tomorrowString}) - require.NoError(t, err) - actualMarkerContent, err := os.ReadFile(expectedTTLMarkerFilePath) - require.NoError(t, err) - assert.YAMLEq(t, b.String(), string(actualMarkerContent)) - } - } - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpDir := t.TempDir() - if tt.setup != nil { - tt.setup(t, tmpDir) - } - T := NewTTLMarkerRegistry(tmpDir) - err := T.addOrReplace(tt.args.m) - if !tt.wantErr(t, err, fmt.Sprintf("addOrReplace(%v)", tt.args.m)) { - return - } - if tt.postAssertions != nil { - tt.postAssertions(t, tmpDir) - } - }) - } -} + "github.com/elastic/elastic-agent/pkg/core/logger/loggertest" +) func TestTTLMarkerRegistry_Get(t *testing.T) { const TTLMarkerYAMLTemplate = ` @@ -221,7 +126,8 @@ func TestTTLMarkerRegistry_Get(t *testing.T) { t.Run(tt.name, func(t *testing.T) { tmpDir := t.TempDir() tt.setup(t, tmpDir) - T := NewTTLMarkerRegistry(tmpDir) + testLogger, _ := loggertest.New(t.Name()) + T := NewTTLMarkerRegistry(testLogger, tmpDir) got, err := T.Get() if !tt.wantErr(t, err, "Get()") { return @@ -324,7 +230,8 @@ func TestTTLMarkerRegistry_Set(t *testing.T) { if tt.setup != nil { tt.setup(t, tmpDir) } - T := NewTTLMarkerRegistry(tmpDir) + testLogger, _ := loggertest.New(t.Name()) + T := NewTTLMarkerRegistry(testLogger, tmpDir) tt.wantErr(t, T.Set(tt.args.m), fmt.Sprintf("Set(%v)", tt.args.m)) if tt.postAssertions != nil { tt.postAssertions(t, tmpDir) From d8ff91f366f54c1ce3a87d09837caaa0aecda5cf Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Mon, 20 Oct 2025 14:15:49 +0200 Subject: [PATCH 15/18] Revert "extract manifest PathMapper to its own package" This reverts commit 0921bec367662a48e16ef1727d1ba013f61d2126. --- .../agent/application/upgrade/step_unpack.go | 26 ++++++++++++++----- .../integration/ess/upgrade_rollback_test.go | 1 + 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/internal/pkg/agent/application/upgrade/step_unpack.go b/internal/pkg/agent/application/upgrade/step_unpack.go index 987bbe50a4b..ae0f964ee65 100644 --- a/internal/pkg/agent/application/upgrade/step_unpack.go +++ b/internal/pkg/agent/application/upgrade/step_unpack.go @@ -24,7 +24,6 @@ import ( v1 "github.com/elastic/elastic-agent/pkg/api/v1" "github.com/elastic/elastic-agent/pkg/component" "github.com/elastic/elastic-agent/pkg/core/logger" - manifestutils "github.com/elastic/elastic-agent/pkg/utils/manifest" ) // UnpackResult contains the location and hash of the unpacked agent files @@ -116,7 +115,7 @@ func unzip(log *logger.Logger, archivePath, dataDir string, flavor string, copy fileNamePrefix := strings.TrimSuffix(filepath.Base(archivePath), ".zip") + "/" // omitting `elastic-agent-{version}-{os}-{arch}/` in filename - var pm *manifestutils.PathMapper + pm := pathMapper{} var versionedHome string metadata, err := getPackageMetadataFromZipReader(r, fileNamePrefix) @@ -127,11 +126,10 @@ func unzip(log *logger.Logger, archivePath, dataDir string, flavor string, copy hash = metadata.hash[:hashLen] var registry map[string][]string if metadata.manifest != nil { - pm = manifestutils.NewPathMapper(metadata.manifest.Package.PathMappings) + pm.mappings = metadata.manifest.Package.PathMappings versionedHome = filepath.FromSlash(pm.Map(metadata.manifest.Package.VersionedHome)) registry = metadata.manifest.Package.Flavors } else { - pm = manifestutils.NewPathMapper(nil) // if at this point we didn't load the manifest, set the versioned to the backup value versionedHome = createVersionedHomeFromHash(hash) } @@ -363,7 +361,7 @@ func untar(log *logger.Logger, archivePath, dataDir string, flavor string, copy var hash string // Look up manifest in the archive and prepare path mappings, if any - var pm *manifestutils.PathMapper + pm := pathMapper{} metadata, err := getPackageMetadataFromTar(archivePath) if err != nil { @@ -375,11 +373,10 @@ func untar(log *logger.Logger, archivePath, dataDir string, flavor string, copy if metadata.manifest != nil { // set the path mappings - pm = manifestutils.NewPathMapper(metadata.manifest.Package.PathMappings) + pm.mappings = metadata.manifest.Package.PathMappings versionedHome = filepath.FromSlash(pm.Map(metadata.manifest.Package.VersionedHome)) registry = metadata.manifest.Package.Flavors } else { - pm = manifestutils.NewPathMapper(nil) // set default value of versioned home if it wasn't set by reading the manifest versionedHome = createVersionedHomeFromHash(metadata.hash) } @@ -664,6 +661,21 @@ func validFileName(p string) bool { return true } +type pathMapper struct { + mappings []map[string]string +} + +func (pm pathMapper) Map(packagePath string) string { + for _, mapping := range pm.mappings { + for pkgPath, mappedPath := range mapping { + if strings.HasPrefix(packagePath, pkgPath) { + return path.Join(mappedPath, packagePath[len(pkgPath):]) + } + } + } + return packagePath +} + type tarCloser struct { tarFile *os.File gzipReader *gzip.Reader diff --git a/testing/integration/ess/upgrade_rollback_test.go b/testing/integration/ess/upgrade_rollback_test.go index 75f065a6d14..71e94ff2cc2 100644 --- a/testing/integration/ess/upgrade_rollback_test.go +++ b/testing/integration/ess/upgrade_rollback_test.go @@ -229,6 +229,7 @@ func TestStandaloneUpgradeRollbackOnRestarts(t *testing.T) { atesting.WithFetcher(atesting.ArtifactFetcher()), ) require.NoError(t, err) + return fromFixture, toFixture }, }, From 478435f348743667d1ef8185663c57f7b1c174cf Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Tue, 21 Oct 2025 11:41:32 +0200 Subject: [PATCH 16/18] Add fatal upgrade error if ttl marker cannot be set and related cleanup --- .../pkg/agent/application/upgrade/upgrade.go | 29 ++++++++++++------- .../agent/application/upgrade/upgrade_test.go | 4 +-- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index c7fe76f23e0..d092d3a0452 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -92,7 +92,7 @@ type copyRunDirectoryFunc func(log *logger.Logger, oldRunPath, newRunPath string type fileDirCopyFunc func(from, to string, opts ...filecopy.Options) error type markUpgradeFunc func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks map[string]TTLMarker) error type changeSymlinkFunc func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error -type rollbackInstallFunc func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error +type rollbackInstallFunc func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, rollbackSource availableRollbacksSource) error // Types used to abstract stdlib functions type mkdirAllFunc func(name string, perm fs.FileMode) error @@ -440,7 +440,7 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s if err := u.changeSymlink(u.log, paths.Top(), symlinkPath, newPath); err != nil { u.log.Errorw("Rolling back: changing symlink failed", "error.message", err) - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.availableRollbacksSource) return nil, goerrors.Join(err, rollbackErr) } @@ -470,34 +470,36 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s availableRollbacks := getAvailableRollbacks(rollbackWindow, time.Now(), release.VersionWithSnapshot(), previousParsedVersion, currentVersionedHome) - if err := u.markUpgrade(u.log, + if err = u.availableRollbacksSource.Set(availableRollbacks); err != nil { + u.log.Errorw("Rolling back: setting ttl markers failed", "error.message", err) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.availableRollbacksSource) + return nil, goerrors.Join(err, rollbackErr) + } + + if err = u.markUpgrade(u.log, paths.Data(), // data dir to place the marker in time.Now(), current, // new agent version data previous, // old agent version data action, det, availableRollbacks); err != nil { u.log.Errorw("Rolling back: marking upgrade failed", "error.message", err) - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.availableRollbacksSource) return nil, goerrors.Join(err, rollbackErr) } - if err = u.availableRollbacksSource.Set(availableRollbacks); err != nil { - u.log.Warnw("Setting rollback targets failed. Manual rollback may not be available beyond grace period", "error.message", err) - } - watcherExecutable := u.watcherHelper.SelectWatcherExecutable(paths.Top(), previous, current) var watcherCmd *exec.Cmd if watcherCmd, err = u.watcherHelper.InvokeWatcher(u.log, watcherExecutable); err != nil { u.log.Errorw("Rolling back: starting watcher failed", "error.message", err) - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.availableRollbacksSource) return nil, goerrors.Join(err, rollbackErr) } watcherWaitErr := u.watcherHelper.WaitForWatcher(ctx, u.log, markerFilePath(paths.Data()), watcherMaxWaitTime) if watcherWaitErr != nil { killWatcherErr := watcherCmd.Process.Kill() - rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome) + rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.availableRollbacksSource) return nil, goerrors.Join(watcherWaitErr, killWatcherErr, rollbackErr) } @@ -729,7 +731,7 @@ func isSameVersion(log *logger.Logger, current agentVersion, newVersion agentVer return current == newVersion } -func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error { +func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, rollbackSource availableRollbacksSource) error { oldAgentPath := paths.BinaryPath(filepath.Join(topDirPath, oldVersionedHome), agentName) err := changeSymlink(log, topDirPath, filepath.Join(topDirPath, agentName), oldAgentPath) if err != nil && !errors.Is(err, fs.ErrNotExist) { @@ -742,6 +744,11 @@ func rollbackInstall(ctx context.Context, log *logger.Logger, topDirPath, versio return fmt.Errorf("rolling back install: removing new agent install at %q failed: %w", newAgentInstallPath, err) } + err = rollbackSource.Set(nil) + if err != nil { + return fmt.Errorf("rolling back install: error clearing ttl markers: %w", err) + } + return nil } diff --git a/internal/pkg/agent/application/upgrade/upgrade_test.go b/internal/pkg/agent/application/upgrade/upgrade_test.go index f7eb6ac3a45..3159ee227ca 100644 --- a/internal/pkg/agent/application/upgrade/upgrade_test.go +++ b/internal/pkg/agent/application/upgrade/upgrade_test.go @@ -1547,7 +1547,7 @@ func TestUpgradeErrorHandling(t *testing.T) { upgrader.copyRunDirectory = func(log *logger.Logger, oldRunPath, newRunPath string) error { return nil } - upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error { + upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, source availableRollbacksSource) error { return nil } upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error { @@ -1593,7 +1593,7 @@ func TestUpgradeErrorHandling(t *testing.T) { upgrader.changeSymlink = func(log *logger.Logger, topDirPath, symlinkPath, newTarget string) error { return nil } - upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string) error { + upgrader.rollbackInstall = func(ctx context.Context, log *logger.Logger, topDirPath, versionedHome, oldVersionedHome string, source availableRollbacksSource) error { return nil } upgrader.markUpgrade = func(log *logger.Logger, dataDirPath string, updatedOn time.Time, agent, previousAgent agentInstall, action *fleetapi.ActionUpgrade, upgradeDetails *details.Details, availableRollbacks map[string]TTLMarker) error { From 2f565ff3ba779ff2947d5b65f41a81c97c7828ec Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Tue, 21 Oct 2025 11:49:44 +0200 Subject: [PATCH 17/18] minor fixes --- internal/pkg/agent/application/upgrade/upgrade.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index d092d3a0452..359fdf25fa5 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -407,11 +407,6 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s return nil, err } - currentVersionedHome, err := filepath.Rel(paths.Top(), paths.Home()) - if err != nil { - return nil, fmt.Errorf("calculating home path relative to top, home: %q top: %q : %w", paths.Home(), paths.Top(), err) - } - newHash := unpackRes.Hash if newHash == "" { return nil, errors.New("unknown hash") @@ -438,6 +433,11 @@ func (u *Upgrader) Upgrade(ctx context.Context, version string, rollback bool, s // paths.BinaryPath properly derives the binary directory depending on the platform. The path to the binary for macOS is inside of the app bundle. newPath := paths.BinaryPath(filepath.Join(paths.Top(), hashedDir), agentName) + currentVersionedHome, err := filepath.Rel(paths.Top(), paths.Home()) + if err != nil { + return nil, fmt.Errorf("calculating home path relative to top, home: %q top: %q : %w", paths.Home(), paths.Top(), err) + } + if err := u.changeSymlink(u.log, paths.Top(), symlinkPath, newPath); err != nil { u.log.Errorw("Rolling back: changing symlink failed", "error.message", err) rollbackErr := u.rollbackInstall(ctx, u.log, paths.Top(), hashedDir, currentVersionedHome, u.availableRollbacksSource) From 9303e2ba68ae9768672bcb438c4f36faaf0b1417 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Tue, 21 Oct 2025 14:35:52 +0200 Subject: [PATCH 18/18] fixup! Add fatal upgrade error if ttl marker cannot be set and related cleanup --- internal/pkg/agent/application/upgrade/upgrade_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/pkg/agent/application/upgrade/upgrade_test.go b/internal/pkg/agent/application/upgrade/upgrade_test.go index 3159ee227ca..a019fa0e2b5 100644 --- a/internal/pkg/agent/application/upgrade/upgrade_test.go +++ b/internal/pkg/agent/application/upgrade/upgrade_test.go @@ -1604,6 +1604,7 @@ func TestUpgradeErrorHandling(t *testing.T) { checkVersionedHomeCleanup: true, setupMocks: func(t *testing.T, mockAgentInfo *info.MockAgent, mockRollbackSrc *mockAvailableRollbacksSource, mockWatcherHelper *MockWatcherHelper) { mockAgentInfo.EXPECT().Version().Return("9.0.0") + mockRollbackSrc.EXPECT().Set(map[string]TTLMarker(nil)).Return(nil) }, }, "should add disk space error to the error chain if downloadArtifact fails with disk space error": {