diff --git a/sonar-project.properties b/sonar-project.properties index fe440d684..090505538 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -36,7 +36,14 @@ sonar.coverage.exclusions=**/tests/** # Ignore cpp:S5028 (macros should not define constants) in config.hpp/testconfig.hpp files only: # these headers define AOS_CONFIG_* build-time knobs with #ifndef/#define guards so they can be # overridden by the build system before this header is included, which constexpr/enum can't do. -sonar.issue.ignore.multicriteria=e1,e2,e3,e4,e5,e6,e7,e8 +# +# Ignore cpp:M23_163 (MISRA C++:2023 13.1.1, classes should not be inherited virtually) in the CM +# and SM database interfaces and the SM image manager storage interface only: DatabaseItf lists all +# storage interfaces the CM or SM database implements, +# and the application database implements the image manager storage in a common base class, so +# StorageItf is inherited virtually to keep a single instance of it in the diamond. StorageItf is +# a pure interface without data. +sonar.issue.ignore.multicriteria=e1,e2,e3,e4,e5,e6,e7,e8,e9,e10,e11,e12 sonar.issue.ignore.multicriteria.e1.ruleKey=cpp:M23_356 sonar.issue.ignore.multicriteria.e1.resourceKey=**/* sonar.issue.ignore.multicriteria.e2.ruleKey=cpp:S6022 @@ -53,5 +60,13 @@ sonar.issue.ignore.multicriteria.e7.ruleKey=cpp:S5028 sonar.issue.ignore.multicriteria.e7.resourceKey=**/config.hpp sonar.issue.ignore.multicriteria.e8.ruleKey=cpp:S5028 sonar.issue.ignore.multicriteria.e8.resourceKey=**/testconfig.hpp +sonar.issue.ignore.multicriteria.e9.ruleKey=cpp:S7032 +sonar.issue.ignore.multicriteria.e9.resourceKey=**/* +sonar.issue.ignore.multicriteria.e10.ruleKey=cpp:M23_163 +sonar.issue.ignore.multicriteria.e10.resourceKey=**/cm/database/itf/database.hpp +sonar.issue.ignore.multicriteria.e11.ruleKey=cpp:M23_163 +sonar.issue.ignore.multicriteria.e11.resourceKey=**/sm/database/itf/database.hpp +sonar.issue.ignore.multicriteria.e12.ruleKey=cpp:M23_163 +sonar.issue.ignore.multicriteria.e12.resourceKey=**/sm/imagemanager/itf/storage.hpp sonar.ws.timeout=600 diff --git a/src/core/cm/communication/communication.md b/src/core/cm/communication/communication.md index 572409e68..396ea90dc 100644 --- a/src/core/cm/communication/communication.md +++ b/src/core/cm/communication/communication.md @@ -14,7 +14,8 @@ It implements the following interfaces: notification; * [aos::cm::smcontroller::SenderItf](../smcontroller/itf/sender.hpp) - sends log messages; * [aos::cm::launcher::SenderItf](../launcher/itf/senderitf.hpp) - sends override env vars statuses; -* [aos::cm::imagemanager::BlobInfoProviderItf](../imagemanager/itf/blobinfoprovider.hpp) - provides blobs info. +* [aos::common::imagemanager::BlobInfoProviderItf](../../common/imagemanager/itf/blobinfoprovider.hpp) - provides + blobs info. It requires the following interfaces: @@ -69,7 +70,7 @@ classDiagram <> } - class BlobInfoProviderItf["aos::cm::imagemanager::BlobInfoProviderItf"] { + class BlobInfoProviderItf["aos::common::imagemanager::BlobInfoProviderItf"] { <> } diff --git a/src/core/cm/communication/itf/communication.hpp b/src/core/cm/communication/itf/communication.hpp index 16d07c2ea..30a52a402 100644 --- a/src/core/cm/communication/itf/communication.hpp +++ b/src/core/cm/communication/itf/communication.hpp @@ -8,7 +8,6 @@ #define AOS_CORE_CM_COMMUNICATION_ITF_COMMUNICATION_HPP_ #include -#include #include #include #include @@ -16,6 +15,7 @@ #include #include #include +#include namespace aos::cm::communication { @@ -28,7 +28,7 @@ namespace aos::cm::communication { */ class CommunicationItf : public alerts::SenderItf, public launcher::SenderItf, - public imagemanager::BlobInfoProviderItf, + public common::imagemanager::BlobInfoProviderItf, public monitoring::SenderItf, public rootcerts::SenderItf, public smcontroller::SenderItf, diff --git a/src/core/cm/database/database.md b/src/core/cm/database/database.md index b7b46164e..e21826a81 100644 --- a/src/core/cm/database/database.md +++ b/src/core/cm/database/database.md @@ -4,7 +4,8 @@ The database module provides persistent storage for various CM modules. It imple following modules: - [aos::cm::updatemanager::StorageItf](../updatemanager/itf/storage.hpp) - update manager storage interface; -- [aos::cm::imagemanager::StorageItf](../imagemanager/itf/storage.hpp) - image manager storage interface; +- [aos::common::imagemanager::StorageItf](../../common/imagemanager/itf/storage.hpp) - image manager storage interface, + inherited virtually so that the implementation may share it with a common image manager storage base class; - [aos::cm::launcher::StorageItf](../launcher/itf/storage.hpp) - launcher storage interface; - [aos::cm::storagestate::StorageItf](../storagestate/itf/storage.hpp) - storage state storage interface. @@ -18,7 +19,7 @@ classDiagram <> } - class ImageManagerStorageItf ["aos::cm::imagemanager::StorageItf"] { + class ImageManagerStorageItf ["aos::common::imagemanager::StorageItf"] { <> } diff --git a/src/core/cm/database/itf/database.hpp b/src/core/cm/database/itf/database.hpp index 405707f01..ffde8a409 100644 --- a/src/core/cm/database/itf/database.hpp +++ b/src/core/cm/database/itf/database.hpp @@ -7,10 +7,10 @@ #ifndef AOS_CORE_CM_DATABASE_ITF_DATABASE_HPP_ #define AOS_CORE_CM_DATABASE_ITF_DATABASE_HPP_ -#include #include #include #include +#include namespace aos::cm::database { @@ -22,7 +22,7 @@ namespace aos::cm::database { * Database interface. */ class DatabaseItf : public updatemanager::StorageItf, - public imagemanager::StorageItf, + public virtual common::imagemanager::StorageItf, public launcher::StorageItf, public storagestate::StorageItf { }; diff --git a/src/core/cm/imagemanager/CMakeLists.txt b/src/core/cm/imagemanager/CMakeLists.txt index 4156422fe..f1bc3350e 100644 --- a/src/core/cm/imagemanager/CMakeLists.txt +++ b/src/core/cm/imagemanager/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (C) 2025 EPAM Systems, Inc. +# Copyright (C) 2026 EPAM Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -20,22 +20,20 @@ set(SOURCES imagemanager.cpp) # Headers # ###################################################################################################################### -set(HEADERS - config.hpp - imagemanager.hpp - itf/blobinfoprovider.hpp - itf/imagemanager.hpp - itf/iteminfoprovider.hpp - itf/itemstatusprovider.hpp - itf/storage.hpp -) +set(HEADERS config.hpp imagemanager.hpp itf/imagemanager.hpp) # ###################################################################################################################### # Libraries # ###################################################################################################################### -set(LIBRARIES aos::core::cm::fileserver aos::core::common::crypto aos::core::common::downloader - aos::core::common::ocispec aos::core::common::spaceallocator aos::core::common::tools +set(LIBRARIES + aos::core::cm::fileserver + aos::core::common::crypto + aos::core::common::downloader + aos::core::common::imagemanager + aos::core::common::ocispec + aos::core::common::spaceallocator + aos::core::common::tools ) # ###################################################################################################################### diff --git a/src/core/cm/imagemanager/config.hpp b/src/core/cm/imagemanager/config.hpp index f6a7bb04b..d685dd69a 100644 --- a/src/core/cm/imagemanager/config.hpp +++ b/src/core/cm/imagemanager/config.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 EPAM Systems, Inc. + * Copyright (C) 2026 EPAM Systems, Inc. * * SPDX-License-Identifier: Apache-2.0 */ @@ -7,41 +7,15 @@ #ifndef AOS_CORE_CM_IMAGEMANAGER_CONFIG_HPP_ #define AOS_CORE_CM_IMAGEMANAGER_CONFIG_HPP_ -#include -#include +#include namespace aos::cm::imagemanager { /** - * Image manager configuration. + * CM image manager config. */ -struct Config { - StaticString mImagePath; - size_t mPartLimit {}; +struct Config : public common::imagemanager::Config { StaticString mDownloadPath; - Duration mUpdateItemTTL; - Duration mRemoveOutdatedPeriod; - - /** - * Compares config. - * - * @param other config to compare. - * @return bool. - */ - friend bool operator==(const Config& lhs, const Config& other) - { - return lhs.mImagePath == other.mImagePath && lhs.mPartLimit == other.mPartLimit - && lhs.mDownloadPath == other.mDownloadPath && lhs.mUpdateItemTTL == other.mUpdateItemTTL - && lhs.mRemoveOutdatedPeriod == other.mRemoveOutdatedPeriod; - }; - - /** - * Compares config. - * - * @param other config to compare. - * @return bool. - */ - friend bool operator!=(const Config& lhs, const Config& other) { return !(lhs == other); }; }; } // namespace aos::cm::imagemanager diff --git a/src/core/cm/imagemanager/imagemanager.cpp b/src/core/cm/imagemanager/imagemanager.cpp index bf9746a4c..beba6e406 100644 --- a/src/core/cm/imagemanager/imagemanager.cpp +++ b/src/core/cm/imagemanager/imagemanager.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 EPAM Systems, Inc. + * Copyright (C) 2026 EPAM Systems, Inc. * * SPDX-License-Identifier: Apache-2.0 */ @@ -14,1947 +14,522 @@ namespace aos::cm::imagemanager { * Public **********************************************************************************************************************/ -Error ImageManager::Init(AllocatorItf& allocator, const Config& config, StorageItf& storage, - BlobInfoProviderItf& blobInfoProvider, spaceallocator::SpaceAllocatorItf& downloadingSpaceAllocator, - spaceallocator::SpaceAllocatorItf& installSpaceAllocator, downloader::DownloaderItf& downloader, - fileserver::FileServerItf& fileserver, crypto::CryptoHelperItf& cryptoHelper, - fs::FileInfoProviderItf& fileInfoProvider, oci::OCISpecItf& ociSpec) +Error ImageManager::Init(const Config& config, common::imagemanager::BlobInfoProviderItf& cloudBlobInfoProvider, + common::imagemanager::StorageItf& storage, spaceallocator::SpaceAllocatorItf& spaceAllocator, + spaceallocator::SpaceAllocatorItf& downloadSpaceAllocator, downloader::DownloaderItf& downloader, + crypto::CryptoHelperItf& cryptoHelper, fileserver::FileServerItf& fileServer, + fs::FileInfoProviderItf& fileInfoProvider, oci::OCISpecItf& ociSpec, fs::FSPlatformItf& fsPlatform, + AllocatorItf& allocator) { LOG_DBG() << "Init image manager"; - mAllocator = &allocator; - mConfig = config; - mStorage = &storage; - mBlobInfoProvider = &blobInfoProvider; - mDownloadingSpaceAllocator = &downloadingSpaceAllocator; - mInstallSpaceAllocator = &installSpaceAllocator; - mDownloader = &downloader; - mFileServer = &fileserver; - mCryptoHelper = &cryptoHelper; - mFileInfoProvider = &fileInfoProvider; - mOCISpec = &ociSpec; - - auto items = MakeUnique>(mAllocator); - if (!items) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + if (auto err = common::imagemanager::ImageManager::Init( + config, cloudBlobInfoProvider, storage, spaceAllocator, fileInfoProvider, ociSpec, fsPlatform, allocator); + !err.IsNone()) { + return err; } - if (auto err = mStorage->GetAllItemsInfos(*items); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + mConfig = config; + mStorage = &storage; + mDownloadSpaceAllocator = &downloadSpaceAllocator; + mDownloader = &downloader; + mCryptoHelper = &cryptoHelper; + mFileServer = &fileServer; + mFileInfoProvider = &fileInfoProvider; + mAllocator = &allocator; - mBlobsInstallPath = fs::JoinPath(mConfig.mImagePath, cBlobsDirName); + return ErrorEnum::eNone; +} - if (auto err = fs::MakeDirAll(mBlobsInstallPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } +Error ImageManager::SetCerts( + const Array& certificates, const Array& certificateChains) +{ + LOG_DBG() << "Set certs" << Log::Field("numCerts", certificates.Size()) + << Log::Field("numCertChains", certificateChains.Size()); - mBlobsDownloadPath = fs::JoinPath(mConfig.mDownloadPath, cBlobsDirName); + LockGuard lock {mCertsMutex}; - if (auto err = fs::MakeDirAll(mBlobsDownloadPath); !err.IsNone()) { + if (auto err = mCertificates.Assign(certificates); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - RegisterOutdatedItems(*items); - - auto [cleanupSize, cleanupErr] = CleanupOrphanedBlobs(); - if (!cleanupErr.IsNone()) { - LOG_ERR() << "Failed to cleanup orphaned blobs" << Log::Field(cleanupErr); - } else { - LOG_DBG() << "Cleaned up orphaned blobs" << Log::Field("size", cleanupSize); + if (auto err = mCertificateChains.Assign(certificateChains); !err.IsNone()) { + return AOS_ERROR_WRAP(err); } return ErrorEnum::eNone; } -Error ImageManager::Start() +Error ImageManager::InstallItem(const UpdateItemInfo& itemInfo) { - LOG_DBG() << "Start image manager"; + LOG_DBG() << "Install item" << Log::Field("itemID", itemInfo.mItemID) << Log::Field("version", itemInfo.mVersion) + << Log::Field("indexDigest", itemInfo.mIndexDigest); - if (auto err = RemoveOutdatedItems(); !err.IsNone()) { - LOG_ERR() << "Failed to remove outdated items during start" << Log::Field(err); - } + InstallItemInfo installItemInfo; - if (auto err = mTimer.Start( - mConfig.mRemoveOutdatedPeriod, - [this](void*) { - if (auto err = RemoveOutdatedItems(); !err.IsNone()) { - LOG_ERR() << "Error removing outdated items" << Log::Field(err); - } - }, - false); - !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + installItemInfo.mItemID = itemInfo.mItemID; + installItemInfo.mType = itemInfo.mType; + installItemInfo.mVersion = itemInfo.mVersion; + installItemInfo.mRootDigest = itemInfo.mIndexDigest; + installItemInfo.mIsRootIndex = true; - return ErrorEnum::eNone; + return common::imagemanager::ImageManager::InstallItem(installItemInfo); } -Error ImageManager::Stop() +Error ImageManager::RemoveItem(const String& itemID, const String& version) { - LOG_DBG() << "Stop image manager"; + LOG_DBG() << "Remove item" << Log::Field("itemID", itemID) << Log::Field("version", version); - return mTimer.Stop(Timer::StopMode::WaitForCallbacks); + return common::imagemanager::ImageManager::RemoveItem(itemID, version); } -Error ImageManager::DownloadUpdateItems(const Array& itemsInfo, - const Array& certificates, const Array& certificateChains, - Array& statuses) +Error ImageManager::Cancel() { - LOG_INF() << "Download update items" << Log::Field("count", itemsInfo.Size()); - - for (const auto& itemInfo : itemsInfo) { - LOG_INF() << "Download update item" << Log::Field("itemID", itemInfo.mItemID) - << Log::Field("type", itemInfo.mType) << Log::Field("version", itemInfo.mVersion) - << Log::Field("indexDigest", itemInfo.mIndexDigest); - } + LOG_DBG() << "Cancel"; - if (!StartAction()) { - return ErrorEnum::eCanceled; - } - - auto stopAction = DeferRelease(&mMutex, [this](void*) { StopAction(); }); + return common::imagemanager::ImageManager::Cancel(); +} - if (auto err = statuses.Resize(itemsInfo.Size()); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } +Error ImageManager::GetBlobInfo(const String& digest, BlobInfo& blobInfo) +{ + LOG_DBG() << "Get blob info" << Log::Field("digest", digest); - for (size_t i = 0; i < itemsInfo.Size(); i++) { - statuses[i].mItemID = itemsInfo[i].mItemID; - statuses[i].mType = itemsInfo[i].mType; - statuses[i].mVersion = itemsInfo[i].mVersion; - statuses[i].mState = ItemStateEnum::eDownloading; - statuses[i].mError = ErrorEnum::eNone; - } + StaticString path; - auto storedItems = MakeUnique>(mAllocator); - if (!storedItems) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + if (auto err = GetBlobPath(digest, path); !err.IsNone()) { + return err; } - if (auto err = mStorage->GetAllItemsInfos(*storedItems); !err.IsNone()) { + auto [used, err] = mStorage->IsBlobUsed(digest); + if (!err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (itemsInfo.IsEmpty()) { - auto err = RemovePendingItems(*storedItems, statuses); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove pending items" << Log::Field(err); - } - - auto [cleanupSize, cleanupErr] = CleanupOrphanedBlobs(); - if (!cleanupErr.IsNone()) { - LOG_ERR() << "Failed to cleanup orphaned blobs" << Log::Field(cleanupErr); - - if (err.IsNone()) { - err = cleanupErr; - } - } else { - LOG_DBG() << "Cleaned up orphaned blobs" << Log::Field("size", cleanupSize); - } - - return err; + if (!used) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "blob not used")); } - if (auto err = CleanupDownloadingItems(itemsInfo, *storedItems); !err.IsNone()) { - LOG_ERR() << "Failed to cleanup downloading items" << Log::Field(err); + auto [size, sizeErr] = fs::CalculateSize(*mAllocator, path); + if (!sizeErr.IsNone()) { + return AOS_ERROR_WRAP(sizeErr); } - storedItems->Clear(); + StaticString url; - if (auto err = mStorage->GetAllItemsInfos(*storedItems); !err.IsNone()) { + if (err = mFileServer->TranslateFilePathURL(path, url); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (auto err = VerifyStoredItems(itemsInfo, *storedItems, certificates, certificateChains, statuses); - !err.IsNone()) { - LOG_ERR() << "Failed to verify stored items" << Log::Field(err); - - return err; - } + blobInfo.mDigest = digest; + blobInfo.mSize = size; - if (auto err = ProcessDownloadRequest(itemsInfo, *storedItems, certificates, certificateChains, statuses); - !err.IsNone()) { - LOG_ERR() << "Failed to process download request" << Log::Field(err); + blobInfo.mSHA256.Clear(); + blobInfo.mURLs.Clear(); - return err; + if (err = blobInfo.mURLs.PushBack(url); !err.IsNone()) { + return AOS_ERROR_WRAP(err); } - auto [cleanupSize, cleanupErr] = CleanupOrphanedBlobs(); - if (!cleanupErr.IsNone()) { - LOG_ERR() << "Failed to cleanup orphaned blobs" << Log::Field(cleanupErr); - } else { - LOG_DBG() << "Cleaned up orphaned blobs" << Log::Field("size", cleanupSize); - } + blobInfo.mDecryptInfo.Reset(); + blobInfo.mSignInfo.Reset(); return ErrorEnum::eNone; } -Error ImageManager::InstallUpdateItems(const Array& itemsInfo, Array& statuses) -{ - LOG_INF() << "Install update items" << Log::Field("count", itemsInfo.Size()); - - for (const auto& itemInfo : itemsInfo) { - LOG_INF() << "Install update item" << Log::Field("itemID", itemInfo.mItemID) - << Log::Field("type", itemInfo.mType) << Log::Field("version", itemInfo.mVersion); - } - - if (!StartAction()) { - return ErrorEnum::eCanceled; - } +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ - auto stopAction = DeferRelease(&mMutex, [this](void*) { StopAction(); }); +Error ImageManager::DownloadBlob(CancelContext& ctx, const String& path, const BlobInfo& blobInfo) +{ + LOG_DBG() << "Download blob" << Log::Field("digest", blobInfo.mDigest) << Log::Field("path", path) + << Log::Field("size", blobInfo.mSize); - if (auto err = statuses.Resize(itemsInfo.Size()); !err.IsNone()) { + if (auto err = ctx.Err(); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - for (size_t i = 0; i < itemsInfo.Size(); i++) { - statuses[i].mItemID = itemsInfo[i].mItemID; - statuses[i].mType = itemsInfo[i].mType; - statuses[i].mVersion = itemsInfo[i].mVersion; - statuses[i].mState = ItemStateEnum::eInstalled; - statuses[i].mError = ErrorEnum::eNone; - } - - auto storedItems = MakeUnique>(mAllocator); - if (!storedItems) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = mStorage->GetAllItemsInfos(*storedItems); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + StaticString downloadPath; - if (auto err = RemoveDifferentVersions(itemsInfo, *storedItems); !err.IsNone()) { - LOG_ERR() << "Failed to remove different versions" << Log::Field(err); + if (auto err = GetDownloadPath(blobInfo.mDigest, downloadPath); !err.IsNone()) { + return err; } - storedItems->Clear(); + StartDownload(blobInfo.mDigest); - if (auto err = mStorage->GetAllItemsInfos(*storedItems); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + auto finishDownload + = DeferRelease([this, &blobInfo, &downloadPath]() { FinishDownload(blobInfo.mDigest, downloadPath); }); - if (auto err = VerifyBlobsIntegrity(itemsInfo, *storedItems, statuses); !err.IsNone()) { - LOG_ERR() << "Failed to verify blobs integrity" << Log::Field(err); + if (auto err = DownloadFile(ctx, downloadPath, blobInfo); !err.IsNone()) { + return err; } - storedItems->Clear(); - - if (auto err = mStorage->GetAllItemsInfos(*storedItems); !err.IsNone()) { + if (auto err = ctx.Err(); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (auto err = SetItemsToInstalled(itemsInfo, *storedItems); !err.IsNone()) { - LOG_ERR() << "Failed to set items to installed" << Log::Field(err); - } + return ProcessDownloadFile(downloadPath, path, blobInfo); +} - storedItems->Clear(); +RetWithError ImageManager::RemoveDownloadedBlob(const String& digest) +{ + LOG_DBG() << "Remove downloaded blob" << Log::Field("digest", digest); - if (auto err = mStorage->GetAllItemsInfos(*storedItems); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + StaticString downloadPath; + + if (auto err = GetDownloadPath(digest, downloadPath); !err.IsNone()) { + return {0, err}; } - if (auto err = SetItemsToRemoved(itemsInfo, *storedItems); !err.IsNone()) { - LOG_ERR() << "Failed to set items to removed" << Log::Field(err); + auto [size, err] = GetFileSize(downloadPath); + if (!err.IsNone()) { + return {0, err}; } - auto [cleanupSize, cleanupErr] = CleanupOrphanedBlobs(); - if (!cleanupErr.IsNone()) { - LOG_ERR() << "Failed to cleanup orphaned blobs" << Log::Field(cleanupErr); - } else { - LOG_DBG() << "Cleaned up orphaned blobs" << Log::Field("size", cleanupSize); + if (err = fs::RemoveAll(downloadPath); !err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; } - return ErrorEnum::eNone; + return {size, ErrorEnum::eNone}; } -Error ImageManager::Cancel() +void ImageManager::StartDownload(const String& digest) { - LockGuard lock {mMutex}; + LockGuard spaceLock {mDownloadSpaceMutex}; - LOG_DBG() << "Cancel image manager downloading"; + const bool firstDownload = mNumDownloads++ == 0; - mCancel = true; - if (auto err = mCondVar.NotifyAll(); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (auto err = mDownloadSpaceAllocator->RestoreOutdatedItem(digest, ""); + !err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { + LOG_ERR() << "Failed to restore download file" << Log::Field("digest", digest) << Log::Field(err); } - if (!mCurrentDownloadDigest.IsEmpty()) { - if (auto err = mDownloader->Cancel(mCurrentDownloadDigest); !err.IsNone()) { - LOG_ERR() << "Failed to cancel downloader" << Log::Field("digest", mCurrentDownloadDigest) - << Log::Field(err); - - return AOS_ERROR_WRAP(err); - } + if (firstDownload) { + RegisterDownloadFiles(digest); } - - return ErrorEnum::eNone; } -Error ImageManager::GetUpdateItemsStatuses(Array& statuses) +void ImageManager::FinishDownload(const String& digest, const String& downloadPath) { - LockGuard lock {mMutex}; + LockGuard spaceLock {mDownloadSpaceMutex}; - LOG_DBG() << "Get update items statuses"; + mNumDownloads--; - auto items = MakeUnique>(mAllocator); - if (!items) { - return ErrorEnum::eNoMemory; - } + auto [exists, err] = fs::FileExist(downloadPath); + if (!err.IsNone()) { + LOG_ERR() << "Failed to check download file" << Log::Field("path", downloadPath) << Log::Field(err); - if (auto err = mStorage->GetAllItemsInfos(*items); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + return; } - for (const auto& item : *items) { - UpdateItemStatus status; - - status.mItemID = item.mItemID; - status.mType = item.mType; - status.mVersion = item.mVersion; - status.mState = item.mState; - status.mError = ErrorEnum::eNone; - - if (auto err = statuses.PushBack(status); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + if (!exists) { + return; } - return ErrorEnum::eNone; -} - -Error ImageManager::SubscribeListener(ItemStatusListenerItf& listener) -{ - LockGuard lock {mMutex}; - - LOG_DBG() << "Subscribe item status listener"; + const auto now = Time::Now(); - if (mListeners.Find(&listener) != mListeners.end()) { - return ErrorEnum::eAlreadyExist; + if (err = fs::SetFileMTime(downloadPath, now); !err.IsNone()) { + LOG_ERR() << "Failed to set download file modification time" << Log::Field("path", downloadPath) + << Log::Field(err); } - if (auto err = mListeners.PushBack(&listener); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (err = mDownloadSpaceAllocator->AddOutdatedItem(digest, "", now); !err.IsNone()) { + LOG_ERR() << "Failed to add outdated download file" << Log::Field("digest", digest) << Log::Field(err); } - - return ErrorEnum::eNone; -} - -Error ImageManager::UnsubscribeListener(ItemStatusListenerItf& listener) -{ - LockGuard lock {mMutex}; - - LOG_DBG() << "Unsubscribed item status listener"; - - return mListeners.Remove(&listener) != 0 ? ErrorEnum::eNone : ErrorEnum::eNotFound; } -Error ImageManager::GetIndexDigest(const String& itemID, const String& version, String& digest) const +void ImageManager::RegisterDownloadFiles(const String& currentDigest) { - LockGuard lock {mMutex}; - - LOG_DBG() << "Get index digest" << Log::Field("itemID", itemID) << Log::Field("version", version); + LOG_DBG() << "Register download files"; - auto items = MakeUnique>(mAllocator); - if (!items) { - return ErrorEnum::eNoMemory; - } + for (auto algIt = fs::DirIterator(mConfig.mDownloadPath); algIt.Next();) { + const auto algPath = fs::JoinPath(mConfig.mDownloadPath, algIt->mPath); - if (auto err = mStorage->GetItemInfos(itemID, *items); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + if (!algIt->mIsDir) { + RemoveInvalidDownloadFile(algPath); - auto it = items->FindIf( - [&](const auto& item) { return item.mVersion == version && item.mState != ItemStateEnum::eDownloading; }); + continue; + } - if (it == items->end()) { - return ErrorEnum::eNotFound; - } + for (auto fileIt = fs::DirIterator(algPath); fileIt.Next();) { + const auto filePath = fs::JoinPath(algPath, fileIt->mPath); + StaticString digest; + StaticString algorithm; + StaticString hash; - if (auto err = digest.Assign(it->mIndexDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + if (algIt->mPath.Size() + 1 + fileIt->mPath.Size() <= digest.MaxSize()) { + digest.Append(algIt->mPath).Append(":").Append(fileIt->mPath); + } - return ErrorEnum::eNone; -} + if (fileIt->mIsDir || !ParseDigest(digest, algorithm, hash).IsNone()) { + RemoveInvalidDownloadFile(filePath); -Error ImageManager::GetBlobPath(const String& digest, String& path) const -{ - LockGuard lock {mMutex}; + continue; + } - LOG_DBG() << "Get blob path" << Log::Field("digest", digest); + if (digest == currentDigest) { + continue; + } - StaticString blobPath; - if (auto err = GetBlobFilePath(mBlobsInstallPath, digest, blobPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + auto [modifyTime, err] = fs::GetFileMTime(filePath); + if (!err.IsNone()) { + LOG_ERR() << "Failed to get download file modification time" << Log::Field("path", filePath) + << Log::Field(err); - if (auto err = path.Assign(blobPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + continue; + } - auto [exists, err] = fs::FileExist(path); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (err = mDownloadSpaceAllocator->AddOutdatedItem(digest, "", modifyTime); !err.IsNone()) { + LOG_ERR() << "Failed to add outdated download file" << Log::Field("digest", digest) << Log::Field(err); + } + } } - - return exists ? ErrorEnum::eNone : ErrorEnum::eNotFound; } -Error ImageManager::GetBlobURL(const String& digest, String& url) const +void ImageManager::RemoveInvalidDownloadFile(const String& path) { - LockGuard lock {mMutex}; + LOG_WRN() << "Remove invalid download file" << Log::Field("path", path); - LOG_DBG() << "Get blob URL" << Log::Field("digest", digest); + auto [size, err] = fs::CalculateSize(*mAllocator, path); + if (!err.IsNone()) { + LOG_ERR() << "Failed to get invalid download file size" << Log::Field("path", path) << Log::Field(err); - StaticString blobPath; - if (auto err = GetBlobFilePath(mBlobsInstallPath, digest, blobPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + return; } - auto [exists, existErr] = fs::FileExist(blobPath); - if (!existErr.IsNone()) { - return AOS_ERROR_WRAP(existErr); - } + if (err = fs::RemoveAll(path); !err.IsNone()) { + LOG_ERR() << "Failed to remove invalid download file" << Log::Field("path", path) << Log::Field(err); - if (!exists) { - return ErrorEnum::eNotFound; + return; } - return mFileServer->TranslateFilePathURL(blobPath, url); + mDownloadSpaceAllocator->FreeSpace(size); } -Error ImageManager::GetItemCurrentVersion(const String& itemID, String& version) const +Error ImageManager::GetDownloadPath(const String& digest, String& downloadPath) const { - LockGuard lock {mMutex}; - - LOG_DBG() << "Get item current version" << Log::Field("itemID", itemID); - - auto items = MakeUnique>(mAllocator); - if (!items) { - return ErrorEnum::eNoMemory; - } - - if (auto err = mStorage->GetItemInfos(itemID, *items); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + StaticString algorithm; + StaticString hash; - auto it = items->FindIf([&](const auto& item) { return item.mState == ItemStateEnum::ePending; }); - if (it == items->end()) { - it = items->FindIf([&](const auto& item) { return item.mState == ItemStateEnum::eInstalled; }); + if (auto err = ParseDigest(digest, algorithm, hash); !err.IsNone()) { + return err; } - if (it == items->end()) { - return ErrorEnum::eNotFound; + if (mConfig.mDownloadPath.Size() + 1 + digest.Size() > cFilePathLen) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "download path too long")); } - if (auto err = version.Assign(it->mVersion); !err.IsNone()) { + if (auto err = downloadPath.Assign(fs::JoinPath(mConfig.mDownloadPath, algorithm, hash)); !err.IsNone()) { return AOS_ERROR_WRAP(err); } return ErrorEnum::eNone; } -RetWithError ImageManager::RemoveItem(const String& id, const String& version) +RetWithError ImageManager::GetFileSize(const String& path) const { - LockGuard lock {mMutex}; - - LOG_DBG() << "Remove item" << Log::Field("id", id) << Log::Field("version", version); - - auto storedItems = MakeUnique>(mAllocator); - if (!storedItems) { - return {0, AOS_ERROR_WRAP(ErrorEnum::eNoMemory)}; - } - - if (auto err = mStorage->GetItemInfos(id, *storedItems); !err.IsNone()) { + auto [exists, err] = fs::FileExist(path); + if (!err.IsNone()) { return {0, AOS_ERROR_WRAP(err)}; } - auto itemIt = storedItems->FindIf([](const auto& item) { return item.mState == ItemStateEnum::eRemoved; }); + if (!exists) { + return {0, ErrorEnum::eNone}; + } - if (itemIt == storedItems->end()) { - return {0, ErrorEnum::eNotFound}; + auto [size, sizeErr] = fs::CalculateSize(*mAllocator, path); + if (!sizeErr.IsNone()) { + return {0, AOS_ERROR_WRAP(sizeErr)}; } - const auto& itemToRemove = *itemIt; + return {size, ErrorEnum::eNone}; +} - if (auto err = mStorage->RemoveItem(itemToRemove.mItemID, itemToRemove.mVersion); !err.IsNone()) { +RetWithError ImageManager::PrepareDownloadFile(const String& downloadPath, size_t size) +{ + if (auto err = fs::MakeDirAll(fs::Dir(downloadPath)); !err.IsNone()) { return {0, AOS_ERROR_WRAP(err)}; } - if (auto err = mInstallSpaceAllocator->RestoreOutdatedItem(itemToRemove.mItemID, itemToRemove.mVersion); - !err.IsNone()) { - LOG_ERR() << "Failed to restore outdated item" << Log::Field("itemID", itemToRemove.mItemID) << Log::Field(err); + auto [existingSize, err] = GetFileSize(downloadPath); + if (!err.IsNone()) { + return {0, err}; } - auto [totalSize, cleanupErr] = CleanupOrphanedBlobs(); - if (!cleanupErr.IsNone()) { - return {0, AOS_ERROR_WRAP(cleanupErr)}; + if (existingSize <= size) { + return {existingSize, ErrorEnum::eNone}; } - LOG_DBG() << "Item removed successfully" << Log::Field("id", id) << Log::Field("totalSize", totalSize); + LOG_WRN() << "Remove oversized download file" << Log::Field("path", downloadPath) + << Log::Field("size", existingSize); - for (auto* listener : mListeners) { - listener->OnItemRemoved(id); + if (err = fs::RemoveAll(downloadPath); !err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; } - return {totalSize, ErrorEnum::eNone}; -} + mDownloadSpaceAllocator->FreeSpace(existingSize); -/*********************************************************************************************************************** - * Private - **********************************************************************************************************************/ + return {0, ErrorEnum::eNone}; +} -Error ImageManager::RemoveOutdatedItems() +Error ImageManager::DownloadFile(CancelContext& ctx, const String& downloadPath, const BlobInfo& blobInfo) { - LockGuard lock {mMutex}; + auto [existingSize, err] = PrepareDownloadFile(downloadPath, blobInfo.mSize); + if (!err.IsNone()) { + return err; + } - LOG_DBG() << "Remove outdated items"; + if (existingSize == blobInfo.mSize) { + LOG_DBG() << "Download file is complete" << Log::Field("digest", blobInfo.mDigest) + << Log::Field("path", downloadPath); - auto items = MakeUnique>(mAllocator); - if (!items) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + return ErrorEnum::eNone; } - if (auto err = mStorage->GetAllItemsInfos(*items); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + auto [space, allocateErr] = mDownloadSpaceAllocator->AllocateSpace(blobInfo.mSize - existingSize); + if (!allocateErr.IsNone()) { + return AOS_ERROR_WRAP(allocateErr); } - bool hasRemovedItems = false; - - for (const auto& item : *items) { - if (item.mState == ItemStateEnum::eRemoved && item.mTimestamp.Add(mConfig.mUpdateItemTTL) < Time::Now()) { - LOG_DBG() << "Removing outdated item" << Log::Field("itemID", item.mItemID) - << Log::Field("version", item.mVersion); - - if (auto err = mStorage->RemoveItem(item.mItemID, item.mVersion); !err.IsNone()) { - LOG_ERR() << "Failed to remove outdated item" << Log::Field("itemID", item.mItemID) - << Log::Field("version", item.mVersion) << Log::Field(err); - - continue; - } - - if (auto err = mInstallSpaceAllocator->RestoreOutdatedItem(item.mItemID, item.mVersion); !err.IsNone()) { - LOG_ERR() << "Failed to restore outdated item" << Log::Field("itemID", item.mItemID) << Log::Field(err); - } + LOG_DBG() << "Download file" << Log::Field("digest", blobInfo.mDigest) << Log::Field("path", downloadPath) + << Log::Field("existingSize", existingSize); - for (auto* listener : mListeners) { - listener->OnItemRemoved(item.mItemID); - } + if (err = mDownloader->Download(ctx, blobInfo.mDigest, blobInfo.mURLs, downloadPath); !err.IsNone()) { + AcceptDownloadedSpace(*space, downloadPath, existingSize); - hasRemovedItems = true; - } + return AOS_ERROR_WRAP(err); } - if (hasRemovedItems) { - auto [totalSize, err] = CleanupOrphanedBlobs(); - if (!err.IsNone()) { - return err; - } - - LOG_DBG() << "Cleaned up orphaned blobs" << Log::Field("size", totalSize); - - mInstallSpaceAllocator->FreeSpace(totalSize); + if (err = space->Accept(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); } return ErrorEnum::eNone; } -Error ImageManager::RemoveOldItemVersions(const String& itemID, Array& storedItems) +void ImageManager::AcceptDownloadedSpace( + spaceallocator::SpaceItf& space, const String& downloadPath, size_t existingSize) { - size_t numVersions = 0; - - for (const auto& item : storedItems) { - if (item.mItemID == itemID) { - numVersions++; - } + auto [size, sizeErr] = GetFileSize(downloadPath); + if (!sizeErr.IsNone()) { + LOG_ERR() << "Failed to get download file size" << Log::Field("path", downloadPath) << Log::Field(sizeErr); + } else if (auto err = space.Resize(size > existingSize ? size - existingSize : 0); !err.IsNone()) { + LOG_ERR() << "Failed to resize space" << Log::Field("path", downloadPath) << Log::Field(err); } - bool hasRemovedItems = false; - - while (numVersions >= cMaxNumItemVersions) { - auto oldestIt = storedItems.end(); - - for (auto it = storedItems.begin(); it != storedItems.end(); ++it) { - if (it->mItemID != itemID || it->mState != ItemStateEnum::eRemoved) { - continue; - } - - if (oldestIt == storedItems.end() || it->mTimestamp < oldestIt->mTimestamp) { - oldestIt = it; - } - } - - if (oldestIt == storedItems.end()) { - break; - } - - LOG_DBG() << "Remove old item version" << Log::Field("itemID", itemID) - << Log::Field("version", oldestIt->mVersion); - - if (auto err = mStorage->RemoveItem(itemID, oldestIt->mVersion); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = mInstallSpaceAllocator->RestoreOutdatedItem(itemID, oldestIt->mVersion); !err.IsNone()) { - LOG_ERR() << "Failed to restore outdated item" << Log::Field("itemID", itemID) - << Log::Field("version", oldestIt->mVersion) << Log::Field(err); - } - - for (auto* listener : mListeners) { - listener->OnItemRemoved(itemID); - } - - (void)storedItems.Erase(oldestIt); + if (auto err = space.Accept(); !err.IsNone()) { + LOG_ERR() << "Failed to accept space" << Log::Field("path", downloadPath) << Log::Field(err); + } - numVersions--; - hasRemovedItems = true; + if (sizeErr.IsNone() && size < existingSize) { + mDownloadSpaceAllocator->FreeSpace(existingSize - size); } +} - if (hasRemovedItems) { - auto [totalSize, err] = CleanupOrphanedBlobs(); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } +void ImageManager::RemoveDownloadFile(const String& downloadPath, size_t size) +{ + LOG_DBG() << "Remove download file" << Log::Field("path", downloadPath); - LOG_DBG() << "Cleaned up orphaned blobs" << Log::Field("size", totalSize); + if (auto err = fs::RemoveAll(downloadPath); !err.IsNone()) { + LOG_ERR() << "Failed to remove download file" << Log::Field("path", downloadPath) << Log::Field(err); - mInstallSpaceAllocator->FreeSpace(totalSize); + return; } - return ErrorEnum::eNone; + mDownloadSpaceAllocator->FreeSpace(size); } -Error ImageManager::WaitForStop() +Error ImageManager::ProcessDownloadFile(const String& downloadPath, const String& path, const BlobInfo& blobInfo) { - UniqueLock lock(mMutex); // NOSONAR cpp:S5486 - false positive; lock released before next WaitForStop() + if (auto err = ValidateDownloadFile(downloadPath, blobInfo); !err.IsNone()) { + return err; + } - if (auto err = mCondVar.Wait(lock, cRetryTimeout, [this]() { return mCancel; }); - !err.IsNone() && err != ErrorEnum::eTimeout) { - return AOS_ERROR_WRAP(err); + if (auto err = DecryptBlob(downloadPath, path, blobInfo); !err.IsNone()) { + return err; } - if (mCancel) { - return ErrorEnum::eCanceled; + if (auto err = ValidateBlobSigns(downloadPath, path, blobInfo); !err.IsNone()) { + return err; } return ErrorEnum::eNone; } -Error ImageManager::RemovePendingItems(const Array& storedItems, Array& statuses) +Error ImageManager::ValidateDownloadFile(const String& downloadPath, const BlobInfo& blobInfo) { - LOG_DBG() << "Remove pending items"; + LOG_DBG() << "Validate download file" << Log::Field("digest", blobInfo.mDigest) << Log::Field("path", downloadPath); - for (const auto& storedItem : storedItems) { - if (storedItem.mState != ItemStateEnum::ePending) { - continue; - } + auto [size, err] = GetFileSize(downloadPath); + if (!err.IsNone()) { + return err; + } - LOG_DBG() << "Removing pending item" << Log::Field("itemID", storedItem.mItemID) - << Log::Field("version", storedItem.mVersion); + if (size != blobInfo.mSize) { + RemoveDownloadFile(downloadPath, blobInfo.mSize); - if (auto err = mStorage->RemoveItem(storedItem.mItemID, storedItem.mVersion); !err.IsNone()) { - LOG_ERR() << "Failed to remove pending item from storage" << Log::Field("itemID", storedItem.mItemID) - << Log::Field("version", storedItem.mVersion) << Log::Field(err); + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidChecksum, "size mismatch")); + } - continue; - } + if (blobInfo.mSHA256.IsEmpty()) { + return ErrorEnum::eNone; + } - UpdateItemStatus status; + fs::FileInfo info; - status.mItemID = storedItem.mItemID; - status.mType = storedItem.mType; - status.mVersion = storedItem.mVersion; - status.mState = ItemStateEnum::eRemoved; - status.mError = ErrorEnum::eNone; + if (err = mFileInfoProvider->GetFileInfo(downloadPath, info, crypto::HashEnum::eSHA3_256); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } - if (auto err = statuses.PushBack(status); !err.IsNone()) { - LOG_ERR() << "Failed to add status to statuses array" << Log::Field(err); - } + if (info.mCheckSum != blobInfo.mSHA256) { + RemoveDownloadFile(downloadPath, blobInfo.mSize); - NotifyItemStatusChanged( - storedItem.mItemID, storedItem.mType, storedItem.mVersion, ItemStateEnum::eRemoved, ErrorEnum::eNone); + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidChecksum, "checksum mismatch")); } return ErrorEnum::eNone; } -Error ImageManager::CleanupDownloadingItems( - const Array& currentItems, const Array& storedItems) +Error ImageManager::DecryptBlob(const String& downloadPath, const String& path, const BlobInfo& blobInfo) { - LOG_DBG() << "Cleanup downloading items"; + if (!blobInfo.mDecryptInfo.HasValue()) { + LOG_DBG() << "Copy blob" << Log::Field("digest", blobInfo.mDigest) << Log::Field("path", path); - for (const auto& storedItem : storedItems) { - if (storedItem.mState != ItemStateEnum::eDownloading && storedItem.mState != ItemStateEnum::ePending) { - continue; + if (auto err = fs::CopyFile(*mAllocator, downloadPath, path); !err.IsNone()) { + return AOS_ERROR_WRAP(err); } - auto isInCurrentRequest = currentItems.FindIf([&storedItem](const auto& currentItem) { - return storedItem.mItemID == currentItem.mItemID && storedItem.mVersion == currentItem.mVersion; - }) != currentItems.end(); - - if (!isInCurrentRequest) { - LOG_DBG() << "Removing stale item" << Log::Field("itemID", storedItem.mItemID) - << Log::Field("version", storedItem.mVersion) - << Log::Field("state", ItemState(storedItem.mState)); - - if (auto err = mStorage->RemoveItem(storedItem.mItemID, storedItem.mVersion); !err.IsNone()) { - LOG_ERR() << "Failed to remove item from storage" << Log::Field("itemID", storedItem.mItemID) - << Log::Field("version", storedItem.mVersion) << Log::Field(err); - } - - if (!storedItem.mIndexDigest.IsEmpty()) { - StaticString filePath; - if (auto err = GetBlobFilePath(mBlobsInstallPath, storedItem.mIndexDigest, filePath); !err.IsNone()) { - LOG_ERR() << "Failed to get blob file path" << Log::Field(err); - } else { - LOG_DBG() << "Remove blob" << Log::Field("path", filePath); - - err = fs::RemoveAll(filePath); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove blob" << Log::Field("path", filePath) << Log::Field(err); - } - } - } - } + return ErrorEnum::eNone; } - return ErrorEnum::eNone; -} - -Error ImageManager::VerifyStoredItems(const Array& itemsInfo, Array& storedItems, - const Array& certificates, const Array& certificateChains, - Array& statuses) -{ - LOG_DBG() << "Verify stored items"; - - for (auto& storedItem : storedItems) { - if (storedItem.mState == ItemStateEnum::eInstalled || storedItem.mState == ItemStateEnum::ePending) { - auto itemIt = itemsInfo.FindIf([&storedItem](const auto& item) { - return item.mItemID == storedItem.mItemID && item.mVersion == storedItem.mVersion; - }); - - UpdateItemInfo itemInfo; - itemInfo.mItemID = storedItem.mItemID; - itemInfo.mVersion = storedItem.mVersion; - itemInfo.mIndexDigest = storedItem.mIndexDigest; + LOG_DBG() << "Decrypt blob" << Log::Field("digest", blobInfo.mDigest) << Log::Field("path", path); - LOG_DBG() << "Verify stored item" << Log::Field("id", storedItem.mItemID) - << Log::Field("state", ItemState(storedItem.mState)); + if (auto err = mCryptoHelper->Decrypt(downloadPath, path, blobInfo.mDecryptInfo.GetValue()); !err.IsNone()) { + RemoveDownloadFile(downloadPath, blobInfo.mSize); - if (auto err = DownloadItem(itemInfo, certificates, certificateChains); !err.IsNone()) { - LOG_ERR() << "Failed to verify/download item" << Log::Field("id", storedItem.mItemID) - << Log::Field(err); - - if (itemIt != itemsInfo.end()) { - auto statusIdx = itemIt - itemsInfo.begin(); - statuses[statusIdx].mState = ItemStateEnum::eFailed; - statuses[statusIdx].mError = err; - } - - if (auto updateErr - = mStorage->UpdateItemState(storedItem.mItemID, storedItem.mVersion, ItemStateEnum::eFailed); - !updateErr.IsNone()) { - LOG_ERR() << "Failed to update item state" << Log::Field(updateErr); - } else { - storedItem.mState = ItemStateEnum::eFailed; - } - - NotifyItemStatusChanged( - storedItem.mItemID, storedItem.mType, storedItem.mVersion, ItemStateEnum::eFailed, err); - - if (err == ErrorEnum::eCanceled) { - return err; - } - } else { - if (itemIt != itemsInfo.end()) { - auto statusIdx = itemIt - itemsInfo.begin(); - statuses[statusIdx].mState = storedItem.mState; - statuses[statusIdx].mError = ErrorEnum::eNone; - } - } - } + return AOS_ERROR_WRAP(err); } return ErrorEnum::eNone; } -Error ImageManager::ProcessDownloadRequest(const Array& itemsInfo, Array& storedItems, - const Array& certificates, const Array& certificateChains, - Array& statuses) +Error ImageManager::ValidateBlobSigns(const String& downloadPath, const String& path, const BlobInfo& blobInfo) { - LOG_DBG() << "Process download request"; + if (!blobInfo.mSignInfo.HasValue()) { + return ErrorEnum::eNone; + } - for (size_t i = 0; i < itemsInfo.Size(); i++) { - const auto& itemInfo = itemsInfo[i]; + LOG_DBG() << "Validate blob signs" << Log::Field("digest", blobInfo.mDigest) << Log::Field("path", path); - LOG_DBG() << "Process item" << Log::Field("id", itemInfo.mItemID) << Log::Field("version", itemInfo.mVersion); + LockGuard lock {mCertsMutex}; - if (auto installedOrPendingIt = storedItems.FindIf([&itemInfo](const auto& stored) { - return stored.mItemID == itemInfo.mItemID && stored.mVersion == itemInfo.mVersion - && (stored.mState == ItemStateEnum::eInstalled || stored.mState == ItemStateEnum::ePending); - }); - installedOrPendingIt != storedItems.end()) { - LOG_DBG() << "Item already processed in first loop, skipping"; + if (auto err = mCryptoHelper->ValidateSigns(path, blobInfo.mSignInfo.GetValue(), mCertificateChains, mCertificates); + !err.IsNone()) { + RemoveDownloadFile(downloadPath, blobInfo.mSize); - continue; - } - - if (auto oldVersionIt = storedItems.FindIf([&itemInfo](const auto& stored) { - return stored.mItemID == itemInfo.mItemID && stored.mVersion != itemInfo.mVersion - && (stored.mState == ItemStateEnum::ePending || stored.mState == ItemStateEnum::eFailed - || stored.mState == ItemStateEnum::eDownloading); - }); - oldVersionIt != storedItems.end()) { - LOG_DBG() << "Removing old version" << Log::Field("id", oldVersionIt->mItemID) - << Log::Field("version", oldVersionIt->mVersion); - - if (auto removeErr = mStorage->RemoveItem(oldVersionIt->mItemID, oldVersionIt->mVersion); - !removeErr.IsNone()) { - LOG_ERR() << "Failed to remove old version" << Log::Field(removeErr); - } else { - (void)storedItems.Erase(oldVersionIt); - } - } - - if (auto sameVersionIt = storedItems.FindIf([&itemInfo](const auto& stored) { - return stored.mItemID == itemInfo.mItemID && stored.mVersion == itemInfo.mVersion - && (stored.mState == ItemStateEnum::eDownloading || stored.mState == ItemStateEnum::eFailed - || stored.mState == ItemStateEnum::eRemoved); - }); - sameVersionIt == storedItems.end()) { - if (auto err = RemoveOldItemVersions(itemInfo.mItemID, storedItems); !err.IsNone()) { - LOG_ERR() << "Failed to remove old item versions" << Log::Field("id", itemInfo.mItemID) - << Log::Field(err); - } - - ItemInfo newItem; - newItem.mItemID = itemInfo.mItemID; - newItem.mType = itemInfo.mType; - newItem.mVersion = itemInfo.mVersion; - newItem.mIndexDigest = itemInfo.mIndexDigest; - newItem.mState = ItemStateEnum::eDownloading; - newItem.mTimestamp = Time::Now(); - - if (auto addErr = mStorage->AddItem(newItem); !addErr.IsNone()) { - LOG_ERR() << "Failed to add new item" << Log::Field(addErr); - - continue; - } - } else if (sameVersionIt->mState == ItemStateEnum::eRemoved) { - if (auto updateErr - = mStorage->UpdateItemState(itemInfo.mItemID, itemInfo.mVersion, ItemStateEnum::eDownloading); - !updateErr.IsNone()) { - LOG_ERR() << "Failed to update removed item state" << Log::Field(updateErr); - - continue; - } - - if (auto err = mInstallSpaceAllocator->RestoreOutdatedItem(itemInfo.mItemID, itemInfo.mVersion); - !err.IsNone()) { - LOG_ERR() << "Failed to restore outdated item" << Log::Field("itemID", itemInfo.mItemID) - << Log::Field("version", itemInfo.mVersion) << Log::Field(err); - } - } else { - // item already in a state that requires no action - } - - NotifyItemStatusChanged( - itemInfo.mItemID, itemInfo.mType, itemInfo.mVersion, ItemStateEnum::eDownloading, ErrorEnum::eNone); - - auto downloadErr = DownloadItem(itemInfo, certificates, certificateChains); - - auto finalState = downloadErr.IsNone() ? ItemStateEnum::ePending : ItemStateEnum::eFailed; - if (auto updateErr = mStorage->UpdateItemState(itemInfo.mItemID, itemInfo.mVersion, finalState); - !updateErr.IsNone()) { - LOG_ERR() << "Failed to update item state" << Log::Field(updateErr); - } - - NotifyItemStatusChanged(itemInfo.mItemID, itemInfo.mType, itemInfo.mVersion, finalState, downloadErr); - - if (!downloadErr.IsNone()) { - LOG_ERR() << "Failed to download item" << Log::Field("id", itemInfo.mItemID) - << Log::Field("version", itemInfo.mVersion) << Log::Field(downloadErr); - - if (downloadErr == ErrorEnum::eCanceled) { - return downloadErr; - } - } - - statuses[i].mState = finalState; - statuses[i].mError = downloadErr; - } - - return ErrorEnum::eNone; -} - -Error ImageManager::DownloadItem(const UpdateItemInfo& itemInfo, const Array& certificates, - const Array& certificateChains) -{ - LOG_DBG() << "Download item" << Log::Field("itemID", itemInfo.mItemID) << Log::Field("version", itemInfo.mVersion) - << Log::Field("digest", itemInfo.mIndexDigest); - - mCurrentItemID = itemInfo.mItemID; - mCurrentItemVersion = itemInfo.mVersion; - - auto clearCurrentItem = DeferRelease(&mMutex, [this](void*) { - mCurrentItemID.Clear(); - mCurrentItemVersion.Clear(); - }); - - StaticString downloadPath; - if (auto err = GetBlobFilePath(mBlobsDownloadPath, itemInfo.mIndexDigest, downloadPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString installPath; - if (auto err = GetBlobFilePath(mBlobsInstallPath, itemInfo.mIndexDigest, installPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto imageIndex = MakeUnique(mAllocator); - if (!imageIndex) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err - = LoadIndex(itemInfo.mIndexDigest, downloadPath, installPath, certificates, certificateChains, *imageIndex); - !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - LOG_DBG() << "Processing manifests" << Log::Field("count", imageIndex->mManifests.Size()); - - for (const auto& manifestDescriptor : imageIndex->mManifests) { - auto manifest = MakeUnique(mAllocator); - if (!manifest) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = LoadManifest(manifestDescriptor.mDigest, certificates, certificateChains, *manifest); - !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = LoadBlob(manifest->mConfig, certificates, certificateChains); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (manifest->mItemConfig.HasValue()) { - if (auto err = LoadBlob(*manifest->mItemConfig, certificates, certificateChains); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - if (auto err = LoadLayers(manifest->mLayers, certificates, certificateChains); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - LOG_DBG() << "Successfully processed item" << Log::Field("itemID", itemInfo.mItemID) - << Log::Field("version", itemInfo.mVersion); - - return ErrorEnum::eNone; -} - -Error ImageManager::LoadIndex(const String& digest, const String& downloadPath, const String& installPath, - const Array& certificates, const Array& certificateChains, - oci::ImageIndex& imageIndex) -{ - LOG_DBG() << "Load index" << Log::Field("digest", digest); - - Error err; - UniquePtr space; - - auto releaseSpace = DeferRelease(&err, [&space, &digest, &installPath](const Error* releaseErr) { - if (space && !releaseErr->IsNone()) { - LOG_ERR() << "Failed to load index" << Log::Field("digest", digest) << Log::Field(*releaseErr); - - if (auto removeErr = fs::RemoveAll(installPath); !removeErr.IsNone()) { - LOG_ERR() << "Failed to remove install file" << Log::Field("path", installPath) - << Log::Field(removeErr); - } - - if (auto spaceErr = space->Release(); !spaceErr.IsNone()) { - LOG_ERR() << "Can't release space" << Log::Field(AOS_ERROR_WRAP(spaceErr)); - } - - return; - } - - if (space) { - if (auto spaceErr = space->Accept(); !spaceErr.IsNone()) { - LOG_ERR() << "Can't accept space" << Log::Field(AOS_ERROR_WRAP(spaceErr)); - } - } - }); - - err = EnsureBlob(digest, downloadPath, installPath, certificates, certificateChains, space); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = mOCISpec->LoadImageIndex(installPath, imageIndex); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::LoadManifest(const String& digest, const Array& certificates, - const Array& certificateChains, oci::ImageManifest& manifest) -{ - LOG_DBG() << "Load manifest" << Log::Field("digest", digest); - - Error err; - UniquePtr space; - - StaticString downloadPath; - err = GetBlobFilePath(mBlobsDownloadPath, digest, downloadPath); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString installPath; - err = GetBlobFilePath(mBlobsInstallPath, digest, installPath); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto releaseSpace = DeferRelease(&err, [&space, &digest, &installPath](const Error* releaseErr) { - if (space && !releaseErr->IsNone()) { - LOG_ERR() << "Failed to load manifest" << Log::Field("digest", digest) << Log::Field(*releaseErr); - - if (auto removeErr = fs::RemoveAll(installPath); !removeErr.IsNone()) { - LOG_ERR() << "Failed to remove install file" << Log::Field("path", installPath) - << Log::Field(removeErr); - } - - if (auto spaceErr = space->Release(); !spaceErr.IsNone()) { - LOG_ERR() << "Can't release space" << Log::Field(AOS_ERROR_WRAP(spaceErr)); - } - - return; - } - - if (space) { - if (auto spaceErr = space->Accept(); !spaceErr.IsNone()) { - LOG_ERR() << "Can't accept space" << Log::Field(AOS_ERROR_WRAP(spaceErr)); - } - } - }); - - err = EnsureBlob(digest, downloadPath, installPath, certificates, certificateChains, space); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = mOCISpec->LoadImageManifest(installPath, manifest); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::LoadBlob(const oci::ContentDescriptor& descriptor, - const Array& certificates, const Array& certificateChains) -{ - LOG_DBG() << "Load blob" << Log::Field("digest", descriptor.mDigest); - - Error err; - UniquePtr space; - - StaticString downloadPath; - err = GetBlobFilePath(mBlobsDownloadPath, descriptor.mDigest, downloadPath); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString installPath; - err = GetBlobFilePath(mBlobsInstallPath, descriptor.mDigest, installPath); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto releaseSpace = DeferRelease(&err, [&space, &descriptor, &installPath](const Error* releaseErr) { - if (space && !releaseErr->IsNone()) { - LOG_ERR() << "Failed to load blob" << Log::Field("digest", descriptor.mDigest) << Log::Field(*releaseErr); - - if (auto removeErr = fs::RemoveAll(installPath); !removeErr.IsNone()) { - LOG_ERR() << "Failed to remove install file" << Log::Field("path", installPath) - << Log::Field(removeErr); - } - - if (auto spaceErr = space->Release(); !spaceErr.IsNone()) { - LOG_ERR() << "Can't release space" << Log::Field(AOS_ERROR_WRAP(spaceErr)); - } - - return; - } - - if (space) { - if (auto spaceErr = space->Accept(); !spaceErr.IsNone()) { - LOG_ERR() << "Can't accept space" << Log::Field(AOS_ERROR_WRAP(spaceErr)); - } - } - }); - - err = EnsureBlob(descriptor.mDigest, downloadPath, installPath, certificates, certificateChains, space); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::LoadLayers(const Array& layers, - const Array& certificates, const Array& certificateChains) -{ - LOG_DBG() << "Load layers" << Log::Field("count", layers.Size()); - - for (const auto& layer : layers) { - if (auto err = LoadBlob(layer, certificates, certificateChains); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::EnsureBlob(const String& digest, const String& downloadPath, const String& installPath, - const Array& certificates, const Array& certificateChains, - UniquePtr& space) -{ - LOG_DBG() << "Ensure blob" << Log::Field("digest", digest); - - auto blobInfo = MakeUnique(mAllocator); - if (!blobInfo) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - DownloadSpace downloadSpace; - - auto discardDownload = DeferRelease( - &downloadSpace, [this, &downloadPath](DownloadSpace* spacePtr) { DiscardDownload(downloadPath, *spacePtr); }); - - do { - if (auto err = DownloadBlob(digest, downloadPath, installPath, *blobInfo, downloadSpace); !err.IsNone()) { - if (err == ErrorEnum::eAlreadyExist) { - return ErrorEnum::eNone; - } - - return AOS_ERROR_WRAP(err); - } - - fs::FileInfo downloadFileInfo; - - if (auto err = mFileInfoProvider->GetFileInfo(downloadPath, downloadFileInfo, crypto::HashEnum::eSHA3_256); - !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (downloadFileInfo.mCheckSum == blobInfo->mSHA256) { - break; - } - - LOG_WRN() << "Download checksum mismatch, retrying download" << Log::Field("digest", digest); - - DiscardDownload(downloadPath, downloadSpace); - } while (true); - - auto err = DecryptAndValidateBlob(downloadPath, installPath, *blobInfo, certificates, certificateChains, space); - - DiscardDownload(downloadPath, downloadSpace); - - return AOS_ERROR_WRAP(err); -} - -Error ImageManager::GetBlobInfo(const String& digest, BlobInfo& blobInfo) -{ - StaticArray, 1> digests; - if (auto err = digests.PushBack(digest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto blobsInfo = MakeUnique>(mAllocator); - if (!blobsInfo) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - while (true) { - if (auto err = mBlobInfoProvider->GetBlobsInfos(digests, *blobsInfo); !err.IsNone()) { - LOG_ERR() << "Failed to get blobs info" << Log::Field("digest", digest) << Log::Field(err); - - if (auto waitErr = WaitForStop(); !waitErr.IsNone()) { - return AOS_ERROR_WRAP(waitErr); - } - - LOG_DBG() << "Retrying get blobs info" << Log::Field("digest", digest); - - continue; - } - - break; - } - - if (mCancel) { - return ErrorEnum::eCanceled; - } - - if (blobsInfo->IsEmpty()) { - return AOS_ERROR_WRAP(ErrorEnum::eNotFound); - } - - blobInfo = (*blobsInfo)[0]; - - return ErrorEnum::eNone; -} - -Error ImageManager::CheckExistingBlob(const String& installPath) -{ - auto [installExists, checkInstallErr] = fs::FileExist(installPath); - if (!checkInstallErr.IsNone()) { - return AOS_ERROR_WRAP(checkInstallErr); - } - - if (!installExists) { - return ErrorEnum::eNone; - } - - fs::FileInfo fileInfo; - - if (auto err = mFileInfoProvider->GetFileInfo(installPath, fileInfo); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString fileName; - if (auto err = fs::BaseName(installPath, fileName); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto expectedSHA256 = MakeUnique>(mAllocator); - if (!expectedSHA256) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = fileName.HexToByteArray(*expectedSHA256); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (fileInfo.mCheckSum == *expectedSHA256) { - return ErrorEnum::eAlreadyExist; - } - - LOG_WRN() << "Blob exists but SHA256 mismatch, will redownload"; - - if (auto removeErr = fs::RemoveAll(installPath); !removeErr.IsNone()) { - LOG_ERR() << "Failed to remove file with mismatched SHA256" << Log::Field(removeErr); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::PrepareDownloadSpace( - const String& downloadPath, const BlobInfo& blobInfo, DownloadSpace& downloadSpace) -{ - downloadSpace.mExistingSize = 0; - downloadSpace.mTotalSize = blobInfo.mSize; - - auto [downloadExists, checkDownloadErr] = fs::FileExist(downloadPath); - if (!checkDownloadErr.IsNone()) { - return AOS_ERROR_WRAP(checkDownloadErr); - } - - if (downloadExists) { - auto [partialSize, getSizeErr] = fs::CalculateSize(*mAllocator, downloadPath); - if (!getSizeErr.IsNone()) { - return AOS_ERROR_WRAP(getSizeErr); - } - - if (partialSize > blobInfo.mSize) { - LOG_WRN() << "Partial download exceeds blob size, removing" << Log::Field("path", downloadPath) - << Log::Field("size", partialSize) << Log::Field("blobSize", blobInfo.mSize); - - if (auto err = fs::RemoveAll(downloadPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } else { - downloadSpace.mExistingSize = partialSize; - } - } - - Error err; - - Tie(downloadSpace.mSpace, err) - = mDownloadingSpaceAllocator->AllocateSpace(blobInfo.mSize - downloadSpace.mExistingSize); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - LOG_DBG() << "Prepared download space" << Log::Field("path", downloadPath) - << Log::Field("existingSize", downloadSpace.mExistingSize) << Log::Field("blobSize", blobInfo.mSize); - - return ErrorEnum::eNone; -} - -void ImageManager::DiscardDownload(const String& downloadPath, DownloadSpace& downloadSpace) -{ - if (!downloadSpace.mSpace) { - return; - } - - if (auto err = fs::RemoveAll(downloadPath); !err.IsNone()) { - LOG_ERR() << "Failed to remove download path" << Log::Field("path", downloadPath) << Log::Field(err); - } - - if (downloadSpace.mExistingSize != 0) { - mDownloadingSpaceAllocator->FreeSpace(downloadSpace.mExistingSize); - } - - if (auto err = downloadSpace.mSpace->Release(); !err.IsNone()) { - LOG_ERR() << "Failed to release downloading space" << Log::Field(err); - } - - downloadSpace.mSpace.Reset(); - downloadSpace.mExistingSize = 0; -} - -void ImageManager::AcceptDownloadSpace(DownloadSpace& downloadSpace, size_t bytesOnDisk) -{ - if (!downloadSpace.mSpace) { - return; - } - - if (bytesOnDisk < downloadSpace.mTotalSize) { - mDownloadingSpaceAllocator->FreeSpace(downloadSpace.mTotalSize - bytesOnDisk); - } - - if (auto err = downloadSpace.mSpace->Accept(); !err.IsNone()) { - LOG_ERR() << "Failed to accept downloading space" << Log::Field(err); - } - - downloadSpace.mSpace.Reset(); - downloadSpace.mExistingSize = 0; -} - -Error ImageManager::PerformDownload(const BlobInfo& blobInfo, const String& downloadPath, DownloadSpace& downloadSpace) -{ - { - LockGuard lock {mMutex}; - - mCurrentDownloadDigest = blobInfo.mDigest; - } - - auto clearDigest = DeferRelease(&mMutex, [this](Mutex* mutex) { - LockGuard lock {*mutex}; - - mCurrentDownloadDigest.Clear(); - }); - - while (true) { - if (auto err = mDownloader->Download(blobInfo.mDigest, blobInfo.mURLs[0], downloadPath); !err.IsNone()) { - LOG_ERR() << "Failed to download" << Log::Field("url", blobInfo.mURLs[0]) - << Log::Field("path", downloadPath) << Log::Field(AOS_ERROR_WRAP(err)); - - err = WaitForStop(); - if (!err.IsNone()) { - auto [bytesOnDisk, sizeErr] = fs::CalculateSize(*mAllocator, downloadPath); - if (!sizeErr.IsNone()) { - LOG_WRN() << "Failed to get partial download size" << Log::Field("path", downloadPath) - << Log::Field(sizeErr); - - return err; - } - - AcceptDownloadSpace(downloadSpace, bytesOnDisk); - - return err; - } - - LOG_DBG() << "Retrying download" << Log::Field("url", blobInfo.mURLs[0]) - << Log::Field("path", downloadPath); - - continue; - } - - break; - } - - LOG_DBG() << "Downloaded successfully" << Log::Field("path", downloadPath); - - return ErrorEnum::eNone; -} - -Error ImageManager::DownloadBlob(const String& digest, const String& downloadPath, const String& installPath, - BlobInfo& blobInfo, DownloadSpace& downloadSpace) -{ - LOG_DBG() << "Download blob" << Log::Field("digest", digest); - - if (mCancel) { - return ErrorEnum::eCanceled; - } - - if (auto err = CheckExistingBlob(installPath); !err.IsNone()) { - if (err == ErrorEnum::eAlreadyExist) { - return err; - } - - return AOS_ERROR_WRAP(err); - } - - if (auto err = GetBlobInfo(digest, blobInfo); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString downloadDir; - - if (auto err = fs::ParentPath(downloadPath, downloadDir); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = fs::MakeDirAll(downloadDir); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = PrepareDownloadSpace(downloadPath, blobInfo, downloadSpace); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = PerformDownload(blobInfo, downloadPath, downloadSpace); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::DecryptAndValidateBlob(const String& downloadPath, const String& installPath, - const BlobInfo& blobInfo, const Array& certificates, - const Array& certificateChains, UniquePtr& installSpace) -{ - LOG_DBG() << "Decrypt and validate blob" << Log::Field("downloadPath", downloadPath) - << Log::Field("installPath", installPath); - - Error err; - - Tie(installSpace, err) = mInstallSpaceAllocator->AllocateSpace(blobInfo.mSize); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString installDir; - - err = fs::ParentPath(installPath, installDir); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = fs::MakeDirAll(installDir); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (blobInfo.mDecryptInfo.HasValue()) { - err = mCryptoHelper->Decrypt(downloadPath, installPath, *blobInfo.mDecryptInfo); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } else { - err = fs::CopyFile(*mAllocator, downloadPath, installPath); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - LOG_DBG() << "Decrypted successfully" << Log::Field("path", installPath); - - if (blobInfo.mSignInfo.HasValue()) { - err = mCryptoHelper->ValidateSigns(installPath, *blobInfo.mSignInfo, certificateChains, certificates); - if (!err.IsNone()) { - if (auto removeErr = fs::RemoveAll(installPath); !removeErr.IsNone()) { - LOG_ERR() << "Failed to remove install file" << Log::Field("path", installPath) - << Log::Field(removeErr); - } - - return AOS_ERROR_WRAP(err); - } - } - - fs::FileInfo fileInfo; - - err = mFileInfoProvider->GetFileInfo(installPath, fileInfo); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = VerifyBlobChecksum(blobInfo.mDigest, fileInfo); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - LOG_DBG() << "Validated successfully" << Log::Field("path", installPath); - - return ErrorEnum::eNone; -} - -bool ImageManager::StartAction() -{ - UniqueLock lock {mMutex}; - - if (auto err = mCondVar.Wait(lock, [this]() { return !mInProgress || mCancel; }); !err.IsNone()) { - LOG_ERR() << "Failed to wait for image manager action" << Log::Field(err); - - return false; - } - - const bool cancelledWhileRunning = mCancel && mInProgress; - - mCancel = false; - - if (cancelledWhileRunning) { - return false; - } - - mInProgress = true; - - return true; -} - -void ImageManager::StopAction() -{ - LockGuard lock {mMutex}; - - mInProgress = false; - if (auto err = mCondVar.NotifyAll(); !err.IsNone()) { - LOG_ERR() << "Failed to notify image manager stop" << Log::Field(err); - } -} - -void ImageManager::NotifyItemsStatusesChanged(const Array& statuses) -{ - LockGuard lock {mMutex}; - - for (const auto& status : statuses) { - LOG_DBG() << "Item status changed" << Log::Field("id", status.mItemID) << Log::Field("type", status.mType) - << Log::Field("version", status.mVersion) << Log::Field("state", ItemState(status.mState)) - << Log::Field("error", status.mError); - } - - for (auto* listener : mListeners) { - listener->OnItemsStatusesChanged(statuses); - } -} - -void ImageManager::NotifyItemStatusChanged( - const String& itemID, const UpdateItemType& type, const String& version, ItemStateEnum state, const Error& error) -{ - StaticArray status; - - (void)status.Resize(1); - - status[0].mItemID = itemID; - status[0].mType = type; - status[0].mVersion = version; - status[0].mState = state; - status[0].mError = error; - - NotifyItemsStatusesChanged(status); -} - -void ImageManager::RegisterOutdatedItems(const Array& items) -{ - for (const auto& item : items) { - if (item.mState == ItemStateEnum::eRemoved) { - if (auto err = mInstallSpaceAllocator->AddOutdatedItem(item.mItemID, item.mVersion, item.mTimestamp); - !err.IsNone()) { - LOG_ERR() << "Failed to add outdated item" << Log::Field("itemID", item.mItemID) - << Log::Field("version", item.mVersion) << Log::Field(err); - } - } - } -} - -Error ImageManager::VerifyBlobIntegrity(const String& digest) -{ - StaticString blobPath; - if (auto err = GetBlobFilePath(mBlobsInstallPath, digest, blobPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - fs::FileInfo fileInfo; - if (auto err = mFileInfoProvider->GetFileInfo(blobPath, fileInfo); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = VerifyBlobChecksum(digest, fileInfo); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::VerifyBlobChecksum(const String& digest, const fs::FileInfo& fileInfo) -{ - auto [colonPos, findErr] = digest.FindSubstr(0, ":"); - if (!findErr.IsNone()) { - return AOS_ERROR_WRAP(ErrorEnum::eInvalidArgument); - } - - StaticString hash; - if (auto err = hash.Insert(hash.end(), digest.CStr() + colonPos + 1, digest.CStr() + digest.Size()); - !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto expectedSHA256 = MakeUnique>(mAllocator); - if (!expectedSHA256) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = hash.HexToByteArray(*expectedSHA256); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (fileInfo.mCheckSum != *expectedSHA256) { - return ErrorEnum::eInvalidChecksum; - } - - return ErrorEnum::eNone; -} - -Error ImageManager::VerifyItemBlobs(const String& indexDigest) -{ - LOG_DBG() << "Verify item blobs" << Log::Field("indexDigest", indexDigest); - - if (auto err = VerifyBlobIntegrity(indexDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString indexPath; - if (auto err = GetBlobFilePath(mBlobsInstallPath, indexDigest, indexPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto imageIndex = MakeUnique(mAllocator); - if (!imageIndex) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = mOCISpec->LoadImageIndex(indexPath, *imageIndex); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - for (const auto& manifestDescriptor : imageIndex->mManifests) { - if (auto err = VerifyBlobIntegrity(manifestDescriptor.mDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString manifestPath; - if (auto err = GetBlobFilePath(mBlobsInstallPath, manifestDescriptor.mDigest, manifestPath); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto manifest = MakeUnique(mAllocator); - if (!manifest) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = mOCISpec->LoadImageManifest(manifestPath, *manifest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = VerifyBlobIntegrity(manifest->mConfig.mDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (manifest->mItemConfig.HasValue()) { - if (auto err = VerifyBlobIntegrity(manifest->mItemConfig->mDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - for (const auto& layer : manifest->mLayers) { - if (auto err = VerifyBlobIntegrity(layer.mDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - } - - LOG_DBG() << "Item blobs verified successfully"; - - return ErrorEnum::eNone; -} - -bool ImageManager::IsBlobUsedByItems(const String& blobDigest, const Array& items) -{ - for (const auto& item : items) { - if (item.mIndexDigest == blobDigest) { - return true; - } - - StaticString indexPath; - if (auto err = GetBlobFilePath(mBlobsInstallPath, item.mIndexDigest, indexPath); !err.IsNone()) { - continue; - } - - auto imageIndex = MakeUnique(mAllocator); - if (!imageIndex) { - continue; - } - - if (auto err = mOCISpec->LoadImageIndex(indexPath, *imageIndex); !err.IsNone()) { - continue; - } - - for (const auto& manifestDescriptor : imageIndex->mManifests) { - if (manifestDescriptor.mDigest == blobDigest) { - return true; - } - - StaticString manifestPath; - if (auto err = GetBlobFilePath(mBlobsInstallPath, manifestDescriptor.mDigest, manifestPath); - !err.IsNone()) { - continue; - } - - auto manifest = MakeUnique(mAllocator); - if (!manifest) { - continue; - } - - if (auto err = mOCISpec->LoadImageManifest(manifestPath, *manifest); !err.IsNone()) { - continue; - } - - if (manifest->mConfig.mDigest == blobDigest) { - return true; - } - - if (manifest->mItemConfig.HasValue() && manifest->mItemConfig->mDigest == blobDigest) { - return true; - } - - for (const auto& layer : manifest->mLayers) { - if (layer.mDigest == blobDigest) { - return true; - } - } - } - } - - return false; -} - -RetWithError ImageManager::CleanupOrphanedBlobs() -{ - LOG_DBG() << "Cleanup orphaned blobs"; - - size_t totalSize = 0; - - auto storedItems = MakeUnique>(mAllocator); - if (!storedItems) { - return {0, AOS_ERROR_WRAP(ErrorEnum::eNoMemory)}; - } - - if (auto err = mStorage->GetAllItemsInfos(*storedItems); !err.IsNone()) { - return {0, AOS_ERROR_WRAP(err)}; - } - - auto algorithmDirIterator = fs::DirIterator(mBlobsInstallPath); - - while (algorithmDirIterator.Next()) { - auto algorithm = algorithmDirIterator->mPath; - auto algorithmDir = fs::JoinPath(mBlobsInstallPath, algorithm); - - auto blobIterator = fs::DirIterator(algorithmDir); - - while (blobIterator.Next()) { - auto hash = blobIterator->mPath; - - StaticString blobDigest; - (void)blobDigest.Append(algorithm).Append(":").Append(hash); - - if (!IsBlobUsedByItems(blobDigest, *storedItems)) { - auto filePath = fs::JoinPath(algorithmDir, hash); - - auto [blobSize, sizeErr] = fs::CalculateSize(*mAllocator, filePath); - if (!sizeErr.IsNone()) { - LOG_WRN() << "Failed to get blob size" << Log::Field("path", filePath) << Log::Field(sizeErr); - } else { - totalSize += blobSize; - } - - LOG_DBG() << "Remove orphaned blob" << Log::Field("path", filePath) << Log::Field("size", blobSize); - - if (auto removeErr = fs::RemoveAll(filePath); !removeErr.IsNone()) { - LOG_ERR() << "Failed to remove orphaned blob" << Log::Field(removeErr); - } - } - } - } - - LOG_DBG() << "Cleanup orphaned blobs completed" << Log::Field("totalSize", totalSize); - - return {totalSize, ErrorEnum::eNone}; -} - -Error ImageManager::RemoveDifferentVersions(const Array& itemsInfo, const Array& storedItems) -{ - LOG_DBG() << "Remove different versions"; - - for (const auto& storedItem : storedItems) { - if (storedItem.mState != ItemStateEnum::eInstalled) { - continue; - } - - auto requestedIt = itemsInfo.FindIf([&storedItem](const auto& item) { - return item.mItemID == storedItem.mItemID && item.mVersion != storedItem.mVersion; - }); - - if (requestedIt != itemsInfo.end()) { - LOG_DBG() << "Wiping different version" << Log::Field("itemID", storedItem.mItemID) - << Log::Field("storedVersion", storedItem.mVersion) - << Log::Field("requestedVersion", requestedIt->mVersion); - - if (auto err = mStorage->RemoveItem(storedItem.mItemID, storedItem.mVersion); !err.IsNone()) { - LOG_ERR() << "Failed to remove item from storage" << Log::Field("itemID", storedItem.mItemID) - << Log::Field("version", storedItem.mVersion) << Log::Field(err); - } - - NotifyItemStatusChanged( - storedItem.mItemID, storedItem.mType, storedItem.mVersion, ItemStateEnum::eRemoved, ErrorEnum::eNone); - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::VerifyBlobsIntegrity( - const Array& itemsInfo, const Array& storedItems, Array& statuses) -{ - LOG_DBG() << "Verify blobs integrity"; - - for (size_t i = 0; i < itemsInfo.Size(); i++) { - const auto& itemInfo = itemsInfo[i]; - - auto storedIt = storedItems.FindIf([&itemInfo](const auto& stored) { - return stored.mItemID == itemInfo.mItemID && stored.mVersion == itemInfo.mVersion; - }); - - if (storedIt == storedItems.end()) { - LOG_WRN() << "Item not found in storage" << Log::Field("itemID", itemInfo.mItemID) - << Log::Field("version", itemInfo.mVersion); - - statuses[i].mState = ItemStateEnum::eFailed; - statuses[i].mError = ErrorEnum::eNotFound; - - continue; - } - - if (auto err = VerifyItemBlobs(storedIt->mIndexDigest); !err.IsNone()) { - LOG_ERR() << "Item blobs verification failed" << Log::Field("itemID", itemInfo.mItemID) - << Log::Field("version", itemInfo.mVersion) << Log::Field(err); - - statuses[i].mState = ItemStateEnum::eFailed; - statuses[i].mError = err; - - if (auto removeErr = mStorage->RemoveItem(storedIt->mItemID, storedIt->mVersion); !removeErr.IsNone()) { - LOG_ERR() << "Failed to remove invalid item" << Log::Field(removeErr); - } - - NotifyItemStatusChanged(itemInfo.mItemID, itemInfo.mType, itemInfo.mVersion, ItemStateEnum::eFailed, err); - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::SetItemsToInstalled(const Array& itemsInfo, const Array& storedItems) -{ - LOG_DBG() << "Set items to installed"; - - for (const auto& itemInfo : itemsInfo) { - auto storedIt = storedItems.FindIf([&itemInfo](const auto& stored) { - return stored.mItemID == itemInfo.mItemID && stored.mVersion == itemInfo.mVersion; - }); - - if (storedIt != storedItems.end() && storedIt->mState != ItemStateEnum::eInstalled) { - if (auto err = mStorage->UpdateItemState(storedIt->mItemID, storedIt->mVersion, ItemStateEnum::eInstalled); - !err.IsNone()) { - LOG_ERR() << "Failed to update item state to installed" << Log::Field("itemID", storedIt->mItemID) - << Log::Field("version", storedIt->mVersion) << Log::Field(err); - } - - NotifyItemStatusChanged( - storedIt->mItemID, storedIt->mType, storedIt->mVersion, ItemStateEnum::eInstalled, ErrorEnum::eNone); - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::SetItemsToRemoved(const Array& itemsInfo, const Array& storedItems) -{ - LOG_DBG() << "Set items to removed"; - - for (const auto& storedItem : storedItems) { - auto requestedIt = itemsInfo.FindIf([&storedItem](const auto& item) { - return item.mItemID == storedItem.mItemID && item.mVersion == storedItem.mVersion; - }); - - if (requestedIt == itemsInfo.end() && storedItem.mState != ItemStateEnum::eRemoved) { - LOG_DBG() << "Setting item to removed state" << Log::Field("itemID", storedItem.mItemID) - << Log::Field("version", storedItem.mVersion) - << Log::Field("currentState", ItemState(storedItem.mState)); - - auto now = Time::Now(); - - if (auto err - = mStorage->UpdateItemState(storedItem.mItemID, storedItem.mVersion, ItemStateEnum::eRemoved, now); - !err.IsNone()) { - LOG_ERR() << "Failed to update item state to removed" << Log::Field("itemID", storedItem.mItemID) - << Log::Field("version", storedItem.mVersion) << Log::Field(err); - } - - if (auto err = mInstallSpaceAllocator->AddOutdatedItem(storedItem.mItemID, storedItem.mVersion, now); - !err.IsNone()) { - LOG_ERR() << "Failed to add outdated item" << Log::Field("itemID", storedItem.mItemID) - << Log::Field("version", storedItem.mVersion) << Log::Field(err); - } - - NotifyItemStatusChanged( - storedItem.mItemID, storedItem.mType, storedItem.mVersion, ItemStateEnum::eRemoved, ErrorEnum::eNone); - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::GetBlobFilePath( - const String& basePath, const String& digest, StaticString& path) const -{ - auto [colonPos, findErr] = digest.FindSubstr(0, ":"); - if (!findErr.IsNone()) { - return AOS_ERROR_WRAP(ErrorEnum::eInvalidArgument); - } - - StaticString algorithm; - if (auto err = algorithm.Insert(algorithm.end(), digest.CStr(), digest.CStr() + colonPos); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString hash; - if (auto err = hash.Insert(hash.end(), digest.CStr() + colonPos + 1, digest.CStr() + digest.Size()); - !err.IsNone()) { return AOS_ERROR_WRAP(err); } - path = fs::JoinPath(basePath, algorithm); - path = fs::JoinPath(path, hash); - return ErrorEnum::eNone; } diff --git a/src/core/cm/imagemanager/imagemanager.hpp b/src/core/cm/imagemanager/imagemanager.hpp index 9a5d2a058..c7b5e9dff 100644 --- a/src/core/cm/imagemanager/imagemanager.hpp +++ b/src/core/cm/imagemanager/imagemanager.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 EPAM Systems, Inc. + * Copyright (C) 2026 EPAM Systems, Inc. * * SPDX-License-Identifier: Apache-2.0 */ @@ -8,19 +8,13 @@ #define AOS_CORE_CM_IMAGEMANAGER_IMAGEMANAGER_HPP_ #include -#include +#include +#include #include -#include -#include -#include -#include -#include +#include #include "config.hpp" -#include "itf/blobinfoprovider.hpp" #include "itf/imagemanager.hpp" -#include "itf/iteminfoprovider.hpp" -#include "itf/storage.hpp" namespace aos::cm::imagemanager { @@ -29,245 +23,146 @@ namespace aos::cm::imagemanager { */ /** - * Image manager. + * CM image manager. */ -class ImageManager : public ImageManagerItf, public ItemInfoProviderItf, public spaceallocator::ItemRemoverItf { +class ImageManager : public common::imagemanager::ImageManager, + public ImageManagerItf, + public common::imagemanager::BlobInfoProviderItf { public: /** * Initializes image manager. * - * @param allocator allocator to use for temporary objects. * @param config image manager config. - * @param storage stores internal persistent data. - * @param blobInfoProvider retrieves blobs info. - * @param downloadingSpaceAllocator allocates temporary space for downloading. - * @param installSpaceAllocator allocates disk space for install. + * @param cloudBlobInfoProvider provides download info of the blobs from the cloud. + * @param storage image manager storage. + * @param spaceAllocator space allocator of the blobs. + * @param downloadSpaceAllocator space allocator of the temporary download files. * @param downloader downloads blobs. - * @param fileserver translates local file paths to remote URLs. - * @param cryptoHelper decrypts and verifies update images. - * @param fileInfoProvider gets file info. - * @param ociSpec parses OCI spec files. + * @param cryptoHelper decrypts blobs and verifies their signatures. + * @param fileServer translates local blob paths to URLs. + * @param fileInfoProvider file info provider. + * @param ociSpec OCI spec interface. + * @param fsPlatform file system platform interface. + * @param allocator allocator for temporary objects. * @return Error. */ - Error Init(AllocatorItf& allocator, const Config& config, StorageItf& storage, - BlobInfoProviderItf& blobInfoProvider, spaceallocator::SpaceAllocatorItf& downloadingSpaceAllocator, - spaceallocator::SpaceAllocatorItf& installSpaceAllocator, downloader::DownloaderItf& downloader, - fileserver::FileServerItf& fileserver, crypto::CryptoHelperItf& cryptoHelper, - fs::FileInfoProviderItf& fileInfoProvider, oci::OCISpecItf& ociSpec); + Error Init(const Config& config, common::imagemanager::BlobInfoProviderItf& cloudBlobInfoProvider, + common::imagemanager::StorageItf& storage, spaceallocator::SpaceAllocatorItf& spaceAllocator, + spaceallocator::SpaceAllocatorItf& downloadSpaceAllocator, downloader::DownloaderItf& downloader, + crypto::CryptoHelperItf& cryptoHelper, fileserver::FileServerItf& fileServer, + fs::FileInfoProviderItf& fileInfoProvider, oci::OCISpecItf& ociSpec, fs::FSPlatformItf& fsPlatform, + AllocatorItf& allocator); /** - * Starts image manager. + * Sets certificates and certificate chains used to verify blob signatures. * - * @return Error. - */ - Error Start(); - - /** - * Stops image manager. - * - * @return Error. - */ - Error Stop(); - - /** - * Downloads update items. - * - * @param itemsInfo update items info. * @param certificates list of certificates. * @param certificateChains list of certificate chains. - * @param[out] statuses update items statuses. - * @return Error. - */ - Error DownloadUpdateItems(const Array& itemsInfo, - const Array& certificates, - const Array& certificateChains, Array& statuses) override; - - /** - * Installs update items. - * - * @param itemsInfo update items info. - * @param[out] statuses update items statuses. - * @return Error. - */ - Error InstallUpdateItems(const Array& itemsInfo, Array& statuses) override; - - /** - * Cancels ongoing operations: download or install. - * - * @return Error. - */ - Error Cancel() override; - - /** - * Retrieves update items statuses. - * - * @param[out] statuses list of update items statuses. - * @return Error. - */ - Error GetUpdateItemsStatuses(Array& statuses) override; - - /** - * Subscribes status notifications. - * - * @param listener status listener. * @return Error. */ - Error SubscribeListener(ItemStatusListenerItf& listener) override; + Error SetCerts(const Array& certificates, + const Array& certificateChains) override; /** - * Unsubscribes from status notifications. + * Installs update item. * - * @param listener status listener. + * @param itemInfo update item info. * @return Error. */ - Error UnsubscribeListener(ItemStatusListenerItf& listener) override; + Error InstallItem(const UpdateItemInfo& itemInfo) override; /** - * Returns update item index digest. + * Removes update item. * - * @param itemID update item ID. - * @param version update item version. - * @param[out] digest result item digest. + * @param itemID item ID. + * @param version item version. * @return Error. */ - Error GetIndexDigest(const String& itemID, const String& version, String& digest) const override; + Error RemoveItem(const String& itemID, const String& version) override; /** - * Returns blob path by its digest. + * Cancels all items being installed. Waits until all InstallItem and RemoveItem calls in progress return. * - * @param digest blob digest. - * @param[out] path result blob path. * @return Error. */ - Error GetBlobPath(const String& digest, String& path) const override; + Error Cancel() override; /** - * Returns blob URL by its digest. + * Returns download info of a blob stored on the CM. * * @param digest blob digest. - * @param[out] url result blob URL. - * @return Error. - */ - Error GetBlobURL(const String& digest, String& url) const override; - - /** - * Returns item current version. - * - * @param itemID update item ID. - * @param[out] version result item version. + * @param[out] blobInfo blob info. * @return Error. */ - Error GetItemCurrentVersion(const String& itemID, String& version) const override; + Error GetBlobInfo(const String& digest, BlobInfo& blobInfo) override; /** - * Removes item. + * Returns item remover which should be passed to the download space allocator to remove outdated temporary + * download files. * - * @param id item id. - * @param version item version. - * @return RetWithError. + * @return spaceallocator::ItemRemoverItf&. */ - RetWithError RemoveItem(const String& id, const String& version) override; + spaceallocator::ItemRemoverItf& GetDownloadItemRemover() { return mDownloadItemRemover; } private: - static constexpr auto cBlobsDirName = "blobs"; - - static constexpr auto cMaxNumListeners = 1; - static constexpr auto cRetryTimeout = Time::cSeconds * 2; - static constexpr auto cDigestAlgorithmLen = 16; - static constexpr auto cMaxNumItemVersions = 2; - static constexpr auto cMaxNumStoredItems = cMaxNumUpdateItems * cMaxNumItemVersions; - - /** - * Downloading space of a single blob. mSpace reserves only bytes to be written: mTotalSize - mExistingSize. - */ - struct DownloadSpace { - UniquePtr mSpace; - size_t mExistingSize {}; - size_t mTotalSize {}; + class DownloadItemRemover : public spaceallocator::ItemRemoverItf { + public: + explicit DownloadItemRemover(ImageManager& imageManager) + : mImageManager(imageManager) + { + } + + RetWithError RemoveItem(const String& itemID, const String& version) override + { + (void)version; + + return mImageManager.RemoveDownloadedBlob(itemID); + } + + private: + ImageManager& mImageManager; }; - Error RemoveOutdatedItems(); - Error RemoveOldItemVersions(const String& itemID, Array& storedItems); - Error WaitForStop(); - Error RemovePendingItems(const Array& storedItems, Array& statuses); - Error CleanupDownloadingItems(const Array& currentItems, const Array& storedItems); - Error VerifyStoredItems(const Array& itemsInfo, Array& storedItems, - const Array& certificates, - const Array& certificateChains, Array& statuses); - Error ProcessDownloadRequest(const Array& itemsInfo, Array& storedItems, - const Array& certificates, - const Array& certificateChains, Array& statuses); - RetWithError CleanupOrphanedBlobs(); - Error RemoveDifferentVersions(const Array& itemsInfo, const Array& storedItems); - Error VerifyBlobsIntegrity( - const Array& itemsInfo, const Array& storedItems, Array& statuses); - Error SetItemsToInstalled(const Array& itemsInfo, const Array& storedItems); - Error SetItemsToRemoved(const Array& itemsInfo, const Array& storedItems); - Error DownloadItem(const UpdateItemInfo& itemInfo, const Array& certificates, - const Array& certificateChains); - Error LoadIndex(const String& digest, const String& downloadPath, const String& installPath, - const Array& certificates, - const Array& certificateChains, oci::ImageIndex& imageIndex); - Error LoadManifest(const String& digest, const Array& certificates, - const Array& certificateChains, oci::ImageManifest& manifest); - Error LoadBlob(const oci::ContentDescriptor& descriptor, const Array& certificates, - const Array& certificateChains); - Error LoadLayers(const Array& layers, const Array& certificates, - const Array& certificateChains); - Error EnsureBlob(const String& digest, const String& downloadPath, const String& installPath, - const Array& certificates, - const Array& certificateChains, UniquePtr& space); - Error DownloadBlob(const String& digest, const String& downloadPath, const String& installPath, BlobInfo& blobInfo, - DownloadSpace& downloadSpace); - Error GetBlobInfo(const String& digest, BlobInfo& blobInfo); - Error CheckExistingBlob(const String& installPath); - Error PrepareDownloadSpace(const String& downloadPath, const BlobInfo& blobInfo, DownloadSpace& downloadSpace); - Error PerformDownload(const BlobInfo& blobInfo, const String& downloadPath, DownloadSpace& downloadSpace); - void DiscardDownload(const String& downloadPath, DownloadSpace& downloadSpace); - void AcceptDownloadSpace(DownloadSpace& downloadSpace, size_t bytesOnDisk); - Error DecryptAndValidateBlob(const String& downloadPath, const String& installPath, const BlobInfo& blobInfo, - const Array& certificates, - const Array& certificateChains, - UniquePtr& installSpace); - Error VerifyItemBlobs(const String& indexDigest); - Error VerifyBlobIntegrity(const String& digest); - Error VerifyBlobChecksum(const String& digest, const fs::FileInfo& fileInfo); - bool IsBlobUsedByItems(const String& blobDigest, const Array& items); - void NotifyItemsStatusesChanged(const Array& statuses); - void NotifyItemStatusChanged(const String& itemID, const UpdateItemType& type, const String& version, - ItemStateEnum state, const Error& error); - void RegisterOutdatedItems(const Array& items); - bool StartAction(); - void StopAction(); - Error GetBlobFilePath(const String& basePath, const String& digest, StaticString& path) const; - - StorageItf* mStorage {}; - BlobInfoProviderItf* mBlobInfoProvider {}; - spaceallocator::SpaceAllocatorItf* mDownloadingSpaceAllocator {}; - spaceallocator::SpaceAllocatorItf* mInstallSpaceAllocator {}; + Error DownloadBlob(CancelContext& ctx, const String& path, const BlobInfo& blobInfo) override; + RetWithError RemoveDownloadedBlob(const String& digest); + + void StartDownload(const String& digest); + void FinishDownload(const String& digest, const String& downloadPath); + void RegisterDownloadFiles(const String& currentDigest); + void RemoveInvalidDownloadFile(const String& path); + + Error GetDownloadPath(const String& digest, String& downloadPath) const; + RetWithError GetFileSize(const String& path) const; + RetWithError PrepareDownloadFile(const String& downloadPath, size_t size); + Error DownloadFile(CancelContext& ctx, const String& downloadPath, const BlobInfo& blobInfo); + void AcceptDownloadedSpace(spaceallocator::SpaceItf& space, const String& downloadPath, size_t existingSize); + void RemoveDownloadFile(const String& downloadPath, size_t size); + Error ProcessDownloadFile(const String& downloadPath, const String& path, const BlobInfo& blobInfo); + Error ValidateDownloadFile(const String& downloadPath, const BlobInfo& blobInfo); + Error DecryptBlob(const String& downloadPath, const String& path, const BlobInfo& blobInfo); + Error ValidateBlobSigns(const String& downloadPath, const String& path, const BlobInfo& blobInfo); + + Config mConfig; + common::imagemanager::StorageItf* mStorage {}; + spaceallocator::SpaceAllocatorItf* mDownloadSpaceAllocator {}; downloader::DownloaderItf* mDownloader {}; - fileserver::FileServerItf* mFileServer {}; crypto::CryptoHelperItf* mCryptoHelper {}; + fileserver::FileServerItf* mFileServer {}; fs::FileInfoProviderItf* mFileInfoProvider {}; - oci::OCISpecItf* mOCISpec {}; + AllocatorItf* mAllocator {}; - StaticArray mListeners; + Mutex mCertsMutex; + crypto::CertificateInfoArray mCertificates; + crypto::CertificateChainInfoArray mCertificateChains; - Timer mTimer; - mutable Mutex mMutex; - Config mConfig {}; - StaticString mBlobsDownloadPath {}; - StaticString mBlobsInstallPath {}; - StaticString mCurrentDownloadDigest {}; - StaticString mCurrentItemID {}; - StaticString mCurrentItemVersion {}; - ConditionalVariable mCondVar; - bool mCancel {}; - bool mInProgress {}; + Mutex mDownloadSpaceMutex; + size_t mNumDownloads {}; - AllocatorItf* mAllocator {}; + DownloadItemRemover mDownloadItemRemover {*this}; }; +/** @}*/ + } // namespace aos::cm::imagemanager #endif diff --git a/src/core/cm/imagemanager/imagemanager.md b/src/core/cm/imagemanager/imagemanager.md index d3be3bcc0..33071c868 100644 --- a/src/core/cm/imagemanager/imagemanager.md +++ b/src/core/cm/imagemanager/imagemanager.md @@ -1,25 +1,38 @@ # Image manager -Image manager stores update item images and provide update item image info for other modules. +CM image manager downloads update items from the cloud, verifies them and keeps their blobs for the CM modules and the +SM image managers. It is derived from the +[common image manager](../../common/imagemanager/imagemanager.hpp), which stores the blobs, keeps the item statuses +and removes the outdated items. The CM part implements the download of the blobs from the cloud and manages the +temporary download files. It implements the following interfaces: -* [aos::cm::imagemanager::ImageMangerItf](itf/imagemanager.hpp) - manages update items images; -* [aos::cm::imagemanager::ItemStatusProviderItf](itf/itemstatusprovider.hpp) - provides update items statuses; -* [aos::cm::imagemanager::ItemInfoProviderItf](itf/iteminfoprovider.hpp) - provides update items info; -* [aos::spaceallocator::ItemRemoverItf](../../common/spaceallocator/spaceallocator.hpp) - remove outdated items. +* [aos::cm::imagemanager::ImageManagerItf](itf/imagemanager.hpp) - installs and removes update items; +* [aos::common::imagemanager::BlobInfoProviderItf](../../common/imagemanager/itf/blobinfoprovider.hpp) - provides + download info of the stored blobs to the SM image managers; +* [aos::common::imagemanager::ItemStatusProviderItf](../../common/imagemanager/itf/itemstatusprovider.hpp) - provides + update items statuses (inherited from the common image manager); +* [aos::common::imagemanager::ItemInfoProviderItf](../../common/imagemanager/itf/iteminfoprovider.hpp) - provides + local blob paths (inherited from the common image manager); +* [aos::spaceallocator::ItemRemoverItf](../../common/spaceallocator/itf/spaceallocator.hpp) - removes outdated items: + the remover of the blobs is provided by the common image manager, the remover of the temporary download files by + `GetDownloadItemRemover`. It requires the following interfaces: -* [aos::cm::imagemanager::StorageItf](itf/storage.hpp) - stores internal persistent data; -* [aos::cm::imagemanager::BlobInfoProviderItf](itf/blobinfoprovider.hpp) - retrieves blobs info; -* [aos::spaceallocator::SpaceAllocatorItf](../../common/spaceallocator/spaceallocator.hpp) - allocates disk space for - calculating required disk size; -* [aos::cm::fileserver::FileServerItf](../fileserver/itf/fileserver.hpp) - translates local file path's to remote URL's; -* [aos::crypto::CryptoHelperItf](../../common/crypto/itf/cryptohelper.hpp) - decrypts and verifies update images; -* [aos::fs::FileInfoProviderItf](../../common/tools/fs.hpp) - gets file info (size, checksum, etc.); -* [aos::ocispec::OCISpecItf](../../common/ocispec/ocispec.hpp) - parses OCI spec files; -* [aos::cm::imagemanager::GIDPool::Validator](imagemanager.hpp) - validates GID on the platform. +* [aos::common::imagemanager::BlobInfoProviderItf](../../common/imagemanager/itf/blobinfoprovider.hpp) - provides + download info of the blobs from the cloud; +* [aos::common::imagemanager::StorageItf](../../common/imagemanager/itf/storage.hpp) - stores items and their blobs; +* [aos::spaceallocator::SpaceAllocatorItf](../../common/spaceallocator/itf/spaceallocator.hpp) - allocates the space + of the blobs and, with a separate instance, the space of the temporary download files; +* [aos::downloader::DownloaderItf](../../common/downloader/itf/downloader.hpp) - downloads blobs; +* [aos::crypto::CryptoHelperItf](../../common/crypto/itf/cryptohelper.hpp) - decrypts blobs and verifies their + signatures; +* [aos::cm::fileserver::FileServerItf](../fileserver/itf/fileserver.hpp) - translates local blob paths to URLs; +* [aos::fs::FileInfoProviderItf](../../common/tools/fs.hpp) - gets file info (size, checksum); +* [aos::oci::OCISpecItf](../../common/ocispec/itf/ocispec.hpp) - parses OCI spec files; +* [aos::fs::FSPlatformItf](../../common/tools/fs.hpp) - flushes the file system. ```mermaid classDiagram @@ -28,27 +41,18 @@ classDiagram class ImageManager ["aos::cm::imagemanager::ImageManager"] { } - class ImageManagerItf ["aos::cm::imagemanager::ImageMangerItf"] { - <> - } - - class ItemStatusProviderItf ["aos::cm::imagemanager::ItemStatusProviderItf"] { - <> - } - - class ItemInfoProviderItf ["aos::cm::imagemanager::ItemInfoProviderItf"] { - <> + class CommonImageManager ["aos::common::imagemanager::ImageManager"] { } - class ItemRemoverItf ["aos::spaceallocator::ItemRemoverItf"] { + class ImageManagerItf ["aos::cm::imagemanager::ImageManagerItf"] { <> } - class StorageItf ["aos::cm::imagemanager::StorageItf"] { + class BlobInfoProviderItf ["aos::common::imagemanager::BlobInfoProviderItf"] { <> } - class BlobInfoProviderItf ["aos::cm::imagemanager::BlobInfoProviderItf"] { + class StorageItf ["aos::common::imagemanager::StorageItf"] { <> } @@ -56,7 +60,7 @@ classDiagram <> } - class FileServerItf ["aos::cm::fileserver::FileServerItf"] { + class DownloaderItf ["aos::downloader::DownloaderItf"] { <> } @@ -64,152 +68,85 @@ classDiagram <> } - class FileInfoProviderItf ["aos::fs::FileInfoProviderItf"] { - <> - } - - class OCISpecItf ["aos::ocispec::OCISpecItf"] { - <> - } - - class GIDPoolValidator ["aos::cm::imagemanager::GIDPool::Validator"] { + class FileServerItf ["aos::cm::fileserver::FileServerItf"] { <> } - ImageManagerItf --|> ItemStatusProviderItf - + ImageManager --|> CommonImageManager ImageManager ..|> ImageManagerItf - ImageManager ..|> ItemInfoProviderItf - ImageManager ..|> ItemRemoverItf + ImageManager ..|> BlobInfoProviderItf - ImageManager ..> StorageItf ImageManager ..> BlobInfoProviderItf + ImageManager ..> StorageItf ImageManager ..> SpaceAllocatorItf - ImageManager ..> FileServerItf + ImageManager ..> DownloaderItf ImageManager ..> CryptoHelperItf - ImageManager ..> FileInfoProviderItf - ImageManager ..> OCISpecItf - ImageManager ..> GIDPoolValidator + ImageManager ..> FileServerItf ``` ## Overview -Aos update items consists of set of blobs according to -[OCI specification](https://github.com/opencontainers/image-spec/blob/main/image-layout.md). -Image manager puts all blobs into the install folder with corresponding to digest name: - -```console -./blobs/sha256/3588d02542238316759cbf24502f4344ffcc8a60c803870022f335d1390c13b4 -./blobs/sha256/4b0bc1c4050b03c95ef2a8e36e25feac42fd31283e8c30b3ee5df6b043155d3c -./blobs/sha256/7968321274dc6b6171697c33df7815310468e694ac5be0ec03ff053bb135e768 -``` - -Unlike OCI specification, Aos image manger puts `index.json` as separate blob into the install folder together with -other blobs. - -To assemble the update item, image manger store the index file digest in its internal storage. It allows to retrieve -whole update item layers chain by reading the index file. - -## Initialization - -During initialization: - -* creates install directory if it doesn't exist; -* verifies integrity of each update item and removes corrupted or not fully installed items; -* removes outdated items; -* remove orphaned blobs (blobs that are not referenced by any update item); -* mark deleted items as outdated to space allocator; - -## Removing outdated items - -Image manager has configuration parameter that indicates how long removed update items should be stored on file system. -If update item is in deleted state more than configured timeout, this item should be completely removed from the unit. - -For this purpose, image manager checks and removes outdated update items during initialization and also performs -periodic check for outdated items (with subsequent removal). After removing some update items from the system, image -manager removes orphaned blobs. - -## aos::cm::imagemanager::ImageMangerItf - -### DownloadUpdateItems - -Downloads update items and set them to pending state. - -Downloading procedure consists of the following steps: - -* cleanup downloading items - removes update items that are in downloading or pending state and that are not listed in - the current download request; -* cleanup downloading blobs - cleanups previously downloading blobs that are not listed in the current download request; -* remove orphaned blobs - removes blobs that are not referenced by any item; -* update existing item states - existing installed update items that have all valid blobs remains in installed state. - Other update items that have valid blobs are set to pending state. For newly create update items dedicated group ID - (GID) should be selected. And remaining update items are set to downloading state; -* download missing or corrupted blobs - downloads, validates and decrypts blobs that are missing or corrupted; -* update downloading items state - after successful blobs download, downloading items that have valid blobs are set to - pending state. If blob downloading fails or any other error occurs, the corresponding update items are set to failed - state. - -### InstallUpdateItems - -Installs update items in parallel. - -This method performs the following actions: - -* removes existing different version of the specified update items in order to have only one update item version on the - system; -* verifies blobs integrity for each specified update items. Removes the item if checking integrity fails and return - failed state for this item; -* sets specified update items with valid blobs state to installed; -* sets existing and not specified items state to removed state; -* removes orphaned blobs. - -### Cancel - -Cancels ongoing operation download or install. - -This methods terminates currant download or install operation. Items that are not properly downloaded or installed -should be set to failed status with canceled error. - -## aos::cm::imagemanager::ItemStatusProviderItf - -### GetUpdateItemsStatuses - -Returns all update items statuses. - -### SubscribeListener - -Subscribes to changing update item statuses. - -### UnsubscribeListener +The blobs of the update items are stored by the common image manager as `/blobs//`. The +root blob of a CM item is the OCI image index, as the CM manages the images of several architectures. The layers are +kept as packed blobs: the CM doesn't unpack them. -Unsubscribes from changing update item statuses. +The blobs are downloaded to the temporary files `//` on the download partition, and +then decrypted or copied to the image storage. The temporary files are kept after the download, so an interrupted +download is resumed and a blob is not downloaded again if it is needed again, for example to repair the blob in the +image storage. The temporary files are removed by the download space allocator when the download space is needed. -## aos::cm::imagemanager::ItemStatusListenerItf +The CM update manager calls `SetCerts` with the certificates of the desired status, `InstallItem` for each item of the +desired status and `RemoveItem` for each stored item that is not in the desired status. Up to `cMaxNumConcurrentItems` +items are installed or removed at the same time. -### OnItemStatusChanged +## Install item -This method is called for each registered listener when update item status is changed. +`InstallItem` converts the update item info to the common install item info with the image index as the root blob and +calls `InstallItem` of the common image manager, which processes the blobs of the item and calls `DownloadBlob` for +each blob that is missing or invalid. -### OnUpdateItemRemoved +`DownloadBlob`: -This method is called for each registered listener when specified update item is completely removed from file system. -It is required to perform cleanup operation by other modules e.g. remove associated instances etc. -It is not called when update item is set to removed state. +* restores the temporary file of the blob from the outdated items of the download space allocator, see + [Download space](#download-space); +* removes the temporary file if it is larger than the blob size and frees its space; a complete temporary file is + reused without downloading; +* allocates the space of the rest of the temporary file in the download space allocator and downloads the blob with + all its URLs. The download is canceled through the cancel context of the common image manager; +* on download failure or cancel, the allocated space is resized to the downloaded size and accepted, so the download is + resumed by the next `DownloadBlob` of the blob; +* validates the size and, if provided, the SHA3-256 checksum of the temporary file; +* decrypts the temporary file to the blob path if the decrypt info is set, otherwise copies it; +* verifies the signature of the blob with the certificates set by `SetCerts` if the sign info is set; +* removes the temporary file and frees its space if the checksum validation, the decryption or the signature + verification fails, as the downloaded content is broken. -## aos::cm::imagemanager::ItemInfoProviderItf +The space of the blob in the image storage is allocated and the blob is validated by the common image manager. -### GetIndexDigest +## Download space -Returns index digest of update item. It returns index file digest for installed or pending items. +The temporary files of the blobs that are not being downloaded are registered as outdated items of the download space +allocator, identified by the blob digest with an empty version, so they are removed when the download space is needed. +The item remover of the download space allocator is returned by `GetDownloadItemRemover`: it removes the temporary +file of the digest and returns its size. -### GetBlobPath +When `DownloadBlob` starts, it restores the temporary file of the blob from the outdated items, so the space +allocation doesn't remove it. If no other blob is being downloaded, it also registers the temporary files found in the +download directory with their modification time as the timestamp, which registers the files left by a previous run, +and removes the files that don't match a valid digest. -Returns blob path by digest. +When `DownloadBlob` ends and the temporary file is kept, the modification time of the file is updated and the file is +added to the outdated items with it, so the least recently used temporary files are removed first. -### GetBlobURL +## Cancel -Returns blob URL by digest. +`Cancel` cancels all items being installed and waits until all `InstallItem` and `RemoveItem` calls in progress +return. It is implemented by the common image manager: the downloads in progress are canceled through the cancel +context passed to the downloader. -### GetItemCurrentVersion +## Blob info for SM -Return version of installed item. +`GetBlobInfo` provides the download info of a stored blob to the SM image managers: the digest, the size of the blob +and its URL translated from the local path by the file server. The checksum is not provided, as the SM image manager +validates the downloaded blob against its digest. The blob is not locked while the SM downloads it. `eNotFound` is +returned if the blob is not referenced by any stored item. diff --git a/src/core/cm/imagemanager/itf/blobinfoprovider.hpp b/src/core/cm/imagemanager/itf/blobinfoprovider.hpp deleted file mode 100644 index 4d5d84816..000000000 --- a/src/core/cm/imagemanager/itf/blobinfoprovider.hpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_CM_IMAGEMANAGER_ITF_BLOBINFOPROVIDER_HPP_ -#define AOS_CORE_CM_IMAGEMANAGER_ITF_BLOBINFOPROVIDER_HPP_ - -#include - -namespace aos::cm::imagemanager { - -/** @addtogroup cm Communication Manager - * @{ - */ - -/** - * Interface that provides blob info. - */ -class BlobInfoProviderItf { -public: - /** - * Destructor. - */ - virtual ~BlobInfoProviderItf() = default; - - /** - * Returns blobs info. - * - * @param digests list of blob digests. - * @param[out] blobsInfo blobs infos. - * @return Error. - */ - virtual Error GetBlobsInfos(const Array>& digests, Array& blobsInfo) = 0; -}; - -/** @}*/ - -} // namespace aos::cm::imagemanager - -#endif diff --git a/src/core/cm/imagemanager/itf/imagemanager.hpp b/src/core/cm/imagemanager/itf/imagemanager.hpp index e5c26ca4a..1c6c46402 100644 --- a/src/core/cm/imagemanager/itf/imagemanager.hpp +++ b/src/core/cm/imagemanager/itf/imagemanager.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 EPAM Systems, Inc. + * Copyright (C) 2026 EPAM Systems, Inc. * * SPDX-License-Identifier: Apache-2.0 */ @@ -10,9 +10,6 @@ #include #include -#include "iteminfoprovider.hpp" -#include "itemstatusprovider.hpp" - namespace aos::cm::imagemanager { /** @addtogroup cm Communication Manager @@ -22,7 +19,7 @@ namespace aos::cm::imagemanager { /** * Interface that manages update items images. */ -class ImageManagerItf : public ItemStatusProviderItf { +class ImageManagerItf { public: /** * Destructor. @@ -30,30 +27,35 @@ class ImageManagerItf : public ItemStatusProviderItf { virtual ~ImageManagerItf() = default; /** - * Downloads update items. + * Sets certificates and certificate chains used to verify blob signatures. * - * @param itemsInfo update items info. * @param certificates list of certificates. * @param certificateChains list of certificate chains. - * @param[out] statuses update items statuses. * @return Error. */ - virtual Error DownloadUpdateItems(const Array& itemsInfo, - const Array& certificates, - const Array& certificateChains, Array& statuses) + virtual Error SetCerts(const Array& certificates, + const Array& certificateChains) = 0; /** - * Activate update items. + * Installs update item. + * + * @param itemInfo update item info. + * @return Error. + */ + virtual Error InstallItem(const UpdateItemInfo& itemInfo) = 0; + + /** + * Removes update item. * - * @param ids update items IDs. - * @param[out] statuses update items statuses. + * @param itemID item ID. + * @param version item version. * @return Error. */ - virtual Error InstallUpdateItems(const Array& itemsInfo, Array& statuses) = 0; + virtual Error RemoveItem(const String& itemID, const String& version) = 0; /** - * Cancels ongoing operations: download or install. + * Cancels all items being installed. Waits until all InstallItem and RemoveItem calls in progress return. * * @return Error. */ diff --git a/src/core/cm/imagemanager/itf/iteminfoprovider.hpp b/src/core/cm/imagemanager/itf/iteminfoprovider.hpp deleted file mode 100644 index 3bec5ea1b..000000000 --- a/src/core/cm/imagemanager/itf/iteminfoprovider.hpp +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_CM_IMAGEMANAGER_ITF_ITEMINFOPROVIDER_HPP_ -#define AOS_CORE_CM_IMAGEMANAGER_ITF_ITEMINFOPROVIDER_HPP_ - -#include - -namespace aos::cm::imagemanager { - -/** - * Interface to provide update item info. - */ -class ItemInfoProviderItf { -public: - /** - * Destructor. - */ - virtual ~ItemInfoProviderItf() = default; - - /** - * Returns update item index digest. - * - * @param itemID update item ID. - * @param version update item version. - * @param[out] digest result item digest. - * @return Error. - */ - virtual Error GetIndexDigest(const String& itemID, const String& version, String& digest) const = 0; - - /** - * Returns blob path by its digest. - * - * @param digest blob digest. - * @param[out] path result blob path. - * @return Error. - */ - virtual Error GetBlobPath(const String& digest, String& path) const = 0; - - /** - * Returns blob URL by its digest. - * - * @param digest blob digest. - * @param[out] url result blob URL. - * @return Error. - */ - virtual Error GetBlobURL(const String& digest, String& url) const = 0; - - /** - * Returns item current version. - * - * @param itemID update item ID. - * @param[out] version result item version. - * @return Error. - */ - virtual Error GetItemCurrentVersion(const String& itemID, String& version) const = 0; -}; - -} // namespace aos::cm::imagemanager - -#endif diff --git a/src/core/cm/imagemanager/itf/itemstatusprovider.hpp b/src/core/cm/imagemanager/itf/itemstatusprovider.hpp deleted file mode 100644 index b20d90902..000000000 --- a/src/core/cm/imagemanager/itf/itemstatusprovider.hpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_CM_IMAGEMANAGER_ITF_ITEMSTATUSPROVIDER_HPP_ -#define AOS_CORE_CM_IMAGEMANAGER_ITF_ITEMSTATUSPROVIDER_HPP_ - -#include - -namespace aos::cm::imagemanager { - -/** - * Interface for receiving notification about update image statuses. - */ -class ItemStatusListenerItf { -public: - /** - * Destructor. - */ - virtual ~ItemStatusListenerItf() = default; - - /** - * Notifies about items statuses change. - * - * @param statuses update items statuses. - */ - virtual void OnItemsStatusesChanged(const Array& statuses) = 0; - - /** - * Notifies about item removal. - * - * @param id removed item id. - */ - virtual void OnItemRemoved(const String& id) = 0; -}; - -/** - * Interface to provide update item statuses. - */ -class ItemStatusProviderItf { -public: - /** - * Destructor. - */ - virtual ~ItemStatusProviderItf() = default; - - /** - * Retrieves update items statuses. - * - * @param[out] statuses list of update items statuses. - * @return Error. - */ - virtual Error GetUpdateItemsStatuses(Array& statuses) = 0; - - /** - * Subscribes status notifications. - * - * @param listener status listener. - * @return Error. - */ - virtual Error SubscribeListener(ItemStatusListenerItf& listener) = 0; - - /** - * Unsubscribes from status notifications. - * - * @param listener status listener. - * @return Error. - */ - virtual Error UnsubscribeListener(ItemStatusListenerItf& listener) = 0; -}; - -} // namespace aos::cm::imagemanager - -#endif diff --git a/src/core/cm/imagemanager/itf/storage.hpp b/src/core/cm/imagemanager/itf/storage.hpp deleted file mode 100644 index 4c075ed88..000000000 --- a/src/core/cm/imagemanager/itf/storage.hpp +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_CM_IMAGEMANAGER_ITF_STORAGE_HPP_ -#define AOS_CORE_CM_IMAGEMANAGER_ITF_STORAGE_HPP_ - -#include -#include - -namespace aos::cm::imagemanager { - -/** - * Update item info. - */ -struct ItemInfo { - StaticString mItemID; - UpdateItemType mType; - StaticString mVersion; - StaticString mIndexDigest; - ItemState mState {}; - Time mTimestamp {}; - - /** - * Compares update item info. - * - * @param rhs update item info to compare with. - * @return bool. - */ - friend bool operator==(const ItemInfo& lhs, const ItemInfo& rhs) - { - return lhs.mItemID == rhs.mItemID && lhs.mVersion == rhs.mVersion && lhs.mIndexDigest == rhs.mIndexDigest - && lhs.mState == rhs.mState && lhs.mTimestamp == rhs.mTimestamp; - }; - - /** - * Compares update item info. - * - * @param rhs update item info to compare with. - * @return bool. - */ - friend bool operator!=(const ItemInfo& lhs, const ItemInfo& rhs) { return !(lhs == rhs); }; -}; - -/** - * Storage interface. - */ -class StorageItf { -public: - /** - * Destructor. - */ - virtual ~StorageItf() = default; - - /** - * Adds item. - * - * @param item Item info. - * @return Error. - */ - virtual Error AddItem(const ItemInfo& item) = 0; - - /** - * Removes item. - * - * @param id ID. - * @param version Version. - * @return Error. - */ - virtual Error RemoveItem(const String& id, const String& version) = 0; - - /** - * Updates item state. - * - * @param id ID. - * @param version Version. - * @param state Item state. - * @param timestamp Timestamp (optional). - * @return Error. - */ - virtual Error UpdateItemState(const String& id, const String& version, ItemState state, Time timestamp = {}) = 0; - - /** - * Gets all items info. - * - * @param items Items info. - * @return Error. - */ - virtual Error GetAllItemsInfos(Array& items) = 0; - - /** - * Gets items info by ID. - * - * @param id Item ID. - * @param items Items info. - * @return Error. - */ - virtual Error GetItemInfos(const String& id, Array& items) = 0; -}; - -} // namespace aos::cm::imagemanager - -#endif diff --git a/src/core/cm/imagemanager/tests/CMakeLists.txt b/src/core/cm/imagemanager/tests/CMakeLists.txt index 6af0c8386..a4cb6164e 100644 --- a/src/core/cm/imagemanager/tests/CMakeLists.txt +++ b/src/core/cm/imagemanager/tests/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (C) 2025 EPAM Systems, Inc. +# Copyright (C) 2026 EPAM Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -16,7 +16,9 @@ set(SOURCES imagemanager.cpp) # Libraries # ###################################################################################################################### -set(LIBRARIES aos::core::cm::imagemanager GTest::gmock_main) +set(LIBRARIES aos::core::cm::imagemanager aos::core::cm::tests::mocks aos::core::common::tests::mocks + aos::core::common::tests::utils GTest::gmock_main +) # ###################################################################################################################### # Target diff --git a/src/core/cm/imagemanager/tests/imagemanager.cpp b/src/core/cm/imagemanager/tests/imagemanager.cpp index a8c9203f4..0c5bb0fdc 100644 --- a/src/core/cm/imagemanager/tests/imagemanager.cpp +++ b/src/core/cm/imagemanager/tests/imagemanager.cpp @@ -1,2463 +1,1686 @@ /* - * Copyright (C) 2025 EPAM Systems, Inc. + * Copyright (C) 2026 EPAM Systems, Inc. * * SPDX-License-Identifier: Apache-2.0 */ -#include -#include - +#include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include -#include #include +#include +#include +#include #include #include #include +#include +#include #include #include #include +#include #include -#include "mocks/blobinfoprovidermock.hpp" -#include "mocks/itemstatuslistenermock.hpp" -#include "mocks/storagemock.hpp" +#include using namespace testing; namespace aos::cm::imagemanager { +namespace { + /*********************************************************************************************************************** - * Suite + * Consts **********************************************************************************************************************/ -class ImageManagerTest : public testing::Test { -protected: - static void SetUpTestSuite() - { - tests::utils::InitLog(); - - LOG_INF() << "Image manager size" << Log::Field("size", sizeof(ImageManager)); - } - - void SetUp() override - { - mConfig.mImagePath = "/tmp/imagemanager_test/install"; - mConfig.mDownloadPath = "/tmp/imagemanager_test/download"; - mConfig.mUpdateItemTTL = Time::cSeconds * 10; - mConfig.mRemoveOutdatedPeriod = Time::cSeconds * 20; - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).WillRepeatedly(Return(ErrorEnum::eNone)); - - EXPECT_TRUE(mImageManager - .Init(mAllocator, mConfig, mStorageMock, mBlobInfoProviderMock, mDownloadingSpaceAllocatorMock, - mInstallSpaceAllocatorMock, mDownloaderMock, mFileServerMock, mCryptoHelperMock, - mFileInfoProviderMock, mOCISpecMock) - .IsNone()); - } - - void TearDown() override - { - fs::RemoveAll(mConfig.mImagePath); - fs::RemoveAll(mConfig.mDownloadPath); - } - - static constexpr auto cBlobDigest = "sha256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; - static constexpr auto cBlobHash = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; - - StaticString GetBlobDownloadPath() const - { - return fs::JoinPath(fs::JoinPath(mConfig.mDownloadPath, "blobs/sha256"), cBlobHash); - } - - static void CreateFile(const String& path, size_t size) - { - StaticString dir; - - ASSERT_TRUE(fs::ParentPath(path, dir).IsNone()); - ASSERT_TRUE(fs::MakeDirAll(dir).IsNone()); - - std::ofstream file(path.CStr(), std::ios::binary | std::ios::trunc); - ASSERT_TRUE(file.is_open()); - - if (size > 0) { - file.seekp(static_cast(size) - 1); - file.put('\0'); - } - } - - void ExpectSingleBlobItem(size_t blobSize, int getAllItemsCalls) - { - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(getAllItemsCalls).WillRepeatedly(Return(ErrorEnum::eNone)); - EXPECT_CALL(mStorageMock, AddItem(_)).WillOnce(Return(ErrorEnum::eNone)); - - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)) - .WillOnce(Invoke([blobSize](const auto& digests, Array& blobsInfo) { - BlobInfo info; - info.mDigest = digests[0]; - info.mSize = blobSize; - info.mURLs.PushBack("http://test.com/blob"); - info.mDecryptInfo.EmplaceValue(); - info.mSignInfo.EmplaceValue(); - - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mSHA256.PushBack(static_cast(i)); - } - - blobsInfo.PushBack(info); - - return ErrorEnum::eNone; - })); - } - - void ExpectBlobChecksum(int times) - { - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .Times(times) - .WillRepeatedly(Invoke([](const String&, fs::FileInfo& info, crypto::Hash) { - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mCheckSum.PushBack(static_cast(i)); - } - - return ErrorEnum::eNone; - })); - } - - RetWithError> MakeSpaceMock(int acceptTimes, int releaseTimes) - { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); - - EXPECT_CALL(*space, Accept()).Times(acceptTimes); - EXPECT_CALL(*space, Release()).Times(releaseTimes); - - return {std::move(space), ErrorEnum::eNone}; - } - - void ExpectFileRemoved(const String& path) const - { - auto [exists, err] = fs::FileExist(path); - - EXPECT_TRUE(err.IsNone()); - EXPECT_FALSE(exists); - } - - void ExpectFileSize(const String& path, size_t size) - { - auto [fileSize, err] = fs::CalculateSize(mAllocator, path); - - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(fileSize, size); - } - - // mAllocator must be declared (and therefore destroyed) after any member that allocates from it, since - // members are destroyed in reverse declaration order. - HeapAllocator mAllocator; - - Config mConfig; - ImageManager mImageManager; - StrictMock mStorageMock; - StrictMock mBlobInfoProviderMock; - StrictMock mDownloadingSpaceAllocatorMock; - StrictMock mInstallSpaceAllocatorMock; - StrictMock mDownloaderMock; - StrictMock mFileServerMock; - StrictMock mCryptoHelperMock; - StrictMock mFileInfoProviderMock; - StrictMock mSpaceMock; - StrictMock mOCISpecMock; -}; +constexpr auto cTestDir = "/tmp/cm_imagemanager_test"; +constexpr auto cTestImagePath = "/tmp/cm_imagemanager_test/images"; +constexpr auto cTestDownloadPath = "/tmp/cm_imagemanager_test/downloads"; +constexpr auto cTestItemID = "item1"; +constexpr auto cTestItemID2 = "item2"; +constexpr auto cTestVersion = "1.0.0"; +constexpr auto cImageConfigType = "application/vnd.oci.image.config.v1+json"; +constexpr auto cTestLayerType = "application/vnd.oci.image.layer.v1.tar+gzip"; +constexpr auto cTestDiffID = "sha256:00000000000000000000000000000000000000000000000000000000000000ff"; +constexpr auto cTestDiffID2 = "sha256:00000000000000000000000000000000000000000000000000000000000000fe"; +constexpr auto cLeftoverHash1 = "1111111111111111111111111111111111111111111111111111111111111111"; +constexpr auto cLeftoverHash2 = "2222222222222222222222222222222222222222222222222222222222222222"; +constexpr auto cPastTime = 1000000000; +constexpr auto cItemCacheTTL = 10 * Time::cSeconds; +constexpr auto cWaitTimeout = std::chrono::seconds(5); +constexpr auto cDownloadTimeout = 5 * Time::cSeconds; /*********************************************************************************************************************** - * Tests + * Static **********************************************************************************************************************/ -TEST_F(ImageManagerTest, DownloadUpdateItems_EmptyList) +std::string ToHex(const std::vector& data) { - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; + std::string hex; - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); + for (const auto byte : data) { + char buf[3]; - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + std::snprintf(buf, sizeof(buf), "%02x", byte); + hex += buf; + } - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(statuses.Size(), 0); + return hex; } -TEST_F(ImageManagerTest, DownloadUpdateItems_EmptyList_RemovesPendingItems) +std::string ReadFile(const std::string& path) { - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(2).WillRepeatedly(Invoke([](Array& items) { - ItemInfo pendingItem1; - pendingItem1.mItemID = "service1"; - pendingItem1.mType = UpdateItemTypeEnum::eService; - pendingItem1.mVersion = "1.0.0"; - pendingItem1.mIndexDigest = "sha256:abc123"; - pendingItem1.mState = ItemStateEnum::ePending; - items.PushBack(pendingItem1); - - ItemInfo pendingItem2; - pendingItem2.mItemID = "service2"; - pendingItem2.mType = UpdateItemTypeEnum::eService; - pendingItem2.mVersion = "2.0.0"; - pendingItem2.mIndexDigest = "sha256:def456"; - pendingItem2.mState = ItemStateEnum::ePending; - items.PushBack(pendingItem2); - - ItemInfo installedItem; - installedItem.mItemID = "service3"; - installedItem.mType = UpdateItemTypeEnum::eService; - installedItem.mVersion = "3.0.0"; - installedItem.mIndexDigest = "sha256:ghi789"; - installedItem.mState = ItemStateEnum::eInstalled; - items.PushBack(installedItem); + std::ifstream file(path, std::ios::binary); + std::stringstream content; - return ErrorEnum::eNone; - })); + content << file.rdbuf(); - EXPECT_CALL(mStorageMock, RemoveItem(_, _)) - .Times(2) - .WillOnce(Invoke([](const String& itemID, const String& version) { - EXPECT_EQ(itemID, "service1"); - EXPECT_EQ(version, "1.0.0"); - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](const String& itemID, const String& version) { - EXPECT_EQ(itemID, "service2"); - EXPECT_EQ(version, "2.0.0"); - return ErrorEnum::eNone; - })); - - ItemStatusListenerMock listener; - EXPECT_CALL(listener, OnItemsStatusesChanged(_)) - .Times(2) - .WillRepeatedly(Invoke([](const Array& statuses) { - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::eRemoved); - EXPECT_TRUE(statuses[0].mError.IsNone()); - })); - - EXPECT_TRUE(mImageManager.SubscribeListener(listener).IsNone()); + return content.str(); +} - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); +void WriteFile(const std::string& path, const std::string& content, bool append = false) +{ + auto err = fs::MakeDirAll(fs::Dir(path.c_str())); + ASSERT_TRUE(err.IsNone()) << "Failed to create dir: " << tests::utils::ErrorToStr(err); - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 2); - EXPECT_EQ(statuses[0].mItemID, "service1"); - EXPECT_EQ(statuses[0].mType, UpdateItemTypeEnum::eService); - EXPECT_EQ(statuses[0].mVersion, "1.0.0"); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::eRemoved); - EXPECT_TRUE(statuses[0].mError.IsNone()); - EXPECT_EQ(statuses[1].mItemID, "service2"); - EXPECT_EQ(statuses[1].mType, UpdateItemTypeEnum::eService); - EXPECT_EQ(statuses[1].mVersion, "2.0.0"); - EXPECT_EQ(statuses[1].mState, ItemStateEnum::eRemoved); - EXPECT_TRUE(statuses[1].mError.IsNone()); + std::ofstream file(path, std::ios::binary | (append ? std::ios::app : std::ios::trunc)); - EXPECT_TRUE(mImageManager.UnsubscribeListener(listener).IsNone()); + file << content; } -TEST_F(ImageManagerTest, DownloadUpdateItems_Success_NewItem) +bool FileExists(const std::string& path) { - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; + auto [exists, err] = fs::FileExist(path.c_str()); + EXPECT_TRUE(err.IsNone()) << "Failed to check file: " << tests::utils::ErrorToStr(err); - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:abc123"; - itemsInfo.PushBack(item); + return exists; +} - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(3).WillRepeatedly(Return(ErrorEnum::eNone)); +void SetModifyTime(const std::string& path, time_t sec) +{ + auto err = fs::SetFileMTime(path.c_str(), Time::Unix(sec)); + ASSERT_TRUE(err.IsNone()) << "Failed to set modify time: " << tests::utils::ErrorToStr(err); +} - EXPECT_CALL(mStorageMock, AddItem(_)).WillOnce(Return(ErrorEnum::eNone)); +Time GetModifyTime(const std::string& path) +{ + auto [modifyTime, err] = fs::GetFileMTime(path.c_str()); + EXPECT_TRUE(err.IsNone()) << "Failed to get modify time: " << tests::utils::ErrorToStr(err); - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)) - .WillRepeatedly(Invoke([](const auto&, Array& blobsInfo) { - BlobInfo info; - info.mDigest = "sha256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; - info.mSize = 1024; - info.mURLs.PushBack("http://test.com/blob"); - info.mDecryptInfo.EmplaceValue(); - info.mSignInfo.EmplaceValue(); + return modifyTime; +} - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mSHA256.PushBack(static_cast(i)); - } +bool IsNow(const Time& time) +{ + return std::llabs(Time::Now().Sub(time).Nanoseconds()) <= (5 * Time::cSeconds).Nanoseconds(); +} - blobsInfo.PushBack(info); +/** + * Test blob. + */ +struct TestBlob { + std::string mContent; + std::string mDigest; + std::string mPath; + std::string mDownloadPath; +}; - return ErrorEnum::eNone; - })); +/** + * Space event. + */ +struct SpaceEvent { + size_t mAllocatedSize {}; + std::string mName; + size_t mArg {}; - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(AtLeast(0)); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + bool operator==(const SpaceEvent& rhs) const + { + return mAllocatedSize == rhs.mAllocatedSize && mName == rhs.mName && mArg == rhs.mArg; + } +}; - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); +std::ostream& operator<<(std::ostream& os, const SpaceEvent& event) +{ + return os << "{" << event.mAllocatedSize << ", " << event.mName << ", " << event.mArg << "}"; +} - return {std::move(space), ErrorEnum::eNone}; - })); +/** + * Outdated download item. + */ +struct OutdatedItem { + std::string mDigest; + Time mTimestamp; + size_t mSpaceEventIndex {}; +}; - EXPECT_CALL(mInstallSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); +} // namespace - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); +/*********************************************************************************************************************** + * Suite + **********************************************************************************************************************/ - return {std::move(space), ErrorEnum::eNone}; - })); +class CMImageManagerTest : public Test { +protected: + static void SetUpTestSuite() { tests::utils::InitLog(); } - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + void SetUp() override + { + auto err = fs::RemoveAll(cTestDir); + ASSERT_TRUE(err.IsNone()) << "Failed to remove test dir: " << tests::utils::ErrorToStr(err); - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"; - index.mManifests.PushBack(manifest); + ASSERT_TRUE(mCryptoProvider.Init(mAllocator).IsNone()); + ASSERT_TRUE(mFileInfoProviderImpl.Init(mAllocator, mCryptoProvider).IsNone()); - return ErrorEnum::eNone; - })); + mLayer = AddBlob("layer blob content", {"https://example.com/layer1", "https://example.com/layer2"}); + mConfig = AddBlob("image config blob content"); + mManifest = AddBlob("image manifest blob content: manifest"); + mIndex = AddBlob("image index blob content: index of manifests"); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:config1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; + SetUpOCISpec(); - oci::ContentDescriptor layer; - layer.mDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - manifest.mLayers.PushBack(layer); + ON_CALL(mFileInfoProvider, GetFileInfo(_, _, _)) + .WillByDefault(Invoke([this](const String& path, fs::FileInfo& info, const crypto::Hash& hashAlg) { + return mFileInfoProviderImpl.GetFileInfo(path, info, hashAlg); + })); + ON_CALL(mBlobInfoProvider, GetBlobInfo(_, _)) + .WillByDefault(Invoke([this](const String& digest, BlobInfo& blobInfo) -> Error { + std::lock_guard lock {mMutex}; - return ErrorEnum::eNone; - })); + auto it = mBlobInfos.find(digest.CStr()); + if (it == mBlobInfos.end()) { + return ErrorEnum::eNotFound; + } - EXPECT_CALL(mCryptoHelperMock, Decrypt(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); - EXPECT_CALL(mCryptoHelperMock, ValidateSigns(_, _, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + blobInfo = *it->second; - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([](const String&, fs::FileInfo& info, crypto::Hash) { - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mCheckSum.PushBack(static_cast(i)); - } + return ErrorEnum::eNone; + })); + ON_CALL(mDownloader, Download(_, _, _, _)) + .WillByDefault(Invoke([this](CancelContext&, const String& digest, const Array>&, + const String& path) { return DownloadContent(digest, path); })); + ON_CALL(mCryptoHelper, Decrypt(_, _, _)) + .WillByDefault( + Invoke([this](const String& encryptedPath, const String& decryptedPath, const crypto::DecryptInfo&) { + return fs::CopyFile(mAllocator, encryptedPath, decryptedPath); + })); + ON_CALL(mSpaceAllocator, AllocateSpace(_)) + .WillByDefault(Invoke([this](size_t) -> RetWithError> { + auto space = MakeUnique>(&mAllocator); + + return {Move(space), ErrorEnum::eNone}; + })); + ON_CALL(mDownloadSpaceAllocator, AllocateSpace(_)) + .WillByDefault(Invoke([this](size_t size) -> RetWithError> { + return {CreateDownloadSpace(size), ErrorEnum::eNone}; + })); + ON_CALL(mDownloadSpaceAllocator, AddOutdatedItem(_, _, _)) + .WillByDefault(Invoke([this](const String& id, const String&, const Time& timestamp) { + std::lock_guard lock {mMutex}; - info.mSize = 1024; + mOutdatedItems.push_back({id.CStr(), timestamp, mSpaceEvents.size()}); - return ErrorEnum::eNone; - })); + return ErrorEnum::eNone; + })); + ON_CALL(mListener, OnItemStatusChanged(_)).WillByDefault(Invoke([this](const UpdateItemStatus& status) { + std::lock_guard lock {mMutex}; - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::ePending), _)) - .WillOnce(Return(ErrorEnum::eNone)); - - ItemStatusListenerMock listener; - ItemStateEnum lastState = ItemStateEnum::eDownloading; - EXPECT_CALL(listener, OnItemsStatusesChanged(_)) - .Times(AtLeast(1)) - .WillRepeatedly(Invoke([&item, &lastState](const Array& statuses) { - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, item.mItemID); - EXPECT_EQ(statuses[0].mVersion, item.mVersion); - lastState = statuses[0].mState; + mStates.push_back(status.mState.GetValue()); })); - EXPECT_TRUE(mImageManager.SubscribeListener(listener).IsNone()); + EXPECT_CALL(mDownloadSpaceAllocator, RestoreOutdatedItem(_, String(""))).Times(AnyNumber()); - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + Config config; - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, item.mItemID); - EXPECT_EQ(statuses[0].mType, item.mType); - EXPECT_EQ(statuses[0].mVersion, item.mVersion); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::ePending); - EXPECT_EQ(lastState, ItemStateEnum::ePending); + config.mImagePath = cTestImagePath; + config.mItemCacheTTL = cItemCacheTTL; + config.mRemoveOutdatedPeriod = 5 * Time::cSeconds; + config.mDownloadPath = cTestDownloadPath; - EXPECT_TRUE(mImageManager.UnsubscribeListener(listener).IsNone()); -} + err = mImageManager.Init(config, mBlobInfoProvider, mStorage, mSpaceAllocator, mDownloadSpaceAllocator, + mDownloader, mCryptoHelper, mFileServer, mFileInfoProvider, mOCISpec, mFSPlatform, mAllocator); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); -TEST_F(ImageManagerTest, DownloadUpdateItems_AlreadyInstalled) -{ - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:abc123"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(3).WillRepeatedly(Invoke([&](Array& items) { - ItemInfo storedItem; - storedItem.mItemID = item.mItemID; - storedItem.mVersion = item.mVersion; - storedItem.mIndexDigest = item.mIndexDigest; - storedItem.mState = ItemStateEnum::eInstalled; - items.PushBack(storedItem); + ASSERT_TRUE(mImageManager.SubscribeListener(mListener).IsNone()); + } - return ErrorEnum::eNone; - })); + void TearDown() override + { + auto err = mImageManager.Stop(); + EXPECT_TRUE(err.IsNone()) << "Failed to stop image manager: " << tests::utils::ErrorToStr(err); - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)) - .WillRepeatedly(Invoke([](const auto&, Array& blobsInfo) { - BlobInfo info; - info.mDigest = "sha256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; - info.mSize = 1024; - info.mURLs.PushBack("http://test.com/blob"); - info.mDecryptInfo.EmplaceValue(); - info.mSignInfo.EmplaceValue(); + err = fs::RemoveAll(cTestDir); + EXPECT_TRUE(err.IsNone()) << "Failed to remove test dir: " << tests::utils::ErrorToStr(err); + } - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mSHA256.PushBack(static_cast(i)); - } + std::vector CalculateHash(const std::string& content, const crypto::Hash& alg) + { + auto [hash, err] = mCryptoProvider.CreateHash(alg); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - blobsInfo.PushBack(info); + if (!err.IsNone()) { + return {}; + } - return ErrorEnum::eNone; - })); + err = hash->Update(Array(reinterpret_cast(content.data()), content.size())); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(AtLeast(0)); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + StaticArray result; - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); + err = hash->Finalize(result); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - return {std::move(space), ErrorEnum::eNone}; - })); + return std::vector(result.begin(), result.end()); + } - EXPECT_CALL(mInstallSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + TestBlob AddBlob(const std::string& content, const std::vector& urls = {"https://example.com/blob"}) + { + const auto hash = ToHex(CalculateHash(content, crypto::HashEnum::eSHA256)); - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); + TestBlob blob; - return {std::move(space), ErrorEnum::eNone}; - })); + blob.mContent = content; + blob.mDigest = "sha256:" + hash; + blob.mPath = std::string(cTestImagePath) + "/blobs/sha256/" + hash; + blob.mDownloadPath = std::string(cTestDownloadPath) + "/sha256/" + hash; - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + auto blobInfo = std::make_shared(); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([](const String&, fs::FileInfo& info, crypto::Hash) { - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mCheckSum.PushBack(static_cast(i)); - } + blobInfo->mDigest = blob.mDigest.c_str(); + blobInfo->mSize = content.size(); - info.mSize = 1024; + for (const auto& url : urls) { + EXPECT_TRUE(blobInfo->mURLs.EmplaceBack(url.c_str()).IsNone()); + } - return ErrorEnum::eNone; - })); + std::lock_guard lock {mMutex}; - EXPECT_CALL(mCryptoHelperMock, Decrypt(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); - EXPECT_CALL(mCryptoHelperMock, ValidateSigns(_, _, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + mBlobInfos[blob.mDigest] = blobInfo; + mDownloadContents[blob.mDigest] = content; - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"; - index.mManifests.PushBack(manifest); + return blob; + } - return ErrorEnum::eNone; - })); + BlobInfo& GetBlobInfo(const TestBlob& blob) + { + std::lock_guard lock {mMutex}; - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:config1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; + return *mBlobInfos[blob.mDigest]; + } - oci::ContentDescriptor layer; - layer.mDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - manifest.mLayers.PushBack(layer); + void SetDownloadContent(const TestBlob& blob, const std::string& content) + { + std::lock_guard lock {mMutex}; - return ErrorEnum::eNone; - })); + mDownloadContents[blob.mDigest] = content; + } - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + void SetUpOCISpec() + { + AddImage(mIndex, mManifest, mConfig, mLayer, cTestDiffID); - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, item.mItemID); - EXPECT_EQ(statuses[0].mType, item.mType); - EXPECT_EQ(statuses[0].mVersion, item.mVersion); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalled); -} + ON_CALL(mOCISpec, LoadImageIndex(_, _)) + .WillByDefault(Invoke([this](const String& path, oci::ImageIndex& result) -> Error { + std::lock_guard lock {mMutex}; -TEST_F(ImageManagerTest, DownloadUpdateItems_BlobsAlreadyExistOnDisk) -{ - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:aabb"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(3).WillRepeatedly(Return(ErrorEnum::eNone)); - EXPECT_CALL(mStorageMock, AddItem(_)).WillOnce(Return(ErrorEnum::eNone)); - - auto blobsDir = fs::JoinPath(mConfig.mImagePath, "/blobs/sha256/"); - fs::MakeDirAll(blobsDir); - - auto indexPath = fs::JoinPath(blobsDir, "aabb"); - auto manifestPath = fs::JoinPath(blobsDir, "ccdd"); - auto configPath = fs::JoinPath(blobsDir, "eeff"); - auto layerPath = fs::JoinPath(blobsDir, "1122"); - - std::ofstream(indexPath.CStr()).close(); - std::ofstream(manifestPath.CStr()).close(); - std::ofstream(configPath.CStr()).close(); - std::ofstream(layerPath.CStr()).close(); - - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .Times(4) - .WillRepeatedly(Invoke([](const String& path, fs::FileInfo& info, crypto::Hash) { - size_t lastSlashPos = 0; - for (size_t i = 0; i < path.Size(); i++) { - if (path[i] == '/') { - lastSlashPos = i; + auto it = mImageIndexes.find(path.CStr()); + if (it == mImageIndexes.end()) { + return ErrorEnum::eNotFound; } - } - String digest(path.CStr() + lastSlashPos + 1); + result = *it->second; - digest.HexToByteArray(info.mCheckSum); - info.mSize = 100; + return ErrorEnum::eNone; + })); + ON_CALL(mOCISpec, LoadImageManifest(_, _)) + .WillByDefault(Invoke([this](const String& path, oci::ImageManifest& result) -> Error { + std::lock_guard lock {mMutex}; - return ErrorEnum::eNone; - })); + auto it = mImageManifests.find(path.CStr()); + if (it == mImageManifests.end()) { + return ErrorEnum::eNotFound; + } - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:ccdd"; - index.mManifests.PushBack(manifest); + result = *it->second; - return ErrorEnum::eNone; - })); + return ErrorEnum::eNone; + })); + ON_CALL(mOCISpec, LoadImageConfig(_, _)) + .WillByDefault(Invoke([this](const String& path, oci::ImageConfig& result) -> Error { + std::lock_guard lock {mMutex}; - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:eeff"; + auto it = mImageConfigs.find(path.CStr()); + if (it == mImageConfigs.end()) { + return ErrorEnum::eNotFound; + } - oci::ContentDescriptor layer; - layer.mDigest = "sha256:1122"; - manifest.mLayers.PushBack(layer); + result = *it->second; - return ErrorEnum::eNone; - })); + return ErrorEnum::eNone; + })); + } - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::ePending), _)) - .WillOnce(Return(ErrorEnum::eNone)); + void AddImage(const TestBlob& indexBlob, const TestBlob& manifestBlob, const TestBlob& configBlob, + const TestBlob& layerBlob, const char* diffID) + { + auto index = std::make_shared(); - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + index->mMediaType = oci::cMediaTypeImageIndex; - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, item.mItemID); - EXPECT_EQ(statuses[0].mType, item.mType); - EXPECT_EQ(statuses[0].mVersion, item.mVersion); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::ePending); -} + oci::IndexContentDescriptor manifestDescriptor; -TEST_F(ImageManagerTest, DownloadUpdateItems_MultipleItems_Success) -{ - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - for (int i = 0; i < 3; i++) { - UpdateItemInfo item; - item.mItemID = ("service" + std::to_string(i)).c_str(); - item.mVersion = "1.0.0"; - item.mIndexDigest = ("sha256:" + std::string(64, '1' + i)).c_str(); - itemsInfo.PushBack(item); - } + static_cast(manifestDescriptor) = oci::ContentDescriptor { + oci::cMediaTypeImageManifest, manifestBlob.mDigest.c_str(), manifestBlob.mContent.size()}; - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(3).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_TRUE(index->mManifests.PushBack(manifestDescriptor).IsNone()); - EXPECT_CALL(mStorageMock, AddItem(_)).Times(3).WillRepeatedly(Return(ErrorEnum::eNone)); + auto manifest = std::make_shared(); - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)) - .WillRepeatedly(Invoke([](const auto& digests, Array& blobsInfo) { - for (const auto& digest : digests) { - BlobInfo info; - info.mDigest = digest; - info.mSize = 1024; - info.mURLs.PushBack("http://test.com/blob"); - info.mDecryptInfo.EmplaceValue(); - info.mSignInfo.EmplaceValue(); + manifest->mMediaType = oci::cMediaTypeImageManifest; + manifest->mConfig + = oci::ContentDescriptor {cImageConfigType, configBlob.mDigest.c_str(), configBlob.mContent.size()}; - auto [colonPos, findErr] = digest.FindSubstr(0, ":"); - String hash(digest.CStr() + colonPos + 1); - hash.HexToByteArray(info.mSHA256); + EXPECT_TRUE( + manifest->mLayers + .PushBack(oci::ContentDescriptor {cTestLayerType, layerBlob.mDigest.c_str(), layerBlob.mContent.size()}) + .IsNone()); - blobsInfo.PushBack(info); - } + auto imageConfig = std::make_shared(); - return ErrorEnum::eNone; - })); + EXPECT_TRUE(imageConfig->mRootfs.mDiffIDs.EmplaceBack(diffID).IsNone()); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(AtLeast(0)); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + std::lock_guard lock {mMutex}; - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); + mImageIndexes[indexBlob.mPath] = index; + mImageManifests[manifestBlob.mPath] = manifest; + mImageConfigs[configBlob.mPath] = imageConfig; + } - return {std::move(space), ErrorEnum::eNone}; - })); + Error DownloadContent(const String& digest, const String& path) + { + std::string content; - EXPECT_CALL(mInstallSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + { + std::lock_guard lock {mMutex}; - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); + content = mDownloadContents[digest.CStr()]; + } - return {std::move(space), ErrorEnum::eNone}; - })); + const auto existing = FileExists(path.CStr()) ? ReadFile(path.CStr()) : std::string(); - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + if (existing.size() > content.size()) { + return ErrorEnum::eFailed; + } - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:4444444444444444444444444444444444444444444444444444444444444444"; - index.mManifests.PushBack(manifest); + WriteFile(path.CStr(), content.substr(existing.size()), true); return ErrorEnum::eNone; - })); + } - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:5555555555555555555555555555555555555555555555555555555555555555"; + UniquePtr CreateDownloadSpace(size_t size) + { + auto space = MakeUnique>(&mAllocator); - oci::ContentDescriptor layer; - layer.mDigest = "sha256:6666666666666666666666666666666666666666666666666666666666666666"; - manifest.mLayers.PushBack(layer); + ON_CALL(*space, Accept()).WillByDefault(Invoke([this, size]() { + AddSpaceEvent({size, "accept", 0}); return ErrorEnum::eNone; })); - - EXPECT_CALL(mCryptoHelperMock, Decrypt(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); - EXPECT_CALL(mCryptoHelperMock, ValidateSigns(_, _, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); - - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([](const String& path, fs::FileInfo& info, crypto::Hash) { - size_t lastSlashPos = 0; - for (size_t i = 0; i < path.Size(); i++) { - if (path[i] == '/') { - lastSlashPos = i; - } - } - - String hexPart(path.CStr() + lastSlashPos + 1); - hexPart.HexToByteArray(info.mCheckSum); - info.mSize = 1024; + ON_CALL(*space, Release()).WillByDefault(Invoke([this, size]() { + AddSpaceEvent({size, "release", 0}); return ErrorEnum::eNone; })); + ON_CALL(*space, Resize(_)).WillByDefault(Invoke([this, size](size_t newSize) { + AddSpaceEvent({size, "resize", newSize}); - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::ePending), _)) - .Times(3) - .WillRepeatedly(Return(ErrorEnum::eNone)); + return ErrorEnum::eNone; + })); - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + return UniquePtr(Move(space)); + } - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 3); + void AddSpaceEvent(const SpaceEvent& event) + { + std::lock_guard lock {mMutex}; - for (size_t i = 0; i < statuses.Size(); i++) { - EXPECT_EQ(statuses[i].mItemID, itemsInfo[i].mItemID); - EXPECT_EQ(statuses[i].mVersion, itemsInfo[i].mVersion); - EXPECT_EQ(statuses[i].mState, ItemStateEnum::ePending); + mSpaceEvents.push_back(event); } -} - -TEST_F(ImageManagerTest, DownloadUpdateItems_Cancel_BlobInfoFailed) -{ - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - UpdateItemInfo item; - item.mItemID = "service1"; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:abc123"; - itemsInfo.PushBack(item); + std::vector GetSpaceEvents(size_t allocatedSize) + { + std::lock_guard lock {mMutex}; - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); + std::vector events; - EXPECT_CALL(mStorageMock, AddItem(_)).WillOnce(Return(ErrorEnum::eNone)); + std::copy_if(mSpaceEvents.begin(), mSpaceEvents.end(), std::back_inserter(events), + [allocatedSize](const SpaceEvent& event) { return event.mAllocatedSize == allocatedSize; }); - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)).WillRepeatedly(Return(ErrorEnum::eNotFound)); + return events; + } - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(AtLeast(0)); + std::vector GetNotifiedStates() + { + std::lock_guard lock {mMutex}; - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, _, _)).Times(AtLeast(0)); + return mStates; + } - std::thread downloadThread([&]() { - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); - EXPECT_EQ(err, ErrorEnum::eCanceled); - }); + std::vector GetBlobs() const { return {&mIndex, &mManifest, &mConfig, &mLayer}; } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + void ExpectDownload(const TestBlob& blob, std::optional allocateSize = std::nullopt) + { + const auto& blobInfo = GetBlobInfo(blob); - auto cancelErr = mImageManager.Cancel(); - EXPECT_TRUE(cancelErr.IsNone()); + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(allocateSize.value_or(blobInfo.mSize))).Times(1); + EXPECT_CALL(mDownloader, + Download(_, String(blob.mDigest.c_str()), Eq(blobInfo.mURLs), String(blob.mDownloadPath.c_str()))) + .Times(1); + } - downloadThread.join(); -} + void ExpectDownloads(const std::vector& blobs) + { + for (const auto* blob : blobs) { + ExpectDownload(*blob); + } + } -TEST_F(ImageManagerTest, DownloadUpdateItems_Cancel_DownloadFailed) -{ - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:abc123"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); - - EXPECT_CALL(mStorageMock, AddItem(_)).WillOnce(Return(ErrorEnum::eNone)); - - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)) - .WillRepeatedly(Invoke([](const auto& digests, Array& blobsInfo) { - for (const auto& digest : digests) { - BlobInfo info; - info.mDigest = digest; - info.mSize = 1024; - info.mURLs.PushBack("http://test.com/blob"); - info.mDecryptInfo.EmplaceValue(); - info.mSignInfo.EmplaceValue(); - - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mSHA256.PushBack(static_cast(i)); - } + void ExpectRegisterDownloads(const std::vector& blobs) + { + for (const auto* blob : blobs) { + EXPECT_CALL(mDownloadSpaceAllocator, AddOutdatedItem(String(blob->mDigest.c_str()), String(""), _)) + .Times(AtLeast(1)); + } + } - blobsInfo.PushBack(info); - } + std::vector GetOutdatedItems(const std::string& digest) + { + std::lock_guard lock {mMutex}; - return ErrorEnum::eNone; - })); + std::vector items; - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(AtLeast(0)); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + std::copy_if(mOutdatedItems.begin(), mOutdatedItems.end(), std::back_inserter(items), + [&digest](const OutdatedItem& item) { return item.mDigest == digest; }); - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); - testing::Mock::AllowLeak(space.Get()); + return items; + } - return {std::move(space), ErrorEnum::eNone}; - })); + std::optional GetSpaceEventIndex(const SpaceEvent& event) + { + std::lock_guard lock {mMutex}; - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillRepeatedly(Return(ErrorEnum::eRuntime)); + auto it = std::find(mSpaceEvents.begin(), mSpaceEvents.end(), event); + if (it == mSpaceEvents.end()) { + return std::nullopt; + } - EXPECT_CALL(mDownloaderMock, Cancel(_)).WillOnce(Return(ErrorEnum::eNone)); + return static_cast(std::distance(mSpaceEvents.begin(), it)); + } - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, _, _)).Times(AtLeast(0)); + UpdateItemInfo MakeItemInfo(const char* itemID = cTestItemID, const TestBlob* index = nullptr) const + { + UpdateItemInfo itemInfo; - std::thread downloadThread([&]() { - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); - EXPECT_EQ(err, ErrorEnum::eCanceled); - }); + itemInfo.mItemID = itemID; + itemInfo.mType = UpdateItemTypeEnum::eService; + itemInfo.mVersion = cTestVersion; + itemInfo.mIndexDigest = (index != nullptr ? *index : mIndex).mDigest.c_str(); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + return itemInfo; + } - auto cancelErr = mImageManager.Cancel(); - EXPECT_TRUE(cancelErr.IsNone()); + Error Install() { return mImageManager.InstallItem(MakeItemInfo()); } - downloadThread.join(); -} + std::optional GetStoredItem() + { + common::imagemanager::DeployableItemInfo item; -TEST_F(ImageManagerTest, DownloadUpdateItems_UnencryptedBlob_CopiesToInstall) -{ - constexpr size_t cBlobSize = 1024; - - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "1.0.0"; - item.mIndexDigest = cBlobDigest; - itemsInfo.PushBack(item); - - ItemInfo storedItem; - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)) - .Times(3) - .WillRepeatedly(Invoke([&storedItem](Array& items) { - if (!storedItem.mItemID.IsEmpty()) { - return items.PushBack(storedItem); - } - - return Error(ErrorEnum::eNone); - })); - EXPECT_CALL(mStorageMock, AddItem(_)).WillOnce(Invoke([&storedItem](const ItemInfo& addedItem) { - storedItem = addedItem; - return ErrorEnum::eNone; - })); - - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)) - .WillOnce(Invoke([](const auto& digests, Array& blobsInfo) { - BlobInfo info; - info.mDigest = digests[0]; - info.mSize = cBlobSize; - info.mURLs.PushBack("http://test.com/blob"); - info.mSignInfo.EmplaceValue(); - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mSHA256.PushBack(static_cast(i)); - } - - return blobsInfo.PushBack(info); - })); + if (!mStorage.GetDeployableItem(cTestItemID, cTestVersion, item).IsNone()) { + return std::nullopt; + } - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(cBlobSize)).WillOnce(Invoke([this](size_t) { - return MakeSpaceMock(0, 1); - })); + return item; + } - EXPECT_CALL(mInstallSpaceAllocatorMock, AllocateSpace(cBlobSize)).WillOnce(Invoke([this](size_t) { - return MakeSpaceMock(1, 0); - })); + void ExpectItemState(ItemStateEnum state, std::optional error = std::nullopt) + { + auto item = GetStoredItem(); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, state); + + if (error.has_value()) { + EXPECT_TRUE(item->mError.Is(*error)) << tests::utils::ErrorToStr(item->mError); + } else { + EXPECT_TRUE(item->mError.IsNone()) << tests::utils::ErrorToStr(item->mError); + } + } - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillOnce(Invoke([this](const auto&, const auto&, const auto&) { - CreateFile(GetBlobDownloadPath(), cBlobSize); - return ErrorEnum::eNone; - })); + static crypto::SignInfo MakeSignInfo() + { + crypto::SignInfo signInfo; - ExpectBlobChecksum(2); + signInfo.mChainName = "chain1"; + signInfo.mAlg = "RSA/SHA256"; - EXPECT_CALL(mCryptoHelperMock, Decrypt(_, _, _)).Times(0); - EXPECT_CALL(mCryptoHelperMock, ValidateSigns(_, _, _, _)).WillOnce(Return(ErrorEnum::eNone)); + for (uint8_t i = 0; i < 16; ++i) { + EXPECT_TRUE(signInfo.mValue.PushBack(i).IsNone()); + } - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillOnce(Return(ErrorEnum::eNone)); + return signInfo; + } - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::ePending), _)) - .WillOnce(Return(ErrorEnum::eNone)); + static std::unique_ptr MakeCerts(const std::vector& fingerprints) + { + auto certs = std::make_unique(); - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + for (const auto& fingerprint : fingerprints) { + crypto::CertificateInfo cert; - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::ePending); + cert.mFingerprint = fingerprint.c_str(); - ExpectFileRemoved(GetBlobDownloadPath()); - ExpectFileSize(fs::JoinPath(mConfig.mImagePath, "blobs/sha256", cBlobHash), cBlobSize); -} + EXPECT_TRUE(cert.mCertificate.PushBack(static_cast(fingerprint.size())).IsNone()); + EXPECT_TRUE(certs->PushBack(cert).IsNone()); + } -TEST_F(ImageManagerTest, DownloadUpdateItems_PartialDownload_AllocatesRemainingSize) -{ - constexpr size_t cBlobSize = 1024; - constexpr size_t cPartialSize = 600; + return certs; + } - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; + static std::unique_ptr MakeChains(const std::vector& names) + { + auto chains = std::make_unique(); - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "1.0.0"; - item.mIndexDigest = cBlobDigest; - itemsInfo.PushBack(item); + for (const auto& name : names) { + crypto::CertificateChainInfo chain; - CreateFile(GetBlobDownloadPath(), cPartialSize); + chain.mName = name.c_str(); - ExpectSingleBlobItem(cBlobSize, 3); + EXPECT_TRUE(chain.mFingerprints.EmplaceBack((name + "-fingerprint").c_str()).IsNone()); + EXPECT_TRUE(chains->PushBack(chain).IsNone()); + } - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(cBlobSize - cPartialSize)) - .WillOnce(Invoke([this](size_t) { return MakeSpaceMock(0, 1); })); + return chains; + } - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(cPartialSize)).Times(1); + void ExpectValidateSigns(const crypto::SignInfo& signInfo, const crypto::CertificateChainInfoArray& chains, + const crypto::CertificateInfoArray& certs, Error result = ErrorEnum::eNone) + { + EXPECT_CALL(mCryptoHelper, + ValidateSigns(String(mLayer.mPath.c_str()), Eq(signInfo), + Truly([&chains](const Array& arg) { return arg == chains; }), + Truly([&certs](const Array& arg) { return arg == certs; }))) + .WillOnce(Return(result)); + } - EXPECT_CALL(mInstallSpaceAllocatorMock, AllocateSpace(cBlobSize)).WillOnce(Invoke([this](size_t) { - return MakeSpaceMock(1, 0); - })); + // mAllocator must be declared before any member that allocates from it, so it is destroyed after them, since + // members are destroyed in reverse declaration order. + HeapAllocator mAllocator; + crypto::DefaultCryptoProvider mCryptoProvider; + fs::FileInfoProvider mFileInfoProviderImpl; + NiceMock mBlobInfoProvider; + common::imagemanager::StorageStub mStorage; + NiceMock mSpaceAllocator; + StrictMock mDownloadSpaceAllocator; + StrictMock mDownloader; + NiceMock mCryptoHelper; + NiceMock mFileServer; + NiceMock mFileInfoProvider; + NiceMock mOCISpec; + NiceMock mFSPlatform; + NiceMock mListener; + ImageManager mImageManager; + + TestBlob mIndex; + TestBlob mManifest; + TestBlob mConfig; + TestBlob mLayer; + + std::mutex mMutex; + std::map> mBlobInfos; + std::map mDownloadContents; + std::vector mSpaceEvents; + std::vector mOutdatedItems; + std::vector mStates; + + std::map> mImageIndexes; + std::map> mImageManifests; + std::map> mImageConfigs; +}; - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillOnce(Return(ErrorEnum::eNone)); +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ - ExpectBlobChecksum(2); +TEST_F(CMImageManagerTest, SetCertsReplacesPrevious) +{ + const auto signInfo = MakeSignInfo(); + auto certs = MakeCerts({"cert1", "cert2"}); + auto chains = MakeChains({"chain1"}); + auto newCerts = MakeCerts({"cert3"}); + auto newChain = MakeChains({"chain2", "chain3"}); - EXPECT_CALL(mCryptoHelperMock, Decrypt(_, _, _)).WillOnce(Return(ErrorEnum::eNone)); - EXPECT_CALL(mCryptoHelperMock, ValidateSigns(_, _, _, _)).WillOnce(Return(ErrorEnum::eNone)); + GetBlobInfo(mLayer).mSignInfo.SetValue(signInfo); - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillOnce(Return(ErrorEnum::eNone)); + auto err = mImageManager.SetCerts(*certs, *chains); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::ePending), _)) - .WillOnce(Return(ErrorEnum::eNone)); + err = mImageManager.SetCerts(*newCerts, *newChain); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); + ExpectValidateSigns(signInfo, *newChain, *newCerts); - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::ePending); + err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - ExpectFileRemoved(GetBlobDownloadPath()); + ExpectItemState(ItemStateEnum::eInstalled); } -TEST_F(ImageManagerTest, DownloadUpdateItems_PartialDownloadExceedsBlobSize_Redownloads) +TEST_F(CMImageManagerTest, SetCertsNoMemory) { - constexpr size_t cBlobSize = 1024; - constexpr size_t cPartialSize = 2048; - - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "1.0.0"; - item.mIndexDigest = cBlobDigest; - itemsInfo.PushBack(item); - - CreateFile(GetBlobDownloadPath(), cPartialSize); - - ExpectSingleBlobItem(cBlobSize, 3); - - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(cBlobSize)).WillOnce(Invoke([this](size_t) { - return MakeSpaceMock(0, 1); - })); + const auto signInfo = MakeSignInfo(); + auto certs = MakeCerts({"cert1"}); + auto chains = MakeChains({"chain1"}); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(0); + GetBlobInfo(mLayer).mSignInfo.SetValue(signInfo); - EXPECT_CALL(mInstallSpaceAllocatorMock, AllocateSpace(cBlobSize)).WillOnce(Invoke([this](size_t) { - return MakeSpaceMock(1, 0); - })); + auto err = mImageManager.SetCerts(*certs, *chains); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillOnce(Return(ErrorEnum::eNone)); + auto tooManyCerts = std::make_unique>(); - ExpectBlobChecksum(2); + for (size_t i = 0; i < crypto::cMaxNumCertificates + 1; ++i) { + ASSERT_TRUE(tooManyCerts->EmplaceBack().IsNone()); + } - EXPECT_CALL(mCryptoHelperMock, Decrypt(_, _, _)).WillOnce(Return(ErrorEnum::eNone)); - EXPECT_CALL(mCryptoHelperMock, ValidateSigns(_, _, _, _)).WillOnce(Return(ErrorEnum::eNone)); + err = mImageManager.SetCerts(*tooManyCerts, *chains); + EXPECT_TRUE(err.Is(ErrorEnum::eNoMemory)) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillOnce(Return(ErrorEnum::eNone)); + auto tooManyChains = std::make_unique>(); - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::ePending), _)) - .WillOnce(Return(ErrorEnum::eNone)); + for (size_t i = 0; i < crypto::cCertChainsCount + 1; ++i) { + ASSERT_TRUE(tooManyChains->EmplaceBack().IsNone()); + } - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + err = mImageManager.SetCerts(*certs, *tooManyChains); + EXPECT_TRUE(err.Is(ErrorEnum::eNoMemory)) << tests::utils::ErrorToStr(err); - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::ePending); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); + ExpectValidateSigns(signInfo, *chains, *certs); - ExpectFileRemoved(GetBlobDownloadPath()); + err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); } -TEST_F(ImageManagerTest, DownloadUpdateItems_Cancel_KeepsPartialDownloadAccounted) +TEST_F(CMImageManagerTest, SetCertsWaitsForSignVerification) { - constexpr size_t cBlobSize = 1024; - constexpr size_t cDownloadedSize = 700; + const auto signInfo = MakeSignInfo(); + auto certs = MakeCerts({"cert1"}); + auto chains = MakeChains({"chain1"}); + auto newCerts = MakeCerts({"cert2"}); + auto newChain = MakeChains({"chain2"}); - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; + std::promise entered; + std::promise release; + auto releaseFuture = release.get_future().share(); - UpdateItemInfo item; - item.mItemID = "service1"; - item.mVersion = "1.0.0"; - item.mIndexDigest = cBlobDigest; - itemsInfo.PushBack(item); + GetBlobInfo(mLayer).mSignInfo.SetValue(signInfo); - ExpectSingleBlobItem(cBlobSize, 2); + ASSERT_TRUE(mImageManager.SetCerts(*certs, *chains).IsNone()); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(cBlobSize)).WillOnce(Invoke([this](size_t) { - return MakeSpaceMock(1, 0); - })); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(cBlobSize - cDownloadedSize)).Times(1); + EXPECT_CALL(mCryptoHelper, ValidateSigns(String(mLayer.mPath.c_str()), Eq(signInfo), _, _)) + .WillOnce(Invoke([&entered, releaseFuture](const String&, const crypto::SignInfo&, + const Array&, const Array&) { + entered.set_value(); + releaseFuture.wait(); - EXPECT_CALL(mDownloaderMock, Download(_, _, _)) - .WillOnce(Invoke([](const String&, const String&, const String& path) { - CreateFile(path, cDownloadedSize); - - return ErrorEnum::eRuntime; + return ErrorEnum::eNone; })); - EXPECT_CALL(mDownloaderMock, Cancel(_)).WillOnce(Return(ErrorEnum::eNone)); - - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, _, _)).Times(AtLeast(0)); - - std::thread downloadThread([&]() { - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); - EXPECT_EQ(err, ErrorEnum::eCanceled); - }); - - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + auto install = std::async(std::launch::async, [this]() { return Install(); }); - EXPECT_TRUE(mImageManager.Cancel().IsNone()); + ASSERT_EQ(entered.get_future().wait_for(cWaitTimeout), std::future_status::ready); - downloadThread.join(); + auto setCerts = std::async( + std::launch::async, [this, &newCerts, &newChain]() { return mImageManager.SetCerts(*newCerts, *newChain); }); - ExpectFileSize(GetBlobDownloadPath(), cDownloadedSize); -} - -TEST_F(ImageManagerTest, DownloadUpdateItems_Cancel_ShrunkPartialDownload) -{ - constexpr size_t cBlobSize = 1024; - constexpr size_t cPartialSize = 600; - constexpr size_t cDownloadedSize = 50; - - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; + EXPECT_EQ(setCerts.wait_for(std::chrono::milliseconds(100)), std::future_status::timeout); - UpdateItemInfo item; - item.mItemID = "service1"; - item.mVersion = "1.0.0"; - item.mIndexDigest = cBlobDigest; - itemsInfo.PushBack(item); + release.set_value(); - CreateFile(GetBlobDownloadPath(), cPartialSize); + ASSERT_EQ(setCerts.wait_for(cWaitTimeout), std::future_status::ready); - ExpectSingleBlobItem(cBlobSize, 2); + auto err = setCerts.get(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(cBlobSize - cPartialSize)) - .WillOnce(Invoke([this](size_t) { return MakeSpaceMock(1, 0); })); + ASSERT_EQ(install.wait_for(cWaitTimeout), std::future_status::ready); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(cBlobSize - cDownloadedSize)).Times(1); + err = install.get(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mDownloaderMock, Download(_, _, _)) - .WillOnce(Invoke([](const String&, const String&, const String& path) { - CreateFile(path, cDownloadedSize); + ExpectItemState(ItemStateEnum::eInstalled); +} - return ErrorEnum::eRuntime; - })); +TEST_F(CMImageManagerTest, InstallDownloadsBlobs) +{ + for (const auto* blob : GetBlobs()) { + const auto hash = CalculateHash(blob->mContent, crypto::HashEnum::eSHA3_256); - EXPECT_CALL(mDownloaderMock, Cancel(_)).WillOnce(Return(ErrorEnum::eNone)); + ASSERT_TRUE(GetBlobInfo(*blob).mSHA256.Assign(Array(hash.data(), hash.size())).IsNone()); + } - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, _, _)).Times(AtLeast(0)); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - std::thread downloadThread([&]() { - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); - EXPECT_EQ(err, ErrorEnum::eCanceled); - }); + EXPECT_CALL(mCryptoHelper, Decrypt(_, _, _)).Times(0); + EXPECT_CALL(mCryptoHelper, ValidateSigns(_, _, _, _)).Times(0); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_TRUE(mImageManager.Cancel().IsNone()); + for (const auto* blob : GetBlobs()) { + EXPECT_EQ(ReadFile(blob->mPath), blob->mContent) << blob->mDigest; + EXPECT_EQ(ReadFile(blob->mDownloadPath), blob->mContent) << blob->mDigest; + EXPECT_EQ( + GetSpaceEvents(blob->mContent.size()), std::vector({{blob->mContent.size(), "accept", 0}})) + << blob->mDigest; + } - downloadThread.join(); + EXPECT_EQ(GetNotifiedStates(), + std::vector( + {ItemStateEnum::eInstalling, ItemStateEnum::eDownloading, ItemStateEnum::eInstalled})); - ExpectFileSize(GetBlobDownloadPath(), cDownloadedSize); + ExpectItemState(ItemStateEnum::eInstalled); } -TEST_F(ImageManagerTest, DownloadUpdateItems_Cancel_NoPartialFile_ReleasesSpace) +TEST_F(CMImageManagerTest, InstallDecryptsBlob) { - constexpr size_t cBlobSize = 1024; - - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mVersion = "1.0.0"; - item.mIndexDigest = cBlobDigest; - itemsInfo.PushBack(item); + crypto::DecryptInfo decryptInfo; - ExpectSingleBlobItem(cBlobSize, 2); + decryptInfo.mBlockAlg = "AES256/CBC/pkcs7padding"; - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(cBlobSize)).WillOnce(Invoke([this](size_t) { - return MakeSpaceMock(0, 1); - })); + for (uint8_t i = 0; i < 8; ++i) { + ASSERT_TRUE(decryptInfo.mBlockIV.PushBack(i).IsNone()); + ASSERT_TRUE(decryptInfo.mBlockKey.PushBack(i + 100).IsNone()); + } - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(0); + GetBlobInfo(mLayer).mDecryptInfo.SetValue(decryptInfo); - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillOnce(Return(ErrorEnum::eRuntime)); + const auto encrypted = std::string(mLayer.mContent.rbegin(), mLayer.mContent.rend()); - EXPECT_CALL(mDownloaderMock, Cancel(_)).WillOnce(Return(ErrorEnum::eNone)); + SetDownloadContent(mLayer, encrypted); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, _, _)).Times(AtLeast(0)); + EXPECT_CALL(mCryptoHelper, Decrypt(_, _, _)).Times(0); + EXPECT_CALL( + mCryptoHelper, Decrypt(String(mLayer.mDownloadPath.c_str()), String(mLayer.mPath.c_str()), Eq(decryptInfo))) + .WillOnce(Invoke([](const String& encryptedPath, const String& decryptedPath, const crypto::DecryptInfo&) { + const auto content = ReadFile(encryptedPath.CStr()); - std::thread downloadThread([&]() { - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); - EXPECT_EQ(err, ErrorEnum::eCanceled); - }); + WriteFile(decryptedPath.CStr(), std::string(content.rbegin(), content.rend())); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + return ErrorEnum::eNone; + })); - EXPECT_TRUE(mImageManager.Cancel().IsNone()); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - downloadThread.join(); + EXPECT_EQ(ReadFile(mLayer.mPath), mLayer.mContent); + EXPECT_EQ(ReadFile(mLayer.mDownloadPath), encrypted); - ExpectFileRemoved(GetBlobDownloadPath()); + ExpectItemState(ItemStateEnum::eInstalled); } -TEST_F(ImageManagerTest, DownloadUpdateItems_DownloadFileInfoFailed_DiscardsDownload) +TEST_F(CMImageManagerTest, InstallVerifiesSigns) { - constexpr size_t cBlobSize = 1024; + const auto signInfo = MakeSignInfo(); + auto certs = MakeCerts({"cert1"}); + auto chains = MakeChains({"chain1"}); - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; + GetBlobInfo(mLayer).mSignInfo.SetValue(signInfo); - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "1.0.0"; - item.mIndexDigest = cBlobDigest; - itemsInfo.PushBack(item); - - ExpectSingleBlobItem(cBlobSize, 3); - - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(cBlobSize)).WillOnce(Invoke([this](size_t) { - return MakeSpaceMock(0, 1); - })); - - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(0); - - EXPECT_CALL(mDownloaderMock, Download(_, _, _)) - .WillOnce(Invoke([](const String&, const String&, const String& path) { - CreateFile(path, cBlobSize); - - return ErrorEnum::eNone; - })); + ASSERT_TRUE(mImageManager.SetCerts(*certs, *chains).IsNone()); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)).WillOnce(Return(ErrorEnum::eRuntime)); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::eFailed), _)) - .WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mCryptoHelper, ValidateSigns(_, _, _, _)).Times(0); + ExpectValidateSigns(signInfo, *chains, *certs); - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::eFailed); + EXPECT_EQ(ReadFile(mLayer.mPath), mLayer.mContent); + EXPECT_TRUE(FileExists(mLayer.mDownloadPath)); - ExpectFileRemoved(GetBlobDownloadPath()); + ExpectItemState(ItemStateEnum::eInstalled); } -TEST_F(ImageManagerTest, DownloadUpdateItems_RemovesOldPendingVersion) +TEST_F(CMImageManagerTest, InstallSignVerificationFails) { - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mVersion = "2.0.0"; - item.mIndexDigest = "sha256:abc123"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(3).WillRepeatedly(Invoke([](Array& items) { - ItemInfo oldItem; - oldItem.mItemID = "service1"; - oldItem.mVersion = "1.0.0"; - oldItem.mIndexDigest = "sha256:old123"; - oldItem.mState = ItemStateEnum::ePending; - items.PushBack(oldItem); + const auto signInfo = MakeSignInfo(); + auto certs = MakeCerts({"cert1"}); + auto chains = MakeChains({"chain1"}); - return ErrorEnum::eNone; - })); + GetBlobInfo(mLayer).mSignInfo.SetValue(signInfo); - EXPECT_CALL(mStorageMock, RemoveItem(_, _)) - .Times(2) - .WillRepeatedly(Invoke([](const String& itemID, const String& version) { - EXPECT_EQ(itemID, "service1"); - EXPECT_EQ(version, "1.0.0"); + ASSERT_TRUE(mImageManager.SetCerts(*certs, *chains).IsNone()); - return ErrorEnum::eNone; - })); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads({&mIndex, &mManifest, &mConfig}); + ExpectValidateSigns(signInfo, *chains, *certs, ErrorEnum::eFailed); - EXPECT_CALL(mStorageMock, AddItem(_)).WillOnce(Invoke([](const ItemInfo& item) { - EXPECT_EQ(item.mItemID, "service1"); - EXPECT_EQ(item.mVersion, "2.0.0"); - EXPECT_EQ(item.mState, ItemStateEnum::eDownloading); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(mLayer.mContent.size())).Times(1); - return ErrorEnum::eNone; - })); + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)) - .WillRepeatedly(Invoke([](const auto&, Array& blobsInfo) { - BlobInfo info; - info.mDigest = "sha256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; - info.mSize = 1024; - info.mURLs.PushBack("http://test.com/blob"); - info.mDecryptInfo.EmplaceValue(); - info.mSignInfo.EmplaceValue(); + EXPECT_FALSE(FileExists(mLayer.mDownloadPath)); + EXPECT_FALSE(FileExists(mLayer.mPath)); - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mSHA256.PushBack(static_cast(i)); - } - - blobsInfo.PushBack(info); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eFailed); +} - return ErrorEnum::eNone; - })); +TEST_F(CMImageManagerTest, InstallChecksumMismatch) +{ + ASSERT_TRUE(GetBlobInfo(mLayer).mSHA256.Resize(crypto::cSHA256Size, 0xaa).IsNone()); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(AtLeast(0)); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads({&mIndex, &mManifest, &mConfig}); - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(mLayer.mContent.size())).Times(1); - return {std::move(space), ErrorEnum::eNone}; - })); + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidChecksum)) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mInstallSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + EXPECT_FALSE(FileExists(mLayer.mDownloadPath)); + EXPECT_FALSE(FileExists(mLayer.mPath)); + EXPECT_TRUE(FileExists(mConfig.mDownloadPath)); - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eInvalidChecksum); +} - return {std::move(space), ErrorEnum::eNone}; - })); +TEST_F(CMImageManagerTest, InstallDecryptFails) +{ + crypto::DecryptInfo decryptInfo; - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + decryptInfo.mBlockAlg = "AES256/CBC/pkcs7padding"; - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"; - index.mManifests.PushBack(manifest); + GetBlobInfo(mLayer).mDecryptInfo.SetValue(decryptInfo); - return ErrorEnum::eNone; - })); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads({&mIndex, &mManifest, &mConfig}); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:config1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; + EXPECT_CALL(mCryptoHelper, Decrypt(String(mLayer.mDownloadPath.c_str()), String(mLayer.mPath.c_str()), _)) + .WillOnce(Return(Error(ErrorEnum::eFailed, "decrypt failed"))); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(mLayer.mContent.size())).Times(1); - oci::ContentDescriptor layer; - layer.mDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - manifest.mLayers.PushBack(layer); + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })); + EXPECT_FALSE(FileExists(mLayer.mDownloadPath)); + EXPECT_FALSE(FileExists(mLayer.mPath)); - EXPECT_CALL(mCryptoHelperMock, Decrypt(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); - EXPECT_CALL(mCryptoHelperMock, ValidateSigns(_, _, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eFailed); +} - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([](const String&, fs::FileInfo& info, crypto::Hash) { - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mCheckSum.PushBack(static_cast(i)); - } +TEST_F(CMImageManagerTest, InstallCopyFailsKeepsDownload) +{ + auto err = fs::MakeDirAll(mLayer.mPath.c_str()); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - info.mSize = 1024; + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - return ErrorEnum::eNone; - })); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(_)).Times(0); - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::ePending), _)) - .WillOnce(Return(ErrorEnum::eNone)); + err = Install(); + EXPECT_FALSE(err.IsNone()); - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + EXPECT_EQ(ReadFile(mLayer.mDownloadPath), mLayer.mContent); - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, "service1"); - EXPECT_EQ(statuses[0].mVersion, "2.0.0"); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::ePending); + auto item = GetStoredItem(); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); } -TEST_F(ImageManagerTest, DownloadUpdateItems_RemovesOldFailedVersion) +TEST_F(CMImageManagerTest, InstallMalformedBlobDigest) { - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mVersion = "2.0.0"; - item.mIndexDigest = "sha256:abc123"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(3).WillRepeatedly(Invoke([](Array& items) { - ItemInfo oldItem; - oldItem.mItemID = "service1"; - oldItem.mVersion = "1.0.0"; - oldItem.mIndexDigest = "sha256:old123"; - oldItem.mState = ItemStateEnum::eFailed; - items.PushBack(oldItem); + GetBlobInfo(mIndex).mDigest = "sha256:../../x"; - return ErrorEnum::eNone; - })); + EXPECT_CALL(mDownloader, Download(_, _, _, _)).Times(0); + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(_)).Times(0); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(_)).Times(0); - EXPECT_CALL(mStorageMock, RemoveItem(_, _)).WillOnce(Invoke([](const String& itemID, const String& version) { - EXPECT_EQ(itemID, "service1"); - EXPECT_EQ(version, "1.0.0"); + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })); + EXPECT_FALSE(FileExists(std::string(cTestDir) + "/x")); + EXPECT_FALSE(fs::DirExist(cTestDownloadPath).mValue); - EXPECT_CALL(mStorageMock, AddItem(_)).WillOnce(Return(ErrorEnum::eNone)); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eInvalidArgument); +} - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)) - .WillRepeatedly(Invoke([](const auto&, Array& blobsInfo) { - BlobInfo info; - info.mDigest = "sha256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; - info.mSize = 1024; - info.mURLs.PushBack("http://test.com/blob"); - info.mDecryptInfo.EmplaceValue(); - info.mSignInfo.EmplaceValue(); +TEST_F(CMImageManagerTest, InstallDownloadSizeMismatch) +{ + constexpr size_t cPartSize = 7; - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mSHA256.PushBack(static_cast(i)); - } + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads({&mIndex, &mManifest, &mConfig}); - blobsInfo.PushBack(info); + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(mLayer.mContent.size())).Times(1); + EXPECT_CALL(mDownloader, Download(_, String(mLayer.mDigest.c_str()), _, String(mLayer.mDownloadPath.c_str()))) + .WillOnce( + Invoke([this](CancelContext&, const String&, const Array>&, const String& path) { + WriteFile(path.CStr(), mLayer.mContent.substr(0, cPartSize)); - return ErrorEnum::eNone; - })); - - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(AtLeast(0)); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); - - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); + return ErrorEnum::eNone; + })); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(mLayer.mContent.size())).Times(1); - return {std::move(space), ErrorEnum::eNone}; - })); + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidChecksum)) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mInstallSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + EXPECT_EQ(GetSpaceEvents(mLayer.mContent.size()), std::vector({{mLayer.mContent.size(), "accept", 0}})); + EXPECT_FALSE(FileExists(mLayer.mDownloadPath)); + EXPECT_FALSE(FileExists(mLayer.mPath)); - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eInvalidChecksum); +} - return {std::move(space), ErrorEnum::eNone}; - })); +TEST_F(CMImageManagerTest, InstallResumesPartialDownload) +{ + constexpr size_t cPartSize = 5; - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + WriteFile(mLayer.mDownloadPath, mLayer.mContent.substr(0, cPartSize)); - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"; - index.mManifests.PushBack(manifest); + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - return ErrorEnum::eNone; - })); + { + InSequence sequence; - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:config1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; + EXPECT_CALL(mDownloadSpaceAllocator, RestoreOutdatedItem(String(mLayer.mDigest.c_str()), String(""))) + .WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(mLayer.mContent.size() - cPartSize)).Times(1); + } - oci::ContentDescriptor layer; - layer.mDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - manifest.mLayers.PushBack(layer); + EXPECT_CALL(mDownloader, Download(_, String(mLayer.mDigest.c_str()), _, String(mLayer.mDownloadPath.c_str()))) + .Times(1); - return ErrorEnum::eNone; - })); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mCryptoHelperMock, Decrypt(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); - EXPECT_CALL(mCryptoHelperMock, ValidateSigns(_, _, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_EQ(ReadFile(mLayer.mDownloadPath), mLayer.mContent); + EXPECT_EQ(ReadFile(mLayer.mPath), mLayer.mContent); + EXPECT_EQ(GetSpaceEvents(mLayer.mContent.size() - cPartSize), + std::vector({{mLayer.mContent.size() - cPartSize, "accept", 0}})); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([](const String&, fs::FileInfo& info, crypto::Hash) { - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mCheckSum.PushBack(static_cast(i)); - } + ExpectItemState(ItemStateEnum::eInstalled); +} - info.mSize = 1024; +TEST_F(CMImageManagerTest, InstallReusesCompleteDownload) +{ + WriteFile(mLayer.mDownloadPath, mLayer.mContent); - return ErrorEnum::eNone; - })); + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::ePending), _)) - .WillOnce(Return(ErrorEnum::eNone)); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + EXPECT_EQ(ReadFile(mLayer.mPath), mLayer.mContent); + EXPECT_TRUE(FileExists(mLayer.mDownloadPath)); - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::ePending); + ExpectItemState(ItemStateEnum::eInstalled); } -TEST_F(ImageManagerTest, DownloadUpdateItems_RemovesExcessRemovedVersion) +TEST_F(CMImageManagerTest, FinishDownloadTouchesReusedFile) { - StaticArray itemsInfo; - StaticArray certificates; - StaticArray certificateChains; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mVersion = "2.0.0"; - item.mIndexDigest = "sha256:abc123"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).WillRepeatedly(Invoke([](Array& items) { - ItemInfo removedItem; - removedItem.mItemID = "service1"; - removedItem.mVersion = "1.0.0"; - removedItem.mIndexDigest = "sha256:old100"; - removedItem.mState = ItemStateEnum::eRemoved; - removedItem.mTimestamp = Time::Now().Add(-(Time::cSeconds * 5)); - items.PushBack(removedItem); - - ItemInfo installedItem; - installedItem.mItemID = "service1"; - installedItem.mVersion = "1.1.0"; - installedItem.mIndexDigest = "sha256:old110"; - installedItem.mState = ItemStateEnum::eInstalled; - installedItem.mTimestamp = Time::Now(); - items.PushBack(installedItem); + WriteFile(mLayer.mDownloadPath, mLayer.mContent); + SetModifyTime(mLayer.mDownloadPath, cPastTime); - return ErrorEnum::eNone; - })); - - size_t numRemoved = 0; + const auto oldTime = GetModifyTime(mLayer.mDownloadPath); - EXPECT_CALL(mStorageMock, RemoveItem(_, _)) - .WillRepeatedly(Invoke([&numRemoved](const String& itemID, const String& version) { - EXPECT_EQ(itemID, "service1"); - EXPECT_EQ(version, "1.0.0"); + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - numRemoved++; + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })); + const auto newTime = GetModifyTime(mLayer.mDownloadPath); - EXPECT_CALL(mInstallSpaceAllocatorMock, RestoreOutdatedItem(_, _)).WillRepeatedly(Return(ErrorEnum::eNone)); - EXPECT_CALL(mInstallSpaceAllocatorMock, FreeSpace(_)).Times(AtLeast(1)); + EXPECT_TRUE(IsNow(newTime)); - EXPECT_CALL(mStorageMock, AddItem(_)).WillOnce(Invoke([](const ItemInfo& item) { - EXPECT_EQ(item.mItemID, "service1"); - EXPECT_EQ(item.mVersion, "2.0.0"); - EXPECT_EQ(item.mState, ItemStateEnum::eDownloading); + const auto items = GetOutdatedItems(mLayer.mDigest); + ASSERT_FALSE(items.empty()); + EXPECT_EQ(items.back().mTimestamp.UnixTime().tv_sec, newTime.UnixTime().tv_sec); + EXPECT_NE(items.back().mTimestamp.UnixTime().tv_sec, oldTime.UnixTime().tv_sec); - return ErrorEnum::eNone; - })); + ExpectItemState(ItemStateEnum::eInstalled); +} - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfos(_, _)) - .WillRepeatedly(Invoke([](const auto&, Array& blobsInfo) { - BlobInfo info; - info.mDigest = "sha256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; - info.mSize = 1024; - info.mURLs.PushBack("http://test.com/blob"); - info.mDecryptInfo.EmplaceValue(); - info.mSignInfo.EmplaceValue(); +TEST_F(CMImageManagerTest, InstallRemovesOversizedDownload) +{ + const auto oversized = mLayer.mContent + " and some garbage"; - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mSHA256.PushBack(static_cast(i)); - } + WriteFile(mLayer.mDownloadPath, oversized); - blobsInfo.PushBack(info); + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - return ErrorEnum::eNone; - })); + { + InSequence sequence; - EXPECT_CALL(mDownloadingSpaceAllocatorMock, FreeSpace(_)).Times(AtLeast(0)); - EXPECT_CALL(mDownloadingSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(oversized.size())).Times(1); + ExpectDownload(mLayer); + } - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - return {std::move(space), ErrorEnum::eNone}; - })); + EXPECT_EQ(ReadFile(mLayer.mDownloadPath), mLayer.mContent); + EXPECT_EQ(ReadFile(mLayer.mPath), mLayer.mContent); - EXPECT_CALL(mInstallSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](size_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + ExpectItemState(ItemStateEnum::eInstalled); +} - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); +TEST_F(CMImageManagerTest, InstallDownloadFails) +{ + constexpr size_t cPartSize = 7; - return {std::move(space), ErrorEnum::eNone}; - })); + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - EXPECT_CALL(mDownloaderMock, Download(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(mLayer.mContent.size())).Times(1); + EXPECT_CALL(mDownloader, Download(_, String(mLayer.mDigest.c_str()), _, String(mLayer.mDownloadPath.c_str()))) + .WillOnce( + Invoke([this](CancelContext&, const String&, const Array>&, const String& path) { + WriteFile(path.CStr(), mLayer.mContent.substr(0, cPartSize)); - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"; - index.mManifests.PushBack(manifest); + return Error(ErrorEnum::eRuntime, "download failed"); + })); - return ErrorEnum::eNone; - })); + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eRuntime)) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:config1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; + EXPECT_EQ(GetSpaceEvents(mLayer.mContent.size()), + std::vector( + {{mLayer.mContent.size(), "resize", cPartSize}, {mLayer.mContent.size(), "accept", 0}})); + EXPECT_EQ(ReadFile(mLayer.mDownloadPath), mLayer.mContent.substr(0, cPartSize)); + EXPECT_FALSE(FileExists(mLayer.mPath)); - oci::ContentDescriptor layer; - layer.mDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - manifest.mLayers.PushBack(layer); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eRuntime); +} - return ErrorEnum::eNone; - })); +TEST_F(CMImageManagerTest, InstallDownloadFailsResume) +{ + constexpr size_t cPartSize = 5; + constexpr size_t cDownloadedSize = 4; + const auto allocateSize = mLayer.mContent.size() - cPartSize; - EXPECT_CALL(mCryptoHelperMock, Decrypt(_, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); - EXPECT_CALL(mCryptoHelperMock, ValidateSigns(_, _, _, _)).WillRepeatedly(Return(ErrorEnum::eNone)); + WriteFile(mLayer.mDownloadPath, mLayer.mContent.substr(0, cPartSize)); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([](const String&, fs::FileInfo& info, crypto::Hash) { - for (size_t i = 0; i < crypto::cSHA256Size; i++) { - info.mCheckSum.PushBack(static_cast(i)); - } + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - info.mSize = 1024; + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(allocateSize)).Times(1); + EXPECT_CALL(mDownloader, Download(_, String(mLayer.mDigest.c_str()), _, String(mLayer.mDownloadPath.c_str()))) + .WillOnce( + Invoke([this](CancelContext&, const String&, const Array>&, const String& path) { + WriteFile(path.CStr(), mLayer.mContent.substr(cPartSize, cDownloadedSize), true); - return ErrorEnum::eNone; - })); + return Error(ErrorEnum::eRuntime, "download failed"); + })); - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, ItemState(ItemStateEnum::ePending), _)) - .WillOnce(Return(ErrorEnum::eNone)); + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eRuntime)) << tests::utils::ErrorToStr(err); - auto err = mImageManager.DownloadUpdateItems(itemsInfo, certificates, certificateChains, statuses); + EXPECT_EQ(GetSpaceEvents(allocateSize), + std::vector({{allocateSize, "resize", cDownloadedSize}, {allocateSize, "accept", 0}})); + EXPECT_EQ(ReadFile(mLayer.mDownloadPath), mLayer.mContent.substr(0, cPartSize + cDownloadedSize)); + EXPECT_FALSE(FileExists(mLayer.mPath)); - EXPECT_TRUE(err.IsNone()); - EXPECT_GE(numRemoved, 1); - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, "service1"); - EXPECT_EQ(statuses[0].mVersion, "2.0.0"); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::ePending); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eRuntime); } -TEST_F(ImageManagerTest, InstallUpdateItems_Success) +TEST_F(CMImageManagerTest, InstallDownloadFailsAfterRestart) { - StaticArray itemsInfo; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:1111"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).Times(5).WillRepeatedly(Invoke([](Array& items) { - ItemInfo storedItem; - storedItem.mItemID = "service1"; - storedItem.mVersion = "1.0.0"; - storedItem.mIndexDigest = "sha256:1111"; - storedItem.mState = ItemStateEnum::ePending; - items.PushBack(storedItem); + constexpr size_t cPartSize = 10; + constexpr size_t cDownloadedSize = 3; + const auto allocateSize = mLayer.mContent.size() - cPartSize; - return ErrorEnum::eNone; - })); - - auto blobsDir = fs::JoinPath(mConfig.mImagePath, "/blobs/sha256/"); - fs::MakeDirAll(blobsDir); - - auto indexPath = fs::JoinPath(blobsDir, "1111"); - auto manifestPath = fs::JoinPath(blobsDir, "2222"); - auto configPath = fs::JoinPath(blobsDir, "cccc"); - auto layerPath = fs::JoinPath(blobsDir, "3333"); - - std::ofstream(indexPath.CStr()).close(); - std::ofstream(manifestPath.CStr()).close(); - std::ofstream(configPath.CStr()).close(); - std::ofstream(layerPath.CStr()).close(); - - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .Times(4) - .WillRepeatedly(Invoke([](const String& path, fs::FileInfo& info, crypto::Hash) { - size_t lastSlashPos = 0; - for (size_t i = 0; i < path.Size(); i++) { - if (path[i] == '/') { - lastSlashPos = i; - } - } + WriteFile(mLayer.mDownloadPath, mLayer.mContent.substr(0, cPartSize)); - String digest(path.CStr() + lastSlashPos + 1); + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - digest.HexToByteArray(info.mCheckSum); - info.mSize = 100; + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(allocateSize)).Times(1); + EXPECT_CALL(mDownloader, Download(_, String(mLayer.mDigest.c_str()), _, String(mLayer.mDownloadPath.c_str()))) + .WillOnce( + Invoke([this](CancelContext&, const String&, const Array>&, const String& path) { + WriteFile(path.CStr(), mLayer.mContent.substr(0, cDownloadedSize)); - return ErrorEnum::eNone; + return Error(ErrorEnum::eRuntime, "download failed"); + })); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(cPartSize - cDownloadedSize)) + .WillOnce(Invoke([this, allocateSize](size_t size) { + AddSpaceEvent({allocateSize, "free", size}); })); - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:2222"; - index.mManifests.PushBack(manifest); + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eRuntime)) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })); - - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:cccc"; + EXPECT_EQ(GetSpaceEvents(allocateSize), + std::vector({{allocateSize, "resize", 0}, {allocateSize, "accept", 0}, + {allocateSize, "free", cPartSize - cDownloadedSize}})); + EXPECT_EQ(ReadFile(mLayer.mDownloadPath), mLayer.mContent.substr(0, cDownloadedSize)); + EXPECT_FALSE(FileExists(mLayer.mPath)); - oci::ContentDescriptor layer; - layer.mDigest = "sha256:3333"; - manifest.mLayers.PushBack(layer); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eRuntime); +} - return ErrorEnum::eNone; - })); +TEST_F(CMImageManagerTest, InstallAllocateSpaceFails) +{ + constexpr size_t cPartSize = 5; - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, _, _)) - .WillOnce(Invoke([](const String& id, const String& version, ItemState state, Time) { - EXPECT_EQ(id, "service1"); - EXPECT_EQ(version, "1.0.0"); - EXPECT_EQ(state, ItemStateEnum::eInstalled); + WriteFile(mLayer.mDownloadPath, mLayer.mContent.substr(0, cPartSize)); - return ErrorEnum::eNone; - })); + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - ItemStatusListenerMock listener; - ItemStateEnum lastState = ItemStateEnum::ePending; - EXPECT_CALL(listener, OnItemsStatusesChanged(_)) - .Times(AtLeast(1)) - .WillRepeatedly(Invoke([&lastState](const Array& statuses) { - ASSERT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, "service1"); - EXPECT_EQ(statuses[0].mType, UpdateItemTypeEnum::eService); - EXPECT_EQ(statuses[0].mVersion, "1.0.0"); - lastState = statuses[0].mState; + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(mLayer.mContent.size() - cPartSize)) + .WillOnce(Invoke([](size_t) -> RetWithError> { + return {UniquePtr(), ErrorEnum::eNoMemory}; })); + EXPECT_CALL(mDownloader, Download(_, String(mLayer.mDigest.c_str()), _, _)).Times(0); - EXPECT_TRUE(mImageManager.SubscribeListener(listener).IsNone()); + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eNoMemory)) << tests::utils::ErrorToStr(err); - auto err = mImageManager.InstallUpdateItems(itemsInfo, statuses); + EXPECT_EQ(ReadFile(mLayer.mDownloadPath), mLayer.mContent.substr(0, cPartSize)); + EXPECT_FALSE(FileExists(mLayer.mPath)); - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, "service1"); - EXPECT_EQ(statuses[0].mType, UpdateItemTypeEnum::eService); - EXPECT_EQ(statuses[0].mVersion, "1.0.0"); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalled); - EXPECT_TRUE(statuses[0].mError.IsNone()); - EXPECT_EQ(lastState, ItemStateEnum::eInstalled); - - EXPECT_TRUE(mImageManager.UnsubscribeListener(listener).IsNone()); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eNoMemory); } -TEST_F(ImageManagerTest, InstallUpdateItems_VerifyBlobsIntegrity_IndexNotFound) +TEST_F(CMImageManagerTest, CancelDuringDownload) { - StaticArray itemsInfo; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:abc123"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)) - .Times(5) - .WillOnce(Invoke([](Array& items) { - ItemInfo storedItem; - storedItem.mItemID = "service1"; - storedItem.mType = UpdateItemTypeEnum::eService; - storedItem.mVersion = "1.0.0"; - storedItem.mIndexDigest = "sha256:abc123"; - storedItem.mState = ItemStateEnum::ePending; - items.PushBack(storedItem); - - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array& items) { - ItemInfo storedItem; - storedItem.mItemID = "service1"; - storedItem.mType = UpdateItemTypeEnum::eService; - storedItem.mVersion = "1.0.0"; - storedItem.mIndexDigest = "sha256:abc123"; - storedItem.mState = ItemStateEnum::ePending; - items.PushBack(storedItem); + std::promise started; - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array&) { return ErrorEnum::eNone; })) - .WillOnce(Invoke([](Array&) { return ErrorEnum::eNone; })) - .WillOnce(Invoke([](Array&) { return ErrorEnum::eNone; })); + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)).WillOnce(Return(ErrorEnum::eNotFound)); - - EXPECT_CALL(mStorageMock, RemoveItem(_, _)).WillOnce(Invoke([](const String& id, const String& version) { - EXPECT_EQ(id, "service1"); - EXPECT_EQ(version, "1.0.0"); - - return ErrorEnum::eNone; - })); + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(mLayer.mContent.size())).Times(1); + EXPECT_CALL(mDownloader, Download(_, String(mLayer.mDigest.c_str()), _, String(mLayer.mDownloadPath.c_str()))) + .WillOnce(Invoke( + [&started](CancelContext& ctx, const String&, const Array>&, const String& path) { + WriteFile(path.CStr(), ""); + started.set_value(); - auto err = mImageManager.InstallUpdateItems(itemsInfo, statuses); + return ctx.Wait(cDownloadTimeout); + })); - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, "service1"); - EXPECT_EQ(statuses[0].mType, UpdateItemTypeEnum::eService); - EXPECT_EQ(statuses[0].mVersion, "1.0.0"); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::eFailed); - EXPECT_TRUE(!statuses[0].mError.IsNone()); -} + auto install = std::async(std::launch::async, [this]() { return Install(); }); -TEST_F(ImageManagerTest, InstallUpdateItems_RemoveDifferentVersion) -{ - StaticArray itemsInfo; - StaticArray statuses; - - UpdateItemInfo item; - item.mItemID = "service1"; - item.mType = UpdateItemTypeEnum::eService; - item.mVersion = "2.0.0"; - item.mIndexDigest = "sha256:4444"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)) - .Times(5) - .WillOnce(Invoke([](Array& items) { - ItemInfo oldItem; - oldItem.mItemID = "service1"; - oldItem.mType = UpdateItemTypeEnum::eService; - oldItem.mVersion = "1.0.0"; - oldItem.mIndexDigest = "sha256:1111"; - oldItem.mState = ItemStateEnum::eInstalled; - items.PushBack(oldItem); + ASSERT_EQ(started.get_future().wait_for(cWaitTimeout), std::future_status::ready); - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array& items) { - ItemInfo newItem; - newItem.mItemID = "service1"; - newItem.mType = UpdateItemTypeEnum::eService; - newItem.mVersion = "2.0.0"; - newItem.mIndexDigest = "sha256:4444"; - newItem.mState = ItemStateEnum::ePending; - items.PushBack(newItem); + auto err = mImageManager.Cancel(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array& items) { - ItemInfo newItem; - newItem.mItemID = "service1"; - newItem.mType = UpdateItemTypeEnum::eService; - newItem.mVersion = "2.0.0"; - newItem.mIndexDigest = "sha256:4444"; - newItem.mState = ItemStateEnum::ePending; - items.PushBack(newItem); - - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array& items) { - ItemInfo newItem; - newItem.mItemID = "service1"; - newItem.mType = UpdateItemTypeEnum::eService; - newItem.mVersion = "2.0.0"; - newItem.mIndexDigest = "sha256:4444"; - newItem.mState = ItemStateEnum::eInstalled; - items.PushBack(newItem); + ASSERT_EQ(install.wait_for(cWaitTimeout), std::future_status::ready); - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array&) { return ErrorEnum::eNone; })); + err = install.get(); + EXPECT_TRUE(err.Is(ErrorEnum::eCanceled)) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mStorageMock, RemoveItem(_, _)).WillOnce(Invoke([](const String& id, const String& version) { - EXPECT_EQ(id, "service1"); - EXPECT_EQ(version, "1.0.0"); - return ErrorEnum::eNone; - })); - - auto blobsDir = fs::JoinPath(mConfig.mImagePath, "/blobs/sha256/"); - fs::MakeDirAll(blobsDir); - - auto indexPath = fs::JoinPath(blobsDir, "4444"); - auto manifestPath = fs::JoinPath(blobsDir, "5555"); - auto configPath = fs::JoinPath(blobsDir, "7777"); - auto layerPath = fs::JoinPath(blobsDir, "6666"); - - std::ofstream(indexPath.CStr()).close(); - std::ofstream(manifestPath.CStr()).close(); - std::ofstream(configPath.CStr()).close(); - std::ofstream(layerPath.CStr()).close(); - - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .Times(4) - .WillRepeatedly(Invoke([](const String& path, fs::FileInfo& info, crypto::Hash) { - size_t lastSlashPos = 0; - for (size_t i = 0; i < path.Size(); i++) { - if (path[i] == '/') { - lastSlashPos = i; - } - } + EXPECT_EQ(GetSpaceEvents(mLayer.mContent.size()), + std::vector({{mLayer.mContent.size(), "resize", 0}, {mLayer.mContent.size(), "accept", 0}})); + EXPECT_TRUE(FileExists(mLayer.mDownloadPath)); + EXPECT_FALSE(FileExists(mLayer.mPath)); - String digest(path.CStr() + lastSlashPos + 1); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eCanceled); +} - digest.HexToByteArray(info.mCheckSum); - info.mSize = 100; +TEST_F(CMImageManagerTest, CancelStopsNextBlobDownload) +{ + std::promise started; - return ErrorEnum::eNone; - })); + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(mIndex.mContent.size())).Times(1); + EXPECT_CALL(mDownloader, Download(_, _, _, _)) + .WillOnce( + Invoke([&started](CancelContext& ctx, const String&, const Array>&, const String&) { + started.set_value(); - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:5555"; - index.mManifests.PushBack(manifest); + return ctx.Wait(cDownloadTimeout); + })); - return ErrorEnum::eNone; - })); + auto install = std::async(std::launch::async, [this]() { return Install(); }); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:7777"; + ASSERT_EQ(started.get_future().wait_for(cWaitTimeout), std::future_status::ready); - oci::ContentDescriptor layer; - layer.mDigest = "sha256:6666"; - manifest.mLayers.PushBack(layer); + auto err = mImageManager.Cancel(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })); + ASSERT_EQ(install.wait_for(cWaitTimeout), std::future_status::ready); - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, _, _)) - .WillOnce(Invoke([](const String& id, const String& version, ItemState state, Time) { - EXPECT_EQ(id, "service1"); - EXPECT_EQ(version, "2.0.0"); - EXPECT_EQ(state, ItemStateEnum::eInstalled); + err = install.get(); + EXPECT_TRUE(err.Is(ErrorEnum::eCanceled)) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })); + EXPECT_EQ(GetSpaceEvents(mIndex.mContent.size()), + std::vector({{mIndex.mContent.size(), "resize", 0}, {mIndex.mContent.size(), "accept", 0}})); - auto err = mImageManager.InstallUpdateItems(itemsInfo, statuses); + for (const auto* blob : GetBlobs()) { + EXPECT_FALSE(FileExists(blob->mPath)) << blob->mDigest; + } - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, "service1"); - EXPECT_EQ(statuses[0].mType, UpdateItemTypeEnum::eService); - EXPECT_EQ(statuses[0].mVersion, "2.0.0"); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalled); - EXPECT_TRUE(statuses[0].mError.IsNone()); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eCanceled); } -TEST_F(ImageManagerTest, InstallUpdateItems_SetItemsToRemoved) +TEST_F(CMImageManagerTest, InstallRegistersDownloadFiles) { - StaticArray itemsInfo; - StaticArray statuses; - UpdateItemInfo item; - - item.mItemID = "service2"; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:aaaa"; - itemsInfo.PushBack(item); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)) - .Times(5) - .WillOnce(Invoke([](Array& items) { - ItemInfo item1; - item1.mItemID = "service1"; - item1.mVersion = "1.0.0"; - item1.mIndexDigest = "sha256:1111"; - item1.mState = ItemStateEnum::eInstalled; - items.PushBack(item1); - - ItemInfo item2; - item2.mItemID = "service2"; - item2.mVersion = "1.0.0"; - item2.mIndexDigest = "sha256:aaaa"; - item2.mState = ItemStateEnum::ePending; - - items.PushBack(item2); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array& items) { - ItemInfo item1; - item1.mItemID = "service1"; - item1.mVersion = "1.0.0"; - item1.mIndexDigest = "sha256:1111"; - item1.mState = ItemStateEnum::eInstalled; - items.PushBack(item1); - - ItemInfo item2; - item2.mItemID = "service2"; - item2.mVersion = "1.0.0"; - item2.mIndexDigest = "sha256:aaaa"; - item2.mState = ItemStateEnum::ePending; - items.PushBack(item2); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array& items) { - ItemInfo item1; - item1.mItemID = "service1"; - item1.mVersion = "1.0.0"; - item1.mIndexDigest = "sha256:1111"; - item1.mState = ItemStateEnum::eInstalled; - items.PushBack(item1); - - ItemInfo item2; - item2.mItemID = "service2"; - item2.mVersion = "1.0.0"; - item2.mIndexDigest = "sha256:aaaa"; - item2.mState = ItemStateEnum::ePending; - items.PushBack(item2); + for (const auto* blob : GetBlobs()) { + const auto acceptIndex = GetSpaceEventIndex({blob->mContent.size(), "accept", 0}); + ASSERT_TRUE(acceptIndex.has_value()) << blob->mDigest; - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array& items) { - ItemInfo item1; - item1.mItemID = "service1"; - item1.mVersion = "1.0.0"; - item1.mIndexDigest = "sha256:1111"; - item1.mState = ItemStateEnum::eInstalled; - items.PushBack(item1); - - ItemInfo item2; - item2.mItemID = "service2"; - item2.mVersion = "1.0.0"; - item2.mIndexDigest = "sha256:aaaa"; - item2.mState = ItemStateEnum::eInstalled; - items.PushBack(item2); + const auto items = GetOutdatedItems(blob->mDigest); + ASSERT_FALSE(items.empty()) << blob->mDigest; - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](Array&) { return ErrorEnum::eNone; })); - - auto blobsDir = fs::JoinPath(mConfig.mImagePath, "/blobs/sha256/"); - fs::MakeDirAll(blobsDir); - - auto service2Index = fs::JoinPath(blobsDir, "aaaa"); - auto service2Manifest = fs::JoinPath(blobsDir, "bbbb"); - auto service2Config = fs::JoinPath(blobsDir, "dddd"); - auto service2Layer = fs::JoinPath(blobsDir, "cccc"); - - std::ofstream(service2Index.CStr()).close(); - std::ofstream(service2Manifest.CStr()).close(); - std::ofstream(service2Config.CStr()).close(); - std::ofstream(service2Layer.CStr()).close(); - - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .Times(4) - .WillRepeatedly(Invoke([](const String& path, fs::FileInfo& info, crypto::Hash) { - size_t lastSlashPos = 0; - for (size_t i = 0; i < path.Size(); i++) { - if (path[i] == '/') { - lastSlashPos = i; - } - } - - String digest(path.CStr() + lastSlashPos + 1); + for (const auto& item : items) { + EXPECT_GT(item.mSpaceEventIndex, *acceptIndex) << blob->mDigest; + EXPECT_TRUE(IsNow(item.mTimestamp)) << blob->mDigest; + } + } - digest.HexToByteArray(info.mCheckSum); - info.mSize = 100; + ExpectItemState(ItemStateEnum::eInstalled); +} - return ErrorEnum::eNone; - })); +TEST_F(CMImageManagerTest, InstallRegistersLeftoverDownloads) +{ + const auto leftover1 = std::string(cTestDownloadPath) + "/sha256/" + cLeftoverHash1; + const auto leftover2 = std::string(cTestDownloadPath) + "/sha256/" + cLeftoverHash2; + const auto leftoverDigest1 = std::string("sha256:") + cLeftoverHash1; + const auto leftoverDigest2 = std::string("sha256:") + cLeftoverHash2; + const auto invalidName = std::string(cTestDownloadPath) + "/sha256/NOT-A-HASH"; + const auto invalidAlgFile = std::string(cTestDownloadPath) + "/junk"; + const auto invalidContent = std::string("invalid"); + const auto junkContent = std::string("junk content"); + + WriteFile(leftover1, "leftover 1"); + WriteFile(leftover2, "leftover 2"); + WriteFile(invalidName, invalidContent); + WriteFile(invalidAlgFile, junkContent); + WriteFile(mIndex.mDownloadPath, mIndex.mContent); + + SetModifyTime(leftover1, cPastTime); + SetModifyTime(leftover2, cPastTime + 1); + SetModifyTime(mIndex.mDownloadPath, cPastTime + 2); + + const auto modifyTime1 = GetModifyTime(leftover1); + const auto modifyTime2 = GetModifyTime(leftover2); + const auto indexTime = GetModifyTime(mIndex.mDownloadPath); + + ExpectDownloads({&mManifest, &mConfig, &mLayer}); + ExpectRegisterDownloads(GetBlobs()); + + EXPECT_CALL(mDownloadSpaceAllocator, AddOutdatedItem(String(leftoverDigest1.c_str()), String(""), Eq(modifyTime1))) + .Times(AtLeast(1)); + EXPECT_CALL(mDownloadSpaceAllocator, AddOutdatedItem(String(leftoverDigest2.c_str()), String(""), Eq(modifyTime2))) + .Times(AtLeast(1)); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(invalidContent.size())).Times(1); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(junkContent.size())).Times(1); + + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_TRUE(FileExists(leftover1)); + EXPECT_TRUE(FileExists(leftover2)); + EXPECT_FALSE(FileExists(invalidName)); + EXPECT_FALSE(FileExists(invalidAlgFile)); + EXPECT_EQ(ReadFile(mIndex.mPath), mIndex.mContent); + + const auto indexItems = GetOutdatedItems(mIndex.mDigest); + ASSERT_FALSE(indexItems.empty()); + EXPECT_TRUE(IsNow(indexItems.front().mTimestamp)); + EXPECT_FALSE(indexItems.front().mTimestamp == indexTime); + + ExpectItemState(ItemStateEnum::eInstalled); +} - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)).WillRepeatedly(Invoke([](const String&, oci::ImageIndex& index) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:bbbb"; - index.mManifests.PushBack(manifest); +TEST_F(CMImageManagerTest, InstallRestoresBeforeScan) +{ + const auto leftover = std::string(cTestDownloadPath) + "/sha256/" + cLeftoverHash1; + const auto leftoverDigest = std::string("sha256:") + cLeftoverHash1; - return ErrorEnum::eNone; - })); + WriteFile(leftover, "leftover"); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String&, oci::ImageManifest& manifest) { - manifest.mConfig.mDigest = "sha256:dddd"; + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - oci::ContentDescriptor layer; - layer.mDigest = "sha256:cccc"; - manifest.mLayers.PushBack(layer); + { + InSequence sequence; - return ErrorEnum::eNone; - })); + EXPECT_CALL(mDownloadSpaceAllocator, RestoreOutdatedItem(String(mIndex.mDigest.c_str()), String(""))) + .WillOnce(Return(ErrorEnum::eNotFound)); + EXPECT_CALL(mDownloadSpaceAllocator, AddOutdatedItem(String(leftoverDigest.c_str()), String(""), _)) + .Times(AtLeast(1)); + } - EXPECT_CALL(mStorageMock, UpdateItemState(_, _, _, _)) - .WillOnce(Invoke([](const String& id, const String& version, ItemState state, Time) { - EXPECT_EQ(id, "service2"); - EXPECT_EQ(version, "1.0.0"); - EXPECT_EQ(state, ItemStateEnum::eInstalled); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })) - .WillOnce(Invoke([](const String& id, const String& version, ItemState state, Time) { - EXPECT_EQ(id, "service1"); - EXPECT_EQ(version, "1.0.0"); - EXPECT_EQ(state, ItemStateEnum::eRemoved); + EXPECT_TRUE(FileExists(leftover)); - return ErrorEnum::eNone; - })); - - EXPECT_CALL(mInstallSpaceAllocatorMock, AddOutdatedItem(_, _, _)) - .WillOnce(Invoke([](const String& id, const String& version, const Time&) { - EXPECT_EQ(id, "service1"); - EXPECT_EQ(version, "1.0.0"); + ExpectItemState(ItemStateEnum::eInstalled); +} - return ErrorEnum::eNone; +TEST_F(CMImageManagerTest, InstallRestoresCurrentDownload) +{ + const auto index2 = AddBlob("second image index blob content: index of manifests"); + const auto manifest2 = AddBlob("second image manifest blob content"); + const auto config2 = AddBlob("second image config: config"); + const auto layer2 = AddBlob("second image layer blob"); + const auto leftover = std::string(cTestDownloadPath) + "/sha256/" + cLeftoverHash1; + const auto leftoverDigest = std::string("sha256:") + cLeftoverHash1; + const auto blobs2 = std::vector({&index2, &manifest2, &config2, &layer2}); + + AddImage(index2, manifest2, config2, layer2, cTestDiffID2); + + std::promise started; + std::promise release; + auto releaseFuture = release.get_future().share(); + + ExpectDownloads({&mManifest, &mConfig, &mLayer}); + ExpectDownloads(blobs2); + ExpectRegisterDownloads(GetBlobs()); + ExpectRegisterDownloads(blobs2); + + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(mIndex.mContent.size())).Times(1); + EXPECT_CALL(mDownloader, Download(_, String(mIndex.mDigest.c_str()), _, String(mIndex.mDownloadPath.c_str()))) + .WillOnce(Invoke([this, &started, releaseFuture](CancelContext&, const String& digest, + const Array>&, const String& path) { + started.set_value(); + releaseFuture.wait(); + + return DownloadContent(digest, path); })); + EXPECT_CALL(mDownloadSpaceAllocator, AddOutdatedItem(String(leftoverDigest.c_str()), String(""), _)) + .Times(AnyNumber()); - auto err = mImageManager.InstallUpdateItems(itemsInfo, statuses); + for (const auto* blob : blobs2) { + EXPECT_CALL(mDownloadSpaceAllocator, RestoreOutdatedItem(String(blob->mDigest.c_str()), String(""))) + .WillOnce(Return(ErrorEnum::eNotFound)); + } - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(statuses.Size(), 1); - EXPECT_EQ(statuses[0].mItemID, "service2"); - EXPECT_EQ(statuses[0].mType, UpdateItemTypeEnum::eService); - EXPECT_EQ(statuses[0].mVersion, "1.0.0"); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalled); - EXPECT_TRUE(statuses[0].mError.IsNone()); -} + auto install1 = std::async(std::launch::async, [this]() { return Install(); }); -TEST_F(ImageManagerTest, RemoveItem_Success) -{ - EXPECT_CALL(mStorageMock, GetItemInfos(_, _)).WillOnce(Invoke([](const String& id, Array& items) { - EXPECT_EQ(id, "service1"); + ASSERT_EQ(started.get_future().wait_for(cWaitTimeout), std::future_status::ready); - ItemInfo item1; - item1.mItemID = "service1"; - item1.mVersion = "1.0.0"; - item1.mIndexDigest = "sha256:1111"; - item1.mState = ItemStateEnum::eRemoved; - items.PushBack(item1); + WriteFile(leftover, "leftover"); - return ErrorEnum::eNone; - })); + auto install2 = std::async(std::launch::async, + [this, &index2]() { return mImageManager.InstallItem(MakeItemInfo(cTestItemID2, &index2)); }); - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).WillOnce(Invoke([](Array& items) { - ItemInfo item2; - item2.mItemID = "service2"; - item2.mVersion = "1.0.0"; - item2.mIndexDigest = "sha256:4444"; - item2.mState = ItemStateEnum::eInstalled; - items.PushBack(item2); + const auto install2Status = install2.wait_for(cWaitTimeout); - return ErrorEnum::eNone; - })); + EXPECT_EQ(install1.wait_for(std::chrono::milliseconds(0)), std::future_status::timeout); + EXPECT_TRUE(GetOutdatedItems(leftoverDigest).empty()); - EXPECT_CALL(mStorageMock, RemoveItem(_, _)).WillOnce(Invoke([](const String& id, const String& version) { - EXPECT_EQ(id, "service1"); - EXPECT_EQ(version, "1.0.0"); + release.set_value(); - return ErrorEnum::eNone; - })); + ASSERT_EQ(install2Status, std::future_status::ready); - EXPECT_CALL(mInstallSpaceAllocatorMock, RestoreOutdatedItem(_, _)).WillOnce(Return(ErrorEnum::eNone)); + auto err = install2.get(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto blobsDir = fs::JoinPath(mConfig.mImagePath, "/blobs/sha256/"); - fs::MakeDirAll(blobsDir); + ASSERT_EQ(install1.wait_for(cWaitTimeout), std::future_status::ready); - auto index1Path = fs::JoinPath(blobsDir, "1111"); - auto manifest1Path = fs::JoinPath(blobsDir, "2222"); - auto config1Path = fs::JoinPath(blobsDir, "config2"); - auto layer1Path = fs::JoinPath(blobsDir, "3333"); + err = install1.get(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - { - std::ofstream f1(index1Path.CStr(), std::ios::binary); - f1 << "index1_content_data"; - f1.flush(); - } - { - std::ofstream f2(manifest1Path.CStr(), std::ios::binary); - f2 << "manifest1_content_data"; - f2.flush(); - } - { - std::ofstream f3(config1Path.CStr(), std::ios::binary); - f3 << "config1_content_data"; - f3.flush(); - } - { - std::ofstream f4(layer1Path.CStr(), std::ios::binary); - f4 << "layer1_content_data"; - f4.flush(); + for (const auto* blob : blobs2) { + EXPECT_EQ(ReadFile(blob->mPath), blob->mContent) << blob->mDigest; } - auto index2Path = fs::JoinPath(blobsDir, "4444"); - auto manifest2Path = fs::JoinPath(blobsDir, "5555"); - auto config2Path = fs::JoinPath(blobsDir, "config5"); - auto layer2Path = fs::JoinPath(blobsDir, "6666"); + ExpectItemState(ItemStateEnum::eInstalled); +} - { - std::ofstream f1(index2Path.CStr(), std::ios::binary); - f1 << "index2_content_data"; - f1.flush(); - } - { - std::ofstream f2(manifest2Path.CStr(), std::ios::binary); - f2 << "manifest2_content_data"; - f2.flush(); - } - { - std::ofstream f3(config2Path.CStr(), std::ios::binary); - f3 << "config2_content_data"; - f3.flush(); - } - { - std::ofstream f4(layer2Path.CStr(), std::ios::binary); - f4 << "layer2_content_data"; - f4.flush(); - } +TEST_F(CMImageManagerTest, BrokenDownloadNotRegistered) +{ + ASSERT_TRUE(GetBlobInfo(mLayer).mSHA256.Resize(crypto::cSHA256Size, 0xaa).IsNone()); - EXPECT_CALL(mOCISpecMock, LoadImageIndex(_, _)) - .WillRepeatedly(Invoke([]([[maybe_unused]] const String& path, oci::ImageIndex& index) { - auto [pos1, err1] = path.FindSubstr(0, "1111"); - if (err1.IsNone()) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:2222"; - index.mManifests.PushBack(manifest); - } else { - auto [pos2, err2] = path.FindSubstr(0, "4444"); - if (err2.IsNone()) { - oci::IndexContentDescriptor manifest; - manifest.mDigest = "sha256:5555"; - index.mManifests.PushBack(manifest); - } - } - return ErrorEnum::eNone; - })); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads({&mIndex, &mManifest, &mConfig}); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([]([[maybe_unused]] const String& path, oci::ImageManifest& manifest) { - auto [pos1, err1] = path.FindSubstr(0, "2222"); - if (err1.IsNone()) { - manifest.mConfig.mDigest = "sha256:config2"; - - oci::ContentDescriptor layer; - layer.mDigest = "sha256:3333"; - manifest.mLayers.PushBack(layer); - } else { - auto [pos2, err2] = path.FindSubstr(0, "5555"); - if (err2.IsNone()) { - manifest.mConfig.mDigest = "sha256:config5"; - - oci::ContentDescriptor layer; - layer.mDigest = "sha256:6666"; - manifest.mLayers.PushBack(layer); - } - } - return ErrorEnum::eNone; - })); + EXPECT_CALL(mDownloadSpaceAllocator, AddOutdatedItem(String(mLayer.mDigest.c_str()), _, _)).Times(0); + EXPECT_CALL(mDownloadSpaceAllocator, FreeSpace(mLayer.mContent.size())).Times(1); + + auto err = Install(); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidChecksum)) << tests::utils::ErrorToStr(err); - ItemStatusListenerMock listener; - EXPECT_CALL(listener, OnItemRemoved(_)).WillOnce(Invoke([](const String& itemID) { - EXPECT_EQ(itemID, "service1"); - })); + EXPECT_FALSE(FileExists(mLayer.mDownloadPath)); + EXPECT_TRUE(GetOutdatedItems(mLayer.mDigest).empty()); - EXPECT_TRUE(mImageManager.SubscribeListener(listener).IsNone()); + ExpectItemState(ItemStateEnum::eFailed, ErrorEnum::eInvalidChecksum); +} - auto [totalSize, err] = mImageManager.RemoveItem("service1", "1.0.0"); +TEST_F(CMImageManagerTest, CanceledDownloadRegistered) +{ + std::promise started; - EXPECT_TRUE(err.IsNone()); - EXPECT_GT(totalSize, 0); + ExpectDownloads({&mIndex, &mManifest, &mConfig}); + ExpectRegisterDownloads(GetBlobs()); - EXPECT_TRUE(mImageManager.UnsubscribeListener(listener).IsNone()); + EXPECT_CALL(mDownloadSpaceAllocator, AllocateSpace(mLayer.mContent.size())).Times(1); + EXPECT_CALL(mDownloader, Download(_, String(mLayer.mDigest.c_str()), _, String(mLayer.mDownloadPath.c_str()))) + .WillOnce(Invoke( + [&started](CancelContext& ctx, const String&, const Array>&, const String& path) { + WriteFile(path.CStr(), "partial"); + started.set_value(); - auto [index1Exists, index1Err] = fs::FileExist(index1Path); - EXPECT_FALSE(index1Exists); + return ctx.Wait(cDownloadTimeout); + })); - auto [manifest1Exists, manifest1Err] = fs::FileExist(manifest1Path); - EXPECT_FALSE(manifest1Exists); + auto install = std::async(std::launch::async, [this]() { return Install(); }); - auto [config1Exists, config1Err] = fs::FileExist(config1Path); - EXPECT_FALSE(config1Exists); + ASSERT_EQ(started.get_future().wait_for(cWaitTimeout), std::future_status::ready); - auto [layer1Exists, layer1Err] = fs::FileExist(layer1Path); - EXPECT_FALSE(layer1Exists); + EXPECT_TRUE(GetOutdatedItems(mLayer.mDigest).empty()); - auto [index2Exists, index2Err] = fs::FileExist(index2Path); - EXPECT_TRUE(index2Exists); + auto err = mImageManager.Cancel(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto [manifest2Exists, manifest2Err] = fs::FileExist(manifest2Path); - EXPECT_TRUE(manifest2Exists); + ASSERT_EQ(install.wait_for(cWaitTimeout), std::future_status::ready); - auto [config2Exists, config2Err] = fs::FileExist(config2Path); - EXPECT_TRUE(config2Exists); + err = install.get(); + EXPECT_TRUE(err.Is(ErrorEnum::eCanceled)) << tests::utils::ErrorToStr(err); - auto [layer2Exists, layer2Err] = fs::FileExist(layer2Path); - EXPECT_TRUE(layer2Exists); + const auto items = GetOutdatedItems(mLayer.mDigest); + ASSERT_EQ(items.size(), 1); + EXPECT_TRUE(IsNow(items.front().mTimestamp)); + EXPECT_EQ(ReadFile(mLayer.mDownloadPath), "partial"); } -TEST_F(ImageManagerTest, Start_RemovesOutdatedItems) +TEST_F(CMImageManagerTest, DownloadItemRemoverRemovesFile) { - StaticArray items; - - ItemInfo item1; - item1.mItemID = "service1"; - item1.mVersion = "1.0.0"; - item1.mIndexDigest = "sha256:1111"; - item1.mState = ItemStateEnum::eRemoved; - item1.mTimestamp = Time::Now().Add(-(Time::cSeconds * 20)); - items.PushBack(item1); - - ItemInfo item2; - item2.mItemID = "service2"; - item2.mVersion = "2.0.0"; - item2.mIndexDigest = "sha256:2222"; - item2.mState = ItemStateEnum::eRemoved; - item2.mTimestamp = Time::Now(); - items.PushBack(item2); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)) - .WillOnce(DoAll(SetArgReferee<0>(items), Return(ErrorEnum::eNone))) - .WillRepeatedly(Return(ErrorEnum::eNone)); - - EXPECT_CALL(mStorageMock, RemoveItem(_, _)).WillOnce(Invoke([](const String& id, const String& version) { - EXPECT_EQ(id, "service1"); - EXPECT_EQ(version, "1.0.0"); - return ErrorEnum::eNone; - })); + WriteFile(mLayer.mDownloadPath, mLayer.mContent); + WriteFile(mLayer.mPath, mLayer.mContent); - ItemStatusListenerMock listener; - EXPECT_CALL(listener, OnItemRemoved(_)).WillOnce(Invoke([](const String& itemID) { - EXPECT_EQ(itemID, "service1"); - })); + EXPECT_CALL(mDownloadSpaceAllocator, RestoreOutdatedItem(_, _)).Times(0); - EXPECT_TRUE(mImageManager.SubscribeListener(listener).IsNone()); + auto [size, err] = mImageManager.GetDownloadItemRemover().RemoveItem(mLayer.mDigest.c_str(), ""); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mInstallSpaceAllocatorMock, RestoreOutdatedItem(_, _)).WillOnce(Return(ErrorEnum::eNone)); - EXPECT_CALL(mInstallSpaceAllocatorMock, FreeSpace(_)).Times(1); - - EXPECT_TRUE(mImageManager.Start().IsNone()); - EXPECT_TRUE(mImageManager.Stop().IsNone()); - EXPECT_TRUE(mImageManager.UnsubscribeListener(listener).IsNone()); + EXPECT_EQ(size, mLayer.mContent.size()); + EXPECT_FALSE(FileExists(mLayer.mDownloadPath)); + EXPECT_EQ(ReadFile(mLayer.mPath), mLayer.mContent); } -TEST_F(ImageManagerTest, GetIndexDigest_Success) +TEST_F(CMImageManagerTest, DownloadItemRemoverMissingFile) { - StaticArray items; - - ItemInfo item; - item.mItemID = "service1"; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:abc123"; - item.mState = ItemStateEnum::eRemoved; - items.PushBack(item); - - EXPECT_CALL(mStorageMock, GetItemInfos(_, _)).WillOnce(DoAll(SetArgReferee<1>(items), Return(ErrorEnum::eNone))); + EXPECT_CALL(mDownloadSpaceAllocator, RestoreOutdatedItem(_, _)).Times(0); - StaticString digest; - auto err = mImageManager.GetIndexDigest("service1", "1.0.0", digest); + auto [size, err] = mImageManager.GetDownloadItemRemover().RemoveItem(mLayer.mDigest.c_str(), ""); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(digest, "sha256:abc123"); + EXPECT_EQ(size, 0); } -TEST_F(ImageManagerTest, GetIndexDigest_NotFound) +TEST_F(CMImageManagerTest, DownloadItemRemoverMalformedDigest) { - StaticArray items; + EXPECT_CALL(mDownloadSpaceAllocator, RestoreOutdatedItem(_, _)).Times(0); - EXPECT_CALL(mStorageMock, GetItemInfos(_, _)).WillOnce(DoAll(SetArgReferee<1>(items), Return(ErrorEnum::eNone))); + auto [size, err] = mImageManager.GetDownloadItemRemover().RemoveItem("sha256:../../x", ""); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); - StaticString digest; - auto err = mImageManager.GetIndexDigest("nonexistent", "1.0.0", digest); - - EXPECT_EQ(err.Value(), ErrorEnum::eNotFound); + EXPECT_EQ(size, 0); } -TEST_F(ImageManagerTest, GetIndexDigest_WrongState) +TEST_F(CMImageManagerTest, GetBlobInfoReturnsStoredBlob) { - StaticArray items; - - ItemInfo item; - item.mItemID = "service1"; - item.mVersion = "1.0.0"; - item.mIndexDigest = "sha256:abc123"; - item.mState = ItemStateEnum::eDownloading; - items.PushBack(item); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - EXPECT_CALL(mStorageMock, GetItemInfos(_, _)).WillOnce(DoAll(SetArgReferee<1>(items), Return(ErrorEnum::eNone))); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - StaticString digest; - auto err = mImageManager.GetIndexDigest("service1", "1.0.0", digest); + const auto url = "https://cm/blobs/" + mLayer.mDigest.substr(mLayer.mDigest.find(':') + 1); - EXPECT_EQ(err.Value(), ErrorEnum::eNotFound); -} + BlobInfo blobInfo; -TEST_F(ImageManagerTest, GetBlobPath_Success) -{ - auto blobsDir = fs::JoinPath(mConfig.mImagePath, "blobs", "sha256"); - fs::MakeDirAll(blobsDir); + blobInfo.mDigest = "sha256:garbage"; + blobInfo.mSize = 1; + blobInfo.mDecryptInfo.SetValue(crypto::DecryptInfo {}); + blobInfo.mSignInfo.SetValue(MakeSignInfo()); - StaticString blobPath; - blobPath = fs::JoinPath(blobsDir, "testdigest"); + ASSERT_TRUE(blobInfo.mURLs.EmplaceBack("https://example.com/garbage").IsNone()); + ASSERT_TRUE(blobInfo.mSHA256.PushBack(0xff).IsNone()); - std::ofstream file(blobPath.CStr()); - file << "test content"; - file.close(); + EXPECT_CALL(mFileServer, TranslateFilePathURL(String(mLayer.mPath.c_str()), _)) + .WillOnce(Invoke([&url](const String&, String& outURL) { return outURL.Assign(url.c_str()); })); - StaticString path; - auto err = mImageManager.GetBlobPath("sha256:testdigest", path); + err = mImageManager.GetBlobInfo(mLayer.mDigest.c_str(), blobInfo); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(path, blobPath); + EXPECT_EQ(blobInfo.mDigest, String(mLayer.mDigest.c_str())); + EXPECT_EQ(blobInfo.mSize, mLayer.mContent.size()); + EXPECT_TRUE(blobInfo.mSHA256.IsEmpty()); + ASSERT_EQ(blobInfo.mURLs.Size(), 1); + EXPECT_EQ(blobInfo.mURLs[0], String(url.c_str())); + EXPECT_FALSE(blobInfo.mDecryptInfo.HasValue()); + EXPECT_FALSE(blobInfo.mSignInfo.HasValue()); } -TEST_F(ImageManagerTest, GetBlobPath_NotFound) +TEST_F(CMImageManagerTest, GetBlobInfoUnusedBlob) { - StaticString path; - auto err = mImageManager.GetBlobPath("sha256:nonexistent", path); + WriteFile(mLayer.mPath, mLayer.mContent); - EXPECT_EQ(err.Value(), ErrorEnum::eNotFound); + EXPECT_CALL(mFileServer, TranslateFilePathURL(_, _)).Times(0); + + BlobInfo blobInfo; + + auto err = mImageManager.GetBlobInfo(mLayer.mDigest.c_str(), blobInfo); + EXPECT_TRUE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); } -TEST_F(ImageManagerTest, GetBlobURL_Success) +TEST_F(CMImageManagerTest, GetBlobInfoRemovedItem) { - auto blobsDir = fs::JoinPath(mConfig.mImagePath, "/blobs/sha256/"); - fs::MakeDirAll(blobsDir); + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - StaticString blobPath; - blobPath = fs::JoinPath(blobsDir, "testdigest"); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - std::ofstream file(blobPath.CStr()); - file << "test content"; - file.close(); + err = mImageManager.RemoveItem(cTestItemID, cTestVersion); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mFileServerMock, TranslateFilePathURL(_, _)) - .WillOnce(DoAll(SetArgReferee<1>("http://localhost/blobs/testdigest"), Return(ErrorEnum::eNone))); + ExpectItemState(ItemStateEnum::eRemoved); - StaticString url; - auto err = mImageManager.GetBlobURL("sha256:testdigest", url); + EXPECT_CALL(mFileServer, TranslateFilePathURL(String(mLayer.mPath.c_str()), _)).WillOnce(Return(ErrorEnum::eNone)); - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(url, "http://localhost/blobs/testdigest"); -} + BlobInfo blobInfo; -TEST_F(ImageManagerTest, GetBlobURL_NotFound) -{ - StaticString url; - auto err = mImageManager.GetBlobURL("sha256:nonexistent", url); + err = mImageManager.GetBlobInfo(mLayer.mDigest.c_str(), blobInfo); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_EQ(err.Value(), ErrorEnum::eNotFound); + EXPECT_EQ(blobInfo.mSize, mLayer.mContent.size()); } -TEST_F(ImageManagerTest, GetItemCurrentVersion_PendingPriority) +TEST_F(CMImageManagerTest, GetBlobInfoMalformedDigest) { - StaticArray items; - - ItemInfo installed; - installed.mItemID = "service1"; - installed.mVersion = "1.0.0"; - installed.mState = ItemStateEnum::eInstalled; - items.PushBack(installed); - - ItemInfo pending; - pending.mItemID = "service1"; - pending.mVersion = "2.0.0"; - pending.mState = ItemStateEnum::ePending; - items.PushBack(pending); - - EXPECT_CALL(mStorageMock, GetItemInfos(_, _)).WillOnce(DoAll(SetArgReferee<1>(items), Return(ErrorEnum::eNone))); + EXPECT_CALL(mFileServer, TranslateFilePathURL(_, _)).Times(0); - StaticString version; - auto err = mImageManager.GetItemCurrentVersion("service1", version); + BlobInfo blobInfo; - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(version, "2.0.0"); + auto err = mImageManager.GetBlobInfo("not-a-digest", blobInfo); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); } -TEST_F(ImageManagerTest, GetItemCurrentVersion_InstalledFallback) +TEST_F(CMImageManagerTest, GetBlobInfoMissingFile) { - StaticArray items; + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - ItemInfo item; - item.mItemID = "service1"; - item.mVersion = "1.0.0"; - item.mState = ItemStateEnum::eInstalled; - items.PushBack(item); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mStorageMock, GetItemInfos(_, _)).WillOnce(DoAll(SetArgReferee<1>(items), Return(ErrorEnum::eNone))); + err = fs::RemoveAll(mLayer.mPath.c_str()); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - StaticString version; - auto err = mImageManager.GetItemCurrentVersion("service1", version); + EXPECT_CALL(mFileServer, TranslateFilePathURL(_, _)).Times(0); - EXPECT_TRUE(err.IsNone()); - EXPECT_EQ(version, "1.0.0"); + BlobInfo blobInfo; + + err = mImageManager.GetBlobInfo(mLayer.mDigest.c_str(), blobInfo); + EXPECT_FALSE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_FALSE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); } -TEST_F(ImageManagerTest, GetItemCurrentVersion_NotFound) +TEST_F(CMImageManagerTest, GetBlobInfoFileServerFails) { - StaticArray items; + ExpectDownloads(GetBlobs()); + ExpectRegisterDownloads(GetBlobs()); - ItemInfo item; - item.mItemID = "service1"; - item.mVersion = "1.0.0"; - item.mState = ItemStateEnum::eRemoved; - items.PushBack(item); + auto err = Install(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mStorageMock, GetItemInfos(_, _)).WillOnce(DoAll(SetArgReferee<1>(items), Return(ErrorEnum::eNone))); + EXPECT_CALL(mFileServer, TranslateFilePathURL(String(mLayer.mPath.c_str()), _)) + .WillOnce(Return(ErrorEnum::eFailed)); - StaticString version; - auto err = mImageManager.GetItemCurrentVersion("service1", version); + BlobInfo blobInfo; - EXPECT_EQ(err.Value(), ErrorEnum::eNotFound); -} + blobInfo.mDigest = "sha256:unchanged"; + blobInfo.mSize = 42; -TEST_F(ImageManagerTest, GetUpdateItemsStatuses_Success) -{ - StaticArray items; - - ItemInfo item1; - item1.mItemID = "service1"; - item1.mType = UpdateItemTypeEnum::eService; - item1.mVersion = "1.0.0"; - item1.mState = ItemStateEnum::eInstalled; - items.PushBack(item1); - - ItemInfo item2; - item2.mItemID = "service2"; - item2.mType = UpdateItemTypeEnum::eService; - item2.mVersion = "2.0.0"; - item2.mState = ItemStateEnum::ePending; - items.PushBack(item2); - - EXPECT_CALL(mStorageMock, GetAllItemsInfos(_)).WillOnce(DoAll(SetArgReferee<0>(items), Return(ErrorEnum::eNone))); - - StaticArray statuses; - auto err = mImageManager.GetUpdateItemsStatuses(statuses); - - EXPECT_TRUE(err.IsNone()); - ASSERT_EQ(statuses.Size(), 2); - - EXPECT_EQ(statuses[0].mItemID, "service1"); - EXPECT_EQ(statuses[0].mType, UpdateItemTypeEnum::eService); - EXPECT_EQ(statuses[0].mVersion, "1.0.0"); - EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalled); - - EXPECT_EQ(statuses[1].mItemID, "service2"); - EXPECT_EQ(statuses[1].mType, UpdateItemTypeEnum::eService); - EXPECT_EQ(statuses[1].mVersion, "2.0.0"); - EXPECT_EQ(statuses[1].mState, ItemStateEnum::ePending); + err = mImageManager.GetBlobInfo(mLayer.mDigest.c_str(), blobInfo); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + EXPECT_EQ(blobInfo.mDigest, "sha256:unchanged"); + EXPECT_EQ(blobInfo.mSize, 42U); + EXPECT_TRUE(blobInfo.mURLs.IsEmpty()); } } // namespace aos::cm::imagemanager diff --git a/src/core/cm/imagemanager/tests/mocks/blobinfoprovidermock.hpp b/src/core/cm/imagemanager/tests/mocks/blobinfoprovidermock.hpp deleted file mode 100644 index 364253813..000000000 --- a/src/core/cm/imagemanager/tests/mocks/blobinfoprovidermock.hpp +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_CM_IMAGEMANAGER_TESTS_MOCKS_BLOBINFOPROVIDER_HPP_ -#define AOS_CORE_CM_IMAGEMANAGER_TESTS_MOCKS_BLOBINFOPROVIDER_HPP_ - -#include - -#include - -namespace aos::cm::imagemanager { - -/** - * Blob info provider mock. - */ -class BlobInfoProviderMock : public BlobInfoProviderItf { -public: - MOCK_METHOD(Error, GetBlobsInfos, (const Array>&, Array&), (override)); -}; - -} // namespace aos::cm::imagemanager - -#endif diff --git a/src/core/cm/imagemanager/tests/mocks/itemstatuslistenermock.hpp b/src/core/cm/imagemanager/tests/mocks/itemstatuslistenermock.hpp deleted file mode 100644 index 21e2ff074..000000000 --- a/src/core/cm/imagemanager/tests/mocks/itemstatuslistenermock.hpp +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_CM_IMAGEMANAGER_TESTS_MOCKS_ITEMSTATUSLISTENERMOCK_HPP_ -#define AOS_CORE_CM_IMAGEMANAGER_TESTS_MOCKS_ITEMSTATUSLISTENERMOCK_HPP_ - -#include - -#include - -namespace aos::cm::imagemanager { - -class ItemStatusListenerMock : public ItemStatusListenerItf { -public: - MOCK_METHOD(void, OnItemsStatusesChanged, (const Array&), (override)); - MOCK_METHOD(void, OnItemRemoved, (const String&), (override)); -}; - -} // namespace aos::cm::imagemanager - -#endif diff --git a/src/core/cm/imagemanager/tests/mocks/storagemock.hpp b/src/core/cm/imagemanager/tests/mocks/storagemock.hpp deleted file mode 100644 index 80a5b7288..000000000 --- a/src/core/cm/imagemanager/tests/mocks/storagemock.hpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_CM_IMAGEMANAGER_TESTS_MOCKS_STORAGE_HPP_ -#define AOS_CORE_CM_IMAGEMANAGER_TESTS_MOCKS_STORAGE_HPP_ - -#include - -#include - -namespace aos::cm::imagemanager { - -/** - * Storage mock. - */ -class StorageMock : public StorageItf { -public: - MOCK_METHOD(Error, AddItem, (const ItemInfo& item), (override)); - MOCK_METHOD(Error, RemoveItem, (const String& id, const String& version), (override)); - MOCK_METHOD( - Error, UpdateItemState, (const String& id, const String& version, ItemState state, Time timestamp), (override)); - MOCK_METHOD(Error, GetAllItemsInfos, (Array & items), (override)); - MOCK_METHOD(Error, GetItemInfos, (const String& itemID, Array& items), (override)); -}; - -} // namespace aos::cm::imagemanager - -#endif diff --git a/src/core/cm/launcher/balancer.cpp b/src/core/cm/launcher/balancer.cpp index 466259b5d..b79758d73 100644 --- a/src/core/cm/launcher/balancer.cpp +++ b/src/core/cm/launcher/balancer.cpp @@ -101,8 +101,9 @@ Error Balancer::PerformNodeBalancing(Array>& instances) continue; } - if (auto err = mImageInfoProvider->GetImageIndex(id.mItemID, info.mVersion, *imageIndex); !err.IsNone()) { - LOG_ERR() << "Can't get images" << Log::Field("instance", id) << Log::Field(err); + if (auto err = mImageInfoProvider->GetImageIndex(info.mIndexDigest, *imageIndex); !err.IsNone()) { + LOG_ERR() << "Can't get images" << Log::Field("instance", id) + << Log::Field("indexDigest", info.mIndexDigest) << Log::Field(err); mInstanceManager->ScheduleInstance(instance, AOS_ERROR_WRAP(err)); @@ -420,9 +421,9 @@ Error Balancer::PerformPolicyBalancing(Array>& instances) } // Load image descriptor - if (auto err = mImageInfoProvider->GetImageIndex(id.mItemID, version, *imageIndex); !err.IsNone()) { + if (auto err = mImageInfoProvider->GetImageIndex(info.mIndexDigest, *imageIndex); !err.IsNone()) { LOG_ERR() << "Can't get image index" << Log::Field("instance", id) << Log::Field("version", version) - << Log::Field(err); + << Log::Field("indexDigest", info.mIndexDigest) << Log::Field(err); continue; } diff --git a/src/core/cm/launcher/imageinfoprovider.cpp b/src/core/cm/launcher/imageinfoprovider.cpp index d45b4c355..c71f90052 100644 --- a/src/core/cm/launcher/imageinfoprovider.cpp +++ b/src/core/cm/launcher/imageinfoprovider.cpp @@ -12,7 +12,7 @@ namespace aos::cm::launcher { void ImageInfoProvider::Init( - AllocatorItf& allocator, imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec) + AllocatorItf& allocator, common::imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec) { mAllocator = &allocator; mItemInfoProvider = &itemInfoProvider; @@ -95,11 +95,10 @@ Error ImageInfoProvider::GetItemConfig(const oci::IndexContentDescriptor& imageD return ErrorEnum::eNone; } -Error ImageInfoProvider::GetImageIndex(const String& itemID, const String& version, oci::ImageIndex& imageIndex) +Error ImageInfoProvider::GetImageIndex(const String& indexDigest, oci::ImageIndex& imageIndex) { - auto indexDigest = MakeUnique>(mAllocator); - if (!indexDigest) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + if (indexDigest.IsEmpty()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "empty index digest")); } auto indexPath = MakeUnique>(mAllocator); @@ -107,11 +106,7 @@ Error ImageInfoProvider::GetImageIndex(const String& itemID, const String& versi return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); } - if (auto err = mItemInfoProvider->GetIndexDigest(itemID, version, *indexDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = mItemInfoProvider->GetBlobPath(*indexDigest, *indexPath); !err.IsNone()) { + if (auto err = mItemInfoProvider->GetBlobPath(indexDigest, *indexPath); !err.IsNone()) { return AOS_ERROR_WRAP(err); } diff --git a/src/core/cm/launcher/imageinfoprovider.hpp b/src/core/cm/launcher/imageinfoprovider.hpp index 37dbb2878..c05ebd1de 100644 --- a/src/core/cm/launcher/imageinfoprovider.hpp +++ b/src/core/cm/launcher/imageinfoprovider.hpp @@ -7,8 +7,7 @@ #ifndef AOS_CORE_CM_LAUNCHER_IMAGEINFOPROVIDER_HPP_ #define AOS_CORE_CM_LAUNCHER_IMAGEINFOPROVIDER_HPP_ -#include -#include +#include #include #include #include @@ -30,7 +29,8 @@ class ImageInfoProvider { * @param itemInfoProvider item info provider. * @param ociSpec OCI spec. */ - void Init(AllocatorItf& allocator, imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec); + void Init( + AllocatorItf& allocator, common::imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec); /** * Returns OCI image config for the specified instance/image identifiers. @@ -51,18 +51,17 @@ class ImageInfoProvider { Error GetItemConfig(const oci::IndexContentDescriptor& imageDescriptor, oci::ItemConfig& itemConfig); /** - * Returns OCI image index for the specified instance/version. + * Returns OCI image index for the specified index digest. * - * @param itemID item identifier. - * @param version item version. + * @param indexDigest image index digest. * @param[out] imageIndex resolved image index. * @return Error. */ - Error GetImageIndex(const String& itemID, const String& version, oci::ImageIndex& imageIndex); + Error GetImageIndex(const String& indexDigest, oci::ImageIndex& imageIndex); private: - imagemanager::ItemInfoProviderItf* mItemInfoProvider {}; - oci::OCISpecItf* mOCISpec {}; + common::imagemanager::ItemInfoProviderItf* mItemInfoProvider {}; + oci::OCISpecItf* mOCISpec {}; AllocatorItf* mAllocator {}; }; diff --git a/src/core/cm/launcher/instance.cpp b/src/core/cm/launcher/instance.cpp index 08036fab1..abba370f5 100644 --- a/src/core/cm/launcher/instance.cpp +++ b/src/core/cm/launcher/instance.cpp @@ -88,8 +88,7 @@ bool Instance::IsImageValid() return false; } - if (auto err = mImageInfoProvider.GetImageIndex(mInfo.mInstanceIdent.mItemID, mInfo.mVersion, *imageIndex); - !err.IsNone()) { + if (auto err = mImageInfoProvider.GetImageIndex(mInfo.mIndexDigest, *imageIndex); !err.IsNone()) { return false; } diff --git a/src/core/cm/launcher/instance.hpp b/src/core/cm/launcher/instance.hpp index d4b2ec175..14b782942 100644 --- a/src/core/cm/launcher/instance.hpp +++ b/src/core/cm/launcher/instance.hpp @@ -75,6 +75,13 @@ class Instance { */ const InstanceInfo& GetInfo() const { return mInfo; } + /** + * Sets image index digest. + * + * @param digest image index digest. + */ + void SetIndexDigest(const String& digest) { mInfo.mIndexDigest = digest; } + /** * Returns SM instance information. * diff --git a/src/core/cm/launcher/instancemanager.cpp b/src/core/cm/launcher/instancemanager.cpp index 1c9d62353..5534f6add 100644 --- a/src/core/cm/launcher/instancemanager.cpp +++ b/src/core/cm/launcher/instancemanager.cpp @@ -38,7 +38,7 @@ Error InstanceManager::RemoveInstances(Array>& instances, Pr **********************************************************************************************************************/ Error InstanceManager::Init(AllocatorItf& allocator, const Config& config, - imagemanager::ItemInfoProviderItf& itemInfoProvider, storagestate::StorageStateItf& storageState, + common::imagemanager::ItemInfoProviderItf& itemInfoProvider, storagestate::StorageStateItf& storageState, oci::OCISpecItf& ociSpec, IdentifierPoolValidator gidValidator, IdentifierPoolValidator uidValidator, StorageItf& storage) { @@ -214,6 +214,10 @@ RetWithError> InstanceManager::CreateInstance(const RunInsta { auto id = InstanceIdent {request.mItemID, request.mSubjectInfo.mSubjectID, index, request.mUpdateItemType}; if (auto instance = FindReadyInstance(id, request.mVersion); instance) { + if (auto err = UpdateIndexDigest(*instance, request.mIndexDigest); !err.IsNone()) { + return {nullptr, err}; + } + return {instance, ErrorEnum::eNone}; } @@ -235,6 +239,10 @@ RetWithError> InstanceManager::CreateInstance(const RunInsta if (auto instance = FindReadyInstance(request.mItemID, request.mSubjectInfo.mSubjectID, nodeID, runtimeID, request.mVersion); instance) { + if (auto err = UpdateIndexDigest(*instance, request.mIndexDigest); !err.IsNone()) { + return {nullptr, err}; + } + return {instance, ErrorEnum::eNone}; } @@ -748,6 +756,7 @@ UniquePtr InstanceManager::CreateInfo( info->mState = InstanceStateEnum::eActive; info->mIsUnitSubject = request.mSubjectInfo.mIsUnitSubject; info->mVersion = request.mVersion; + info->mIndexDigest = request.mIndexDigest; info->mOwnerID = request.mOwnerID; info->mSubjectType = request.mSubjectInfo.mSubjectType; info->mLabels = request.mLabels; @@ -757,6 +766,27 @@ UniquePtr InstanceManager::CreateInfo( return info; } +Error InstanceManager::UpdateIndexDigest(Instance& instance, const String& indexDigest) +{ + if (instance.GetInfo().mIndexDigest == indexDigest) { + return ErrorEnum::eNone; + } + + LOG_DBG() << "Update instance index digest" << Log::Field("instance", instance.GetInfo().mInstanceIdent) + << Log::Field("version", instance.GetInfo().mVersion) << Log::Field("indexDigest", indexDigest); + + instance.SetIndexDigest(indexDigest); + + if (auto err = mStorage->UpdateInstance(instance.GetInfo()); !err.IsNone()) { + LOG_ERR() << "Can't update instance" << Log::Field("instance", instance.GetInfo().mInstanceIdent) + << Log::Field(err); + + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + SharedPtr InstanceManager::FindInstance( const Array>& instances, const InstanceIdent& id, const String& version) { diff --git a/src/core/cm/launcher/instancemanager.hpp b/src/core/cm/launcher/instancemanager.hpp index 4e37659e3..736290326 100644 --- a/src/core/cm/launcher/instancemanager.hpp +++ b/src/core/cm/launcher/instancemanager.hpp @@ -7,9 +7,8 @@ #ifndef AOS_CORE_CM_LAUNCHER_INSTANCEMANAGER_HPP_ #define AOS_CORE_CM_LAUNCHER_INSTANCEMANAGER_HPP_ -#include -#include #include +#include #include #include #include @@ -36,16 +35,18 @@ class InstanceManager { * * @param allocator allocator to use for temporary objects. * @param config Configuration object. - * @param imageInfoProvider Interface for retrieving service information from images. + * @param itemInfoProvider Interface for retrieving local blob paths. * @param storageState Interface for managing storage and state partitions. + * @param ociSpec OCI spec interface. * @param gidValidator GID validator. * @param uidValidator UID validator. * @param storage Interface to persistent storage. * @return Error. */ - Error Init(AllocatorItf& allocator, const Config& config, imagemanager::ItemInfoProviderItf& itemInfoProvider, - storagestate::StorageStateItf& storageState, oci::OCISpecItf& ociSpec, IdentifierPoolValidator gidValidator, - IdentifierPoolValidator uidValidator, StorageItf& storage); + Error Init(AllocatorItf& allocator, const Config& config, + common::imagemanager::ItemInfoProviderItf& itemInfoProvider, storagestate::StorageStateItf& storageState, + oci::OCISpecItf& ociSpec, IdentifierPoolValidator gidValidator, IdentifierPoolValidator uidValidator, + StorageItf& storage); /** * Starts the instance manager. @@ -275,6 +276,7 @@ class InstanceManager { UniquePtr CreateInfo( const InstanceIdent& id, const String& nodeID, const String& runtimeID, const RunInstanceRequest& request); + Error UpdateIndexDigest(Instance& instance, const String& indexDigest); SharedPtr FindInstance( const Array>& instances, const InstanceIdent& id, const String& version); diff --git a/src/core/cm/launcher/itf/types.hpp b/src/core/cm/launcher/itf/types.hpp index 1f5fd00a0..b9764204f 100644 --- a/src/core/cm/launcher/itf/types.hpp +++ b/src/core/cm/launcher/itf/types.hpp @@ -53,6 +53,7 @@ struct InstanceInfo { InstanceState mState {}; bool mIsUnitSubject {}; StaticString mVersion; + StaticString mIndexDigest; StaticString mOwnerID; SubjectType mSubjectType; LabelsArray mLabels; @@ -71,8 +72,9 @@ struct InstanceInfo { && lhs.mNodeID == rhs.mNodeID && lhs.mPrevNodeID == rhs.mPrevNodeID && lhs.mRuntimeID == rhs.mRuntimeID && lhs.mUID == rhs.mUID && lhs.mGID == rhs.mGID && lhs.mTimestamp == rhs.mTimestamp && lhs.mState == rhs.mState && lhs.mIsUnitSubject == rhs.mIsUnitSubject && lhs.mVersion == rhs.mVersion - && lhs.mOwnerID == rhs.mOwnerID && lhs.mSubjectType == rhs.mSubjectType && lhs.mLabels == rhs.mLabels - && lhs.mPriority == rhs.mPriority && lhs.mDisableRebalancing == rhs.mDisableRebalancing; + && lhs.mIndexDigest == rhs.mIndexDigest && lhs.mOwnerID == rhs.mOwnerID + && lhs.mSubjectType == rhs.mSubjectType && lhs.mLabels == rhs.mLabels && lhs.mPriority == rhs.mPriority + && lhs.mDisableRebalancing == rhs.mDisableRebalancing; }; /** @@ -88,14 +90,15 @@ struct InstanceInfo { * Run instance request. */ struct RunInstanceRequest { - StaticString mItemID; - UpdateItemType mUpdateItemType; - StaticString mVersion; - StaticString mOwnerID; - SubjectInfo mSubjectInfo; - size_t mPriority {}; - size_t mNumInstances {}; - LabelsArray mLabels; + StaticString mItemID; + UpdateItemType mUpdateItemType; + StaticString mVersion; + StaticString mIndexDigest; + StaticString mOwnerID; + SubjectInfo mSubjectInfo; + size_t mPriority {}; + size_t mNumInstances {}; + LabelsArray mLabels; /** * Compares run instance request. @@ -106,9 +109,10 @@ struct RunInstanceRequest { friend bool operator==(const RunInstanceRequest& lhs, const RunInstanceRequest& other) { return lhs.mItemID == other.mItemID && lhs.mUpdateItemType == other.mUpdateItemType - && lhs.mVersion == other.mVersion && lhs.mOwnerID == other.mOwnerID - && lhs.mSubjectInfo == other.mSubjectInfo && lhs.mPriority == other.mPriority - && lhs.mNumInstances == other.mNumInstances && lhs.mLabels == other.mLabels; + && lhs.mVersion == other.mVersion && lhs.mIndexDigest == other.mIndexDigest + && lhs.mOwnerID == other.mOwnerID && lhs.mSubjectInfo == other.mSubjectInfo + && lhs.mPriority == other.mPriority && lhs.mNumInstances == other.mNumInstances + && lhs.mLabels == other.mLabels; }; /** diff --git a/src/core/cm/launcher/launcher.cpp b/src/core/cm/launcher/launcher.cpp index a19297dbb..b6c82b6a2 100644 --- a/src/core/cm/launcher/launcher.cpp +++ b/src/core/cm/launcher/launcher.cpp @@ -35,7 +35,7 @@ class ShouldRebalanceVisitor : public StaticVisitor { Error Launcher::Init(AllocatorItf& allocator, const Config& config, nodeinfoprovider::NodeInfoProviderItf& nodeInfoProvider, InstanceRunnerItf& runner, - imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec, + common::imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec, unitconfig::NodeConfigProviderItf& nodeConfigProvider, storagestate::StorageStateItf& storageState, MonitoringProviderItf& monitorProvider, alerts::AlertsProviderItf& alertsProvider, iamclient::IdentProviderItf& identProvider, IdentifierPoolValidator gidValidator, @@ -233,6 +233,7 @@ Error Launcher::RunInstances(const Array& requests, Array -#include -#include #include #include #include +#include #include #include "itf/envvarhandler.hpp" @@ -51,7 +50,7 @@ class Launcher : public LauncherItf, * @param config configuration. * @param nodeInfoProvider interface providing information about all unit nodes. * @param runner instance runner interface. - * @param imageInfoProvider interface that retrieves service information from its image. + * @param itemInfoProvider interface that provides local blob paths. * @param ociSpec OCI spec interface. * @param nodeConfigProvider node config provider interface. * @param storageState interface to manage storage and state partitions. @@ -65,11 +64,12 @@ class Launcher : public LauncherItf, * @return Error. */ Error Init(AllocatorItf& allocator, const Config& config, nodeinfoprovider::NodeInfoProviderItf& nodeInfoProvider, - InstanceRunnerItf& runner, imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec, - unitconfig::NodeConfigProviderItf& nodeConfigProvider, storagestate::StorageStateItf& storageState, - MonitoringProviderItf& monitorProvider, alerts::AlertsProviderItf& alertsProvider, - iamclient::IdentProviderItf& identProvider, IdentifierPoolValidator gidValidator, - IdentifierPoolValidator uidValidator, StorageItf& storage, SenderItf& sender); + InstanceRunnerItf& runner, common::imagemanager::ItemInfoProviderItf& itemInfoProvider, + oci::OCISpecItf& ociSpec, unitconfig::NodeConfigProviderItf& nodeConfigProvider, + storagestate::StorageStateItf& storageState, MonitoringProviderItf& monitorProvider, + alerts::AlertsProviderItf& alertsProvider, iamclient::IdentProviderItf& identProvider, + IdentifierPoolValidator gidValidator, IdentifierPoolValidator uidValidator, StorageItf& storage, + SenderItf& sender); /** * Starts launcher instance. diff --git a/src/core/cm/launcher/launcher.md b/src/core/cm/launcher/launcher.md index 5748fcb35..7e7ac5c15 100644 --- a/src/core/cm/launcher/launcher.md +++ b/src/core/cm/launcher/launcher.md @@ -150,6 +150,9 @@ Returns current instances statuses. Schedules and run desired instances. Launcher calculates on which node and which runtime to start a specific instance based on requested resources, instance, node priorities etc. +Each run request carries the image index digest of the update item (`mIndexDigest`, taken from the desired status). +The launcher stores it in `InstanceInfo` and resolves the image index through +`common::imagemanager::ItemInfoProviderItf::GetBlobPath` and `OCISpecItf::LoadImageIndex`. This function is blocked till all update instances requests are confirmed or fail. diff --git a/src/core/cm/launcher/nodemanager.cpp b/src/core/cm/launcher/nodemanager.cpp index 48bc7a21f..637f86b3f 100644 --- a/src/core/cm/launcher/nodemanager.cpp +++ b/src/core/cm/launcher/nodemanager.cpp @@ -137,7 +137,7 @@ Error NodeManager::LoadSMDataForActiveInstances( } if (auto findDescErr = FindImageDescriptor( - instanceID.mItemID, instance->GetInfo().mVersion, manifestDigest, imageInfoProvider, *imageDescriptor); + instance->GetInfo().mIndexDigest, manifestDigest, imageInfoProvider, *imageDescriptor); !findDescErr.IsNone()) { LOG_ERR() << "Can't find image descriptor" << Log::Field("instanceID", instanceID) << Log::Field("manifestDigest", manifestDigest) << Log::Field(AOS_ERROR_WRAP(findDescErr)); @@ -371,7 +371,7 @@ bool NodeManager::UpdateNodeInfo(const UnitNodeInfo& info) * Private **********************************************************************************************************************/ -Error NodeManager::FindImageDescriptor(const String& itemID, const String& version, const String& manifestDigest, +Error NodeManager::FindImageDescriptor(const String& indexDigest, const String& manifestDigest, ImageInfoProvider& imageInfoProvider, oci::IndexContentDescriptor& imageDescriptor) { auto imageIndex = MakeUnique(mAllocator); @@ -379,7 +379,7 @@ Error NodeManager::FindImageDescriptor(const String& itemID, const String& versi return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); } - if (auto err = imageInfoProvider.GetImageIndex(itemID, version, *imageIndex); !err.IsNone()) { + if (auto err = imageInfoProvider.GetImageIndex(indexDigest, *imageIndex); !err.IsNone()) { return AOS_ERROR_WRAP(err); } diff --git a/src/core/cm/launcher/nodemanager.hpp b/src/core/cm/launcher/nodemanager.hpp index 441fe7384..155dc7c66 100644 --- a/src/core/cm/launcher/nodemanager.hpp +++ b/src/core/cm/launcher/nodemanager.hpp @@ -145,7 +145,7 @@ class NodeManager { private: static constexpr auto cStatusUpdateTimeout = Time::cMinutes * 10; - Error FindImageDescriptor(const String& itemID, const String& version, const String& manifestDigest, + Error FindImageDescriptor(const String& indexDigest, const String& manifestDigest, ImageInfoProvider& imageInfoProvider, oci::IndexContentDescriptor& imageDescriptor); Error ApplyOverrideEnvVars(const Array>& instances); diff --git a/src/core/cm/launcher/runrequestsloader.cpp b/src/core/cm/launcher/runrequestsloader.cpp index 49752ac24..827ca6ed8 100644 --- a/src/core/cm/launcher/runrequestsloader.cpp +++ b/src/core/cm/launcher/runrequestsloader.cpp @@ -94,7 +94,7 @@ Error RunRequestsLoader::GenerateInstances( return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); } - if (auto err = mImageInfoProvider->GetImageIndex(request.mItemID, request.mVersion, *imageIndex); !err.IsNone()) { + if (auto err = mImageInfoProvider->GetImageIndex(request.mIndexDigest, *imageIndex); !err.IsNone()) { return AOS_ERROR_WRAP(err); } diff --git a/src/core/cm/launcher/tests/launcher.cpp b/src/core/cm/launcher/tests/launcher.cpp index 6ddefae17..a109dcb58 100644 --- a/src/core/cm/launcher/tests/launcher.cpp +++ b/src/core/cm/launcher/tests/launcher.cpp @@ -202,6 +202,7 @@ InstanceInfo CreateInstanceInfo(const InstanceIdent& instance, StaticString(); + CreateNodeConfig(*nodeConfig, cNodeIDLocalSM); + mResourceManager.SetNodeConfig(cNodeIDLocalSM, cNodeTypeVM, *nodeConfig); + + auto nodeMonitoring = std::make_unique(); + CreateNodeMonitoring(*nodeMonitoring, cNodeIDLocalSM, 0.0); + mMonitoringProvider.SetAverageMonitoring(cNodeIDLocalSM, *nodeMonitoring); + + auto itemConfig = std::make_unique(); + CreateItemConfig(*itemConfig, {cRunnerRunc}); + AddItem(cService1, cImageID1, *itemConfig, CreateImageConfig(), cVersion); + + mInstanceRunner.Init(mLauncher, true, aos::InstanceStateEnum::eActive); + + ASSERT_TRUE(mLauncher + .Init(mAllocator, CreateConfig(), mNodeInfoProvider, mInstanceRunner, mImageStore, mImageStore, + mResourceManager, mStorageState, mMonitoringProvider, mAlertsProvider, mIdentProvider, + ValidateGID, ValidateUID, mStorage, mSender) + .IsNone()); + + ASSERT_TRUE(mLauncher.Start().IsNone()); + + mInstanceRunner.SendInitialStatuses(cNodeIDLocalSM); + + // Run request refers to an image index which is not provided by the image store. + auto runRequest = std::make_unique>(); + runRequest->PushBack(CreateRunRequest(cService1, cSubject1, 50, 1, "", {}, UpdateItemTypeEnum::eService, cVersion)); + (*runRequest)[0].mIndexDigest = "sha256:unknown"; + + auto runStatuses = std::make_unique>(); + ASSERT_TRUE(mLauncher.RunInstances(*runRequest, *runStatuses).IsNone()); + ASSERT_TRUE(mLauncher.Stop().IsNone()); + + std::map expectedRunRequests = {{cNodeIDLocalSM, {}}}; + EXPECT_EQ(mInstanceRunner.GetRunRequests(), expectedRunRequests); + + auto expectedRunStatus = std::make_unique>(); + expectedRunStatus->PushBack(CreateInstanceStatus(CreateInstanceIdent(cService1, cSubject1, 0), "", "", + aos::InstanceStateEnum::eFailed, ErrorEnum::eNotFound, cVersion)); + + ASSERT_EQ(*runStatuses, *expectedRunStatus); + EXPECT_TRUE((*runStatuses)[0].mError.Is(ErrorEnum::eNotFound)); +} + +TEST_F(CMLauncherTest, RunInstanceWithEmptyIndexDigest) +{ + constexpr auto cVersion = "1.0.0"; + + mStorageState.Init(); + mStorageState.SetTotalStateSize(1024); + mStorageState.SetTotalStorageSize(1024); + + mNodeInfoProvider.Init(); + mImageStore.Init(); + mMonitoringProvider.Init(); + mResourceManager.Init(); + mStorage.Init(); + + auto nodeInfoLocalSM = CreateNodeInfo(cNodeIDLocalSM, 1000, 1024, {CreateRuntime(cRunnerRunc)}, {}); + mNodeInfoProvider.AddNodeInfo(cNodeIDLocalSM, nodeInfoLocalSM); + + auto nodeConfig = std::make_unique(); + CreateNodeConfig(*nodeConfig, cNodeIDLocalSM); + mResourceManager.SetNodeConfig(cNodeIDLocalSM, cNodeTypeVM, *nodeConfig); + + auto nodeMonitoring = std::make_unique(); + CreateNodeMonitoring(*nodeMonitoring, cNodeIDLocalSM, 0.0); + mMonitoringProvider.SetAverageMonitoring(cNodeIDLocalSM, *nodeMonitoring); + + auto itemConfig = std::make_unique(); + CreateItemConfig(*itemConfig, {cRunnerRunc}); + AddItem(cService1, cImageID1, *itemConfig, CreateImageConfig(), cVersion); + + mInstanceRunner.Init(mLauncher, true, aos::InstanceStateEnum::eActive); + + ASSERT_TRUE(mLauncher + .Init(mAllocator, CreateConfig(), mNodeInfoProvider, mInstanceRunner, mImageStore, mImageStore, + mResourceManager, mStorageState, mMonitoringProvider, mAlertsProvider, mIdentProvider, + ValidateGID, ValidateUID, mStorage, mSender) + .IsNone()); + + ASSERT_TRUE(mLauncher.Start().IsNone()); + + mInstanceRunner.SendInitialStatuses(cNodeIDLocalSM); + + // Run request has no image index digest. + auto runRequest = std::make_unique>(); + runRequest->PushBack(CreateRunRequest(cService1, cSubject1, 50, 1, "", {}, UpdateItemTypeEnum::eService, cVersion)); + (*runRequest)[0].mIndexDigest = ""; + + auto runStatuses = std::make_unique>(); + ASSERT_TRUE(mLauncher.RunInstances(*runRequest, *runStatuses).IsNone()); + ASSERT_TRUE(mLauncher.Stop().IsNone()); + + std::map expectedRunRequests = {{cNodeIDLocalSM, {}}}; + EXPECT_EQ(mInstanceRunner.GetRunRequests(), expectedRunRequests); + + auto expectedRunStatus = std::make_unique>(); + expectedRunStatus->PushBack(CreateInstanceStatus(CreateInstanceIdent(cService1, cSubject1, 0), "", "", + aos::InstanceStateEnum::eFailed, ErrorEnum::eInvalidArgument, cVersion)); + + ASSERT_EQ(*runStatuses, *expectedRunStatus); + EXPECT_TRUE((*runStatuses)[0].mError.Is(ErrorEnum::eInvalidArgument)); +} + +TEST_F(CMLauncherTest, ReusedInstanceUpdatesIndexDigest) +{ + constexpr auto cVersion = "1.0.0"; + constexpr auto cUID = 5000; + + mStorageState.Init(); + mStorageState.SetTotalStateSize(1024); + mStorageState.SetTotalStorageSize(1024); + + mNodeInfoProvider.Init(); + mImageStore.Init(); + mMonitoringProvider.Init(); + mResourceManager.Init(); + mStorage.Init(); + + auto nodeInfoLocalSM = CreateNodeInfo(cNodeIDLocalSM, 1000, 1024, {CreateRuntime(cRunnerRunc)}, {}); + mNodeInfoProvider.AddNodeInfo(cNodeIDLocalSM, nodeInfoLocalSM); + + auto nodeConfig = std::make_unique(); + CreateNodeConfig(*nodeConfig, cNodeIDLocalSM); + mResourceManager.SetNodeConfig(cNodeIDLocalSM, cNodeTypeVM, *nodeConfig); + + auto nodeMonitoring = std::make_unique(); + CreateNodeMonitoring(*nodeMonitoring, cNodeIDLocalSM, 0.0); + mMonitoringProvider.SetAverageMonitoring(cNodeIDLocalSM, *nodeMonitoring); + + auto itemConfig = std::make_unique(); + CreateItemConfig(*itemConfig, {cRunnerRunc}); + AddItem(cService1, cImageID1, *itemConfig, CreateImageConfig(), cVersion); + + // Store instance with the same ident and version but with stale index digest. + auto storedInfo = CreateInstanceInfo(CreateInstanceIdent(cService1, cSubject1, 0), {}, cRunnerRunc, "", + InstanceStateEnum::eActive, cUID, 0, Time::Now(), cVersion, true); + storedInfo.mIndexDigest = "sha256:stale"; + + ASSERT_TRUE(mStorage.AddInstance(storedInfo).IsNone()); + + mInstanceRunner.Init(mLauncher, true, aos::InstanceStateEnum::eActive); + + ASSERT_TRUE(mLauncher + .Init(mAllocator, CreateConfig(), mNodeInfoProvider, mInstanceRunner, mImageStore, mImageStore, + mResourceManager, mStorageState, mMonitoringProvider, mAlertsProvider, mIdentProvider, + ValidateGID, ValidateUID, mStorage, mSender) + .IsNone()); + + ASSERT_TRUE(mLauncher.Start().IsNone()); + + mInstanceRunner.SendInitialStatuses(cNodeIDLocalSM); + + auto runRequest = std::make_unique>(); + runRequest->PushBack(CreateRunRequest(cService1, cSubject1, 50, 1, "", {}, UpdateItemTypeEnum::eService, cVersion)); + + auto runStatuses = std::make_unique>(); + ASSERT_TRUE(mLauncher.RunInstances(*runRequest, *runStatuses).IsNone()); + ASSERT_TRUE(mLauncher.Stop().IsNone()); + + auto expectedRunStatus = std::make_unique>(); + expectedRunStatus->PushBack(CreateInstanceStatus(CreateInstanceIdent(cService1, cSubject1, 0), cNodeIDLocalSM, + cRunnerRunc, aos::InstanceStateEnum::eActive, ErrorEnum::eNone, cVersion, false, + BuildManifestDigest(cService1, cImageID1).CStr())); + + ASSERT_EQ(*runStatuses, *expectedRunStatus); + + auto storedInstances = std::make_unique>(); + ASSERT_TRUE(mStorage.LoadActiveInstances(*storedInstances).IsNone()); + + ASSERT_EQ(storedInstances->Size(), 1); + EXPECT_EQ((*storedInstances)[0].mInstanceIdent, CreateInstanceIdent(cService1, cSubject1, 0)); + EXPECT_EQ((*storedInstances)[0].mUID, cUID); + EXPECT_EQ((*storedInstances)[0].mIndexDigest, imagemanager::ImageStoreStub::BuildIndexDigest(cService1, cVersion)); +} + } // namespace aos::cm::launcher diff --git a/src/core/cm/launcher/tests/stubs/imagestorestub.hpp b/src/core/cm/launcher/tests/stubs/imagestorestub.hpp index e292a4b2e..2b9be2975 100644 --- a/src/core/cm/launcher/tests/stubs/imagestorestub.hpp +++ b/src/core/cm/launcher/tests/stubs/imagestorestub.hpp @@ -12,22 +12,19 @@ #include #include -#include -#include +#include #include namespace aos::cm::imagemanager { /** * Test helper that emulates OCI images. - * Provides ItemInfoProvider, BlobInfoProvider, and OCISpec interfaces. + * Provides ItemInfoProvider and OCISpec interfaces. */ -class ImageStoreStub : public ItemInfoProviderItf, public oci::OCISpecItf { +class ImageStoreStub : public common::imagemanager::ItemInfoProviderItf, public oci::OCISpecItf { public: void Init() { - mItemVersions.clear(); - mIndexDigests.clear(); mImageIndexes.clear(); mImageManifests.clear(); mImageConfigs.clear(); @@ -38,8 +35,7 @@ class ImageStoreStub : public ItemInfoProviderItf, public oci::OCISpecItf { void AddItem(const String& itemID, const String& imageID, const oci::ItemConfig& itemCfg, const oci::ImageConfig& imageCfg, const String& version = DefaultVersion) { - auto versionStr = version.IsEmpty() ? DefaultVersion : version.CStr(); - mItemVersions[itemID.CStr()] = versionStr; + auto versionStr = version.IsEmpty() ? DefaultVersion : version.CStr(); auto manifestDigest = EnsureImageArtifacts(itemID, versionStr, imageID); auto configDigest = MakeDigest(itemID, imageID, "config"); @@ -73,19 +69,17 @@ class ImageStoreStub : public ItemInfoProviderItf, public oci::OCISpecItf { return digest; } - // - // imagemanager::ItemInfoProviderItf - // - Error GetIndexDigest(const String& itemID, const String& version, String& digest) const override + static StaticString BuildIndexDigest(const String& itemID, const String& version) { - auto it = mIndexDigests.find(ItemKey {itemID.CStr(), version.CStr()}); - if (it == mIndexDigests.end()) { - return ErrorEnum::eNotFound; - } + StaticString digest; + digest = MakeDigest(itemID, version, "index").c_str(); - return digest.Assign(it->second.c_str()); + return digest; } + // + // common::imagemanager::ItemInfoProviderItf + // Error GetBlobPath(const String& digest, String& path) const override { if (!mKnownDigests.count(digest.CStr())) { @@ -95,17 +89,7 @@ class ImageStoreStub : public ItemInfoProviderItf, public oci::OCISpecItf { return path.Assign(digest); } - Error GetBlobURL(const String&, String&) const override { return ErrorEnum::eNotSupported; } - - Error GetItemCurrentVersion(const String& itemID, String& version) const override - { - auto it = mItemVersions.find(itemID.CStr()); - if (it == mItemVersions.end()) { - return ErrorEnum::eNotFound; - } - - return version.Assign(it->second.c_str()); - } + Error GetLayerPath(const String&, String&) const override { return ErrorEnum::eNotSupported; } // // oci::OCISpecItf @@ -165,16 +149,6 @@ class ImageStoreStub : public ItemInfoProviderItf, public oci::OCISpecItf { Error SaveRuntimeConfig(const String&, const oci::RuntimeConfig&) override { return ErrorEnum::eNotSupported; } private: - struct ItemKey { - std::string mItemID; - std::string mVersion; - - bool operator<(const ItemKey& rhs) const - { - return std::tie(mItemID, mVersion) < std::tie(rhs.mItemID, rhs.mVersion); - } - }; - static constexpr const char* DefaultVersion = ""; static constexpr const char* cImageManifestType = "application/vnd.oci.image.manifest.v1+json"; static constexpr const char* cImageConfigType = "application/vnd.oci.image.config.v1+json"; @@ -192,13 +166,10 @@ class ImageStoreStub : public ItemInfoProviderItf, public oci::OCISpecItf { std::string EnsureImageArtifacts(const String& itemID, const char* version, const String& imageID) { - auto itemKey = ItemKey {itemID.CStr(), version}; auto indexDigest = MakeDigest(itemID, std::string(version), "index"); auto manifestDigest = MakeDigest(itemID, imageID, "manifest"); auto configDigest = MakeDigest(itemID, imageID, "config"); - mIndexDigests[itemKey] = indexDigest; - auto& index = mImageIndexes[indexDigest]; index.mManifests.Clear(); oci::IndexContentDescriptor descriptor; @@ -219,8 +190,6 @@ class ImageStoreStub : public ItemInfoProviderItf, public oci::OCISpecItf { return manifestDigest; } - std::map mItemVersions; - std::map mIndexDigests; std::map mImageIndexes; std::map mImageManifests; std::map mImageConfigs; diff --git a/src/core/cm/smcontroller/smcontroller.md b/src/core/cm/smcontroller/smcontroller.md index c48811282..ad910a51b 100644 --- a/src/core/cm/smcontroller/smcontroller.md +++ b/src/core/cm/smcontroller/smcontroller.md @@ -19,7 +19,8 @@ It requires the following interfaces: * [aos::iamclient::CertProviderItf](../../common/iamclient/itf/certprovider.hpp) - provides certificates; * [aos::crypto::CertLoaderItf](../../common/crypto/itf/certloader.hpp) - loads certificates; * [aos::crypto::x509::ProviderItf](../../common/crypto/itf/x509provider.hpp) - provides crypto operations; -* [aos::cm::imagemanager::ItemInfoProviderItf](../imagemanager/itf/iteminfoprovider.hpp) - provides items info; +* [aos::common::imagemanager::ItemInfoProviderItf](../../common/imagemanager/itf/iteminfoprovider.hpp) - provides + items info; * [aos::cm::alerts::ReceiverItf](../alerts/itf/receiver.hpp) - receives alerts; * [aos::cm::smcontroller::SenderItf](itf/sender.hpp) - sends logs; * [aos::cm::launcher::SenderItf](../launcher/itf/sender.hpp) - sends env vars statuses; @@ -71,7 +72,7 @@ classDiagram <> } - class ItemInfoProviderItf ["aos::cm::imagemanager::ItemInfoProviderItf"] { + class ItemInfoProviderItf ["aos::common::imagemanager::ItemInfoProviderItf"] { <> } diff --git a/src/core/cm/unitconfig/unitconfig.cpp b/src/core/cm/unitconfig/unitconfig.cpp index 828d69031..c0974c0fb 100644 --- a/src/core/cm/unitconfig/unitconfig.cpp +++ b/src/core/cm/unitconfig/unitconfig.cpp @@ -284,7 +284,7 @@ Error UnitConfig::LoadConfig() auto err = fs::ReadFileToString(mUnitConfigFile, *unitConfig); if (!err.IsNone()) { - if (err == ENOENT) { + if (err.Is(ErrorEnum::eNotFound)) { mUnitConfigState = UnitConfigStateEnum::eAbsent; return ErrorEnum::eNone; diff --git a/src/core/cm/updatemanager/desiredstatushandler.cpp b/src/core/cm/updatemanager/desiredstatushandler.cpp index aa9ffc571..04ee11989 100644 --- a/src/core/cm/updatemanager/desiredstatushandler.cpp +++ b/src/core/cm/updatemanager/desiredstatushandler.cpp @@ -5,28 +5,88 @@ */ #include +#include #include "desiredstatushandler.hpp" namespace aos::cm::updatemanager { +namespace { + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +bool IsItemInstallRequired(const UpdateItemStatus& itemStatus) +{ + // A failed item is retried only after removing it, except the one whose install was canceled. + return itemStatus.mState != ItemStateEnum::eInstalled + && (itemStatus.mState != ItemStateEnum::eFailed || itemStatus.mError.Is(ErrorEnum::eCanceled)); +} + +const UpdateItemStatus* FindItemStatus(const UpdateItemInfo& item, const Array& itemsStatuses) +{ + for (const auto& itemStatus : itemsStatuses) { + if (itemStatus.mItemID == item.mItemID && itemStatus.mType == item.mType + && itemStatus.mVersion == item.mVersion) { + return &itemStatus; + } + } + + return nullptr; +} + +// Returns the instance whose error fails the item: any instance with the integrity error, otherwise the first failed +// instance if all the item instances are failed, otherwise nullptr. +const InstanceStatus* FindItemFailureCause(const UpdateItemInfo& item, const Array& instancesStatuses) +{ + const InstanceStatus* failedInstance = nullptr; + auto allFailed = true; + + for (const auto& instanceStatus : instancesStatuses) { + if (instanceStatus.mItemID != item.mItemID || instanceStatus.mVersion != item.mVersion) { + continue; + } + + if (instanceStatus.mState != InstanceStateEnum::eFailed) { + allFailed = false; + + continue; + } + + if (instanceStatus.mError.Is(ErrorEnum::eIntegrity)) { + return &instanceStatus; + } + + if (failedInstance == nullptr) { + failedInstance = &instanceStatus; + } + } + + return allFailed ? failedInstance : nullptr; +} + +} // namespace + /*********************************************************************************************************************** * Public **********************************************************************************************************************/ Error DesiredStatusHandler::Init(AllocatorItf& allocator, iamclient::NodeHandlerItf& nodeHandler, - unitconfig::UnitConfigItf& unitConfig, imagemanager::ImageManagerItf& imageManager, launcher::LauncherItf& launcher, + unitconfig::UnitConfigItf& unitConfig, imagemanager::ImageManagerItf& imageManager, + common::imagemanager::ItemStatusProviderItf& itemStatusProvider, launcher::LauncherItf& launcher, UnitStatusHandler& unitStatusHandler, StorageItf& storage) { LOG_DBG() << "Init desired status handler"; - mAllocator = &allocator; - mNodeHandler = &nodeHandler; - mUnitConfig = &unitConfig; - mUnitStatusHandler = &unitStatusHandler; - mLauncher = &launcher; - mImageManager = &imageManager; - mStorage = &storage; + mAllocator = &allocator; + mNodeHandler = &nodeHandler; + mUnitConfig = &unitConfig; + mUnitStatusHandler = &unitStatusHandler; + mLauncher = &launcher; + mImageManager = &imageManager; + mItemStatusProvider = &itemStatusProvider; + mStorage = &storage; return ErrorEnum::eNone; } @@ -86,7 +146,8 @@ Error DesiredStatusHandler::Stop() return ErrorEnum::eWrongState; } - if (mUpdateState == UpdateStateEnum::eDownloading || mUpdateState == UpdateStateEnum::eInstalling) { + if (mUpdateState == UpdateStateEnum::eDownloading || mUpdateState == UpdateStateEnum::eInstalling + || mUpdateState == UpdateStateEnum::eRecovering) { if (auto cancelErr = mImageManager->Cancel(); !cancelErr.IsNone() && err.IsNone()) { err = AOS_ERROR_WRAP(cancelErr); } @@ -178,7 +239,8 @@ void DesiredStatusHandler::CancelUpdate() mCancelCurrentUpdate = true; - if (mUpdateState == UpdateStateEnum::eDownloading || mUpdateState == UpdateStateEnum::eInstalling) { + if (mUpdateState == UpdateStateEnum::eDownloading || mUpdateState == UpdateStateEnum::eInstalling + || mUpdateState == UpdateStateEnum::eRecovering) { if (auto err = mImageManager->Cancel(); !err.IsNone()) { LOG_ERR() << "Failed to cancel current update" << Log::Field(err); } @@ -203,6 +265,8 @@ void DesiredStatusHandler::Run() mCurrentDesiredStatus = mPendingDesiredStatus; mHasPendingDesiredStatus = false; + mRecoveryRequired = false; + mRecoveryDone = false; while (mUpdateState != UpdateStateEnum::eNone && mIsRunning) { Error (DesiredStatusHandler::*stateAction)() = nullptr; @@ -210,7 +274,7 @@ void DesiredStatusHandler::Run() switch (mUpdateState.GetValue()) { case UpdateStateEnum::eDownloading: - stateAction = &DesiredStatusHandler::DownloadUpdateItems; + stateAction = &DesiredStatusHandler::InstallUpdateItems; nextState = UpdateStateEnum::ePending; break; @@ -239,6 +303,12 @@ void DesiredStatusHandler::Run() break; + case UpdateStateEnum::eRecovering: + stateAction = &DesiredStatusHandler::RecoverItems; + nextState = UpdateStateEnum::eLaunching; + + break; + case UpdateStateEnum::eFinalizing: stateAction = &DesiredStatusHandler::FinalizeUpdate; nextState = UpdateStateEnum::eNone; @@ -268,6 +338,9 @@ void DesiredStatusHandler::Run() LOG_ERR() << "Failed to process desired status" << Log::Field(err); nextState = UpdateStateEnum::eNone; + } else if (mRecoveryRequired) { + mRecoveryRequired = false; + nextState = UpdateStateEnum::eRecovering; } } @@ -341,35 +414,170 @@ void DesiredStatusHandler::SetState(const UpdateState& state) mUpdateState = state; } -Error DesiredStatusHandler::DownloadUpdateItems() +Error DesiredStatusHandler::InstallUpdateItems() { - auto itemsStatuses = MakeUnique>(mAllocator); - if (!itemsStatuses) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + LOG_INF() << "[profiling] Install update items start" + << Log::Field("count", mCurrentDesiredStatus.mUpdateItems.Size()); + + if (auto err + = mImageManager->SetCerts(mCurrentDesiredStatus.mCertificates, mCurrentDesiredStatus.mCertificateChains); + !err.IsNone()) { + return AOS_ERROR_WRAP(err); } - LOG_INF() << "[profiling] Download update items start" - << Log::Field("count", mCurrentDesiredStatus.mUpdateItems.Size()); + { + auto itemsStatuses = MakeUnique(mAllocator); + if (!itemsStatuses) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (auto err = mItemStatusProvider->GetItemsStatuses(*itemsStatuses); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mThreadPool.Run(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + for (const auto& item : mCurrentDesiredStatus.mUpdateItems) { + const auto* itemStatus = FindItemStatus(item, *itemsStatuses); + if (itemStatus != nullptr && !IsItemInstallRequired(*itemStatus)) { + LOG_DBG() << "Skip install update item" << Log::Field("id", item.mItemID) + << Log::Field("version", item.mVersion) << Log::Field("state", itemStatus->mState); + + continue; + } + + AddInstallTask(item); + } + } - if (auto err = mImageManager->DownloadUpdateItems(mCurrentDesiredStatus.mUpdateItems, - mCurrentDesiredStatus.mCertificates, mCurrentDesiredStatus.mCertificateChains, *itemsStatuses); + auto err = mThreadPool.Wait(); + + if (auto shutdownErr = mThreadPool.Shutdown(); !shutdownErr.IsNone() && err.IsNone()) { + err = shutdownErr; + } + + LOG_INF() << "[profiling] Install update items end"; + + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error DesiredStatusHandler::RecoverItems() +{ + LOG_INF() << "[profiling] Recover update items start"; + + mRecoveryDone = true; + + if (auto err + = mImageManager->SetCerts(mCurrentDesiredStatus.mCertificates, mCurrentDesiredStatus.mCertificateChains); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - for (const auto& itemStatus : *itemsStatuses) { - if (itemStatus.mState == ItemStateEnum::eFailed) { - LOG_ERR() << "Failed to download update item" << Log::Field("id", itemStatus.mItemID) - << Log::Field("type", itemStatus.mType) << Log::Field("version", itemStatus.mVersion) - << Log::Field(itemStatus.mError); + { + auto itemsStatuses = MakeUnique(mAllocator); + if (!itemsStatuses) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (auto err = mItemStatusProvider->GetItemsStatuses(*itemsStatuses); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mThreadPool.Run(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + for (const auto& item : mCurrentDesiredStatus.mUpdateItems) { + const auto* itemStatus = FindItemStatus(item, *itemsStatuses); + if (itemStatus == nullptr || itemStatus->mState != ItemStateEnum::eFailed + || !itemStatus->mError.Is(ErrorEnum::eIntegrity)) { + continue; + } + + LOG_WRN() << "Reinstall update item with integrity error" << Log::Field("id", item.mItemID) + << Log::Field("version", item.mVersion); + + AddInstallTask(item); } } - LOG_INF() << "[profiling] Download update items end"; + auto err = mThreadPool.Wait(); + + if (auto shutdownErr = mThreadPool.Shutdown(); !shutdownErr.IsNone() && err.IsNone()) { + err = shutdownErr; + } + + LOG_INF() << "[profiling] Recover update items end"; + + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } return ErrorEnum::eNone; } +void DesiredStatusHandler::SetItemsFailed(const Array& instancesStatuses) +{ + for (const auto& item : mCurrentDesiredStatus.mUpdateItems) { + const auto* failureCause = FindItemFailureCause(item, instancesStatuses); + if (failureCause == nullptr) { + continue; + } + + if (failureCause->mError.Is(ErrorEnum::eIntegrity) && !mRecoveryDone) { + mRecoveryRequired = true; + } + + LOG_ERR() << "Set update item failed" << Log::Field("id", item.mItemID) << Log::Field("version", item.mVersion) + << Log::Field("err", failureCause->mError); + + if (auto err = mItemStatusProvider->SetItemFail(item.mItemID, item.mVersion, failureCause->mError); + !err.IsNone()) { + LOG_ERR() << "Failed to set update item failed" << Log::Field("id", item.mItemID) + << Log::Field("version", item.mVersion) << Log::Field(err); + } + } +} + +void DesiredStatusHandler::AddInstallTask(const UpdateItemInfo& item) +{ + if (auto err = mThreadPool.AddTask([this, &item](void*) { InstallItem(item); }); !err.IsNone()) { + LOG_ERR() << "Failed to add install update item task" << Log::Field("id", item.mItemID) + << Log::Field("version", item.mVersion) << Log::Field(err); + } +} + +void DesiredStatusHandler::InstallItem(const UpdateItemInfo& item) +{ + { + LockGuard lock {mMutex}; + + if (!mIsRunning || mCancelCurrentUpdate) { + LOG_DBG() << "Skip install update item" << Log::Field("id", item.mItemID) + << Log::Field("version", item.mVersion); + + return; + } + } + + LOG_DBG() << "Install update item" << Log::Field("id", item.mItemID) << Log::Field("type", item.mType) + << Log::Field("version", item.mVersion); + + if (auto err = mImageManager->InstallItem(item); err.Is(ErrorEnum::eCanceled)) { + LOG_INF() << "Install update item canceled" << Log::Field("id", item.mItemID) + << Log::Field("version", item.mVersion); + } else if (!err.IsNone()) { + LOG_ERR() << "Failed to install update item" << Log::Field("id", item.mItemID) << Log::Field("type", item.mType) + << Log::Field("version", item.mVersion) << Log::Field(err); + } +} + Error DesiredStatusHandler::InstallDesiredStatus() { LOG_INF() << "[profiling] Install desired status start"; @@ -441,8 +649,14 @@ Error DesiredStatusHandler::LaunchInstances() [&desiredInstance](const UpdateItemInfo& item) { return item.mItemID == desiredInstance.mItemID; }); it != mCurrentDesiredStatus.mUpdateItems.end()) { request.mVersion = it->mVersion; + request.mIndexDigest = it->mIndexDigest; request.mOwnerID = it->mOwnerID; request.mUpdateItemType = it->mType; + + if (it->mIndexDigest.IsEmpty()) { + LOG_WRN() << "Update item has empty index digest" << Log::Field("itemID", it->mItemID) + << Log::Field("version", it->mVersion); + } } else { LOG_ERR() << "Update item for instance not found" << Log::Field("itemID", desiredInstance.mItemID); } @@ -520,6 +734,8 @@ Error DesiredStatusHandler::WaitInstancesActive() } } + SetItemsFailed(*instancesStatuses); + LOG_INF() << "[profiling] Wait instances active end"; return ErrorEnum::eNone; @@ -527,7 +743,7 @@ Error DesiredStatusHandler::WaitInstancesActive() Error DesiredStatusHandler::FinalizeUpdate() { - auto itemsStatuses = MakeUnique>(mAllocator); + auto itemsStatuses = MakeUnique(mAllocator); if (!itemsStatuses) { return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); } @@ -535,24 +751,59 @@ Error DesiredStatusHandler::FinalizeUpdate() LOG_INF() << "[profiling] Finalize desired status start" << Log::Field("count", mCurrentDesiredStatus.mUpdateItems.Size()); - if (auto err = mImageManager->InstallUpdateItems(mCurrentDesiredStatus.mUpdateItems, *itemsStatuses); - !err.IsNone()) { + if (auto err = mItemStatusProvider->GetItemsStatuses(*itemsStatuses); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mThreadPool.Run(); !err.IsNone()) { return AOS_ERROR_WRAP(err); } for (const auto& itemStatus : *itemsStatuses) { - if (itemStatus.mState == ItemStateEnum::eFailed) { - LOG_ERR() << "Failed to install update item" << Log::Field("id", itemStatus.mItemID) - << Log::Field("type", itemStatus.mType) << Log::Field("version", itemStatus.mVersion) - << Log::Field(itemStatus.mError); + if (itemStatus.mState == ItemStateEnum::eRemoved) { + continue; + } + + if (auto it = mCurrentDesiredStatus.mUpdateItems.FindIf([&itemStatus](const UpdateItemInfo& desiredItem) { + return desiredItem.mItemID == itemStatus.mItemID && desiredItem.mVersion == itemStatus.mVersion; + }); + it != mCurrentDesiredStatus.mUpdateItems.end()) { + continue; } + + if (auto err = mThreadPool.AddTask([this, &itemStatus](void*) { RemoveItem(itemStatus); }); !err.IsNone()) { + LOG_ERR() << "Failed to add remove update item task" << Log::Field("id", itemStatus.mItemID) + << Log::Field("version", itemStatus.mVersion) << Log::Field(err); + } + } + + auto err = mThreadPool.Wait(); + + if (auto shutdownErr = mThreadPool.Shutdown(); !shutdownErr.IsNone() && err.IsNone()) { + err = shutdownErr; } LOG_INF() << "[profiling] Finalize desired status end"; + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + return ErrorEnum::eNone; } +void DesiredStatusHandler::RemoveItem(const UpdateItemStatus& itemStatus) +{ + LOG_DBG() << "Remove update item" << Log::Field("id", itemStatus.mItemID) << Log::Field("type", itemStatus.mType) + << Log::Field("version", itemStatus.mVersion); + + if (auto err = mImageManager->RemoveItem(itemStatus.mItemID, itemStatus.mVersion); !err.IsNone()) { + LOG_ERR() << "Failed to remove update item" << Log::Field("id", itemStatus.mItemID) + << Log::Field("type", itemStatus.mType) << Log::Field("version", itemStatus.mVersion) + << Log::Field(err); + } +} + bool DesiredStatusHandler::IsUpdateRequired(const DesiredStatus& desiredStatus) const { if (!desiredStatus.mNodes.IsEmpty()) { @@ -576,14 +827,14 @@ bool DesiredStatusHandler::IsUpdateRequired(const DesiredStatus& desiredStatus) bool DesiredStatusHandler::IsUpdateItemsRequired(const DesiredStatus& desiredStatus) const { - auto itemsStatuses = MakeUnique>(mAllocator); + auto itemsStatuses = MakeUnique(mAllocator); if (!itemsStatuses) { LOG_ERR() << "Failed to allocate update items statuses" << Log::Field(ErrorEnum::eNoMemory); return true; } - if (auto err = mImageManager->GetUpdateItemsStatuses(*itemsStatuses); !err.IsNone()) { + if (auto err = mItemStatusProvider->GetItemsStatuses(*itemsStatuses); !err.IsNone()) { LOG_ERR() << "Failed to get update items statuses" << Log::Field(err); return true; @@ -592,7 +843,7 @@ bool DesiredStatusHandler::IsUpdateItemsRequired(const DesiredStatus& desiredSta for (const auto& desiredItem : desiredStatus.mUpdateItems) { if (auto it = itemsStatuses->FindIf([&desiredItem](const UpdateItemStatus& itemStatus) { return itemStatus.mItemID == desiredItem.mItemID && itemStatus.mType == desiredItem.mType - && itemStatus.mVersion == desiredItem.mVersion && itemStatus.mState == ItemStateEnum::eInstalled; + && itemStatus.mVersion == desiredItem.mVersion && !IsItemInstallRequired(itemStatus); }); it == itemsStatuses->end()) { return true; @@ -600,7 +851,7 @@ bool DesiredStatusHandler::IsUpdateItemsRequired(const DesiredStatus& desiredSta } for (const auto& itemStatus : *itemsStatuses) { - if (itemStatus.mState == ItemStateEnum::eInstalled) { + if (itemStatus.mState == ItemStateEnum::eInstalled || itemStatus.mState == ItemStateEnum::eFailed) { if (auto it = desiredStatus.mUpdateItems.FindIf([&itemStatus](const UpdateItemInfo& desiredItem) { return desiredItem.mItemID == itemStatus.mItemID && desiredItem.mType == itemStatus.mType && desiredItem.mVersion == itemStatus.mVersion; diff --git a/src/core/cm/updatemanager/desiredstatushandler.hpp b/src/core/cm/updatemanager/desiredstatushandler.hpp index 9d8b14bf5..9d69819b6 100644 --- a/src/core/cm/updatemanager/desiredstatushandler.hpp +++ b/src/core/cm/updatemanager/desiredstatushandler.hpp @@ -9,7 +9,9 @@ #include #include +#include #include +#include #include #include #include @@ -35,14 +37,15 @@ class DesiredStatusHandler : private instancestatusprovider::ListenerItf { * @param nodeHandler node handler. * @param unitConfig unit config interface. * @param imageManager image manager. + * @param itemStatusProvider item status provider. * @param launcher launcher interface. * @param unitStatusHandler unit status handler. * @param storage storage interface. * @return Error. */ Error Init(AllocatorItf& allocator, iamclient::NodeHandlerItf& nodeHandler, unitconfig::UnitConfigItf& unitConfig, - imagemanager::ImageManagerItf& imageManager, launcher::LauncherItf& launcher, - UnitStatusHandler& unitStatusHandler, StorageItf& storage); + imagemanager::ImageManagerItf& imageManager, common::imagemanager::ItemStatusProviderItf& itemStatusProvider, + launcher::LauncherItf& launcher, UnitStatusHandler& unitStatusHandler, StorageItf& storage); /** * Starts desired status handler. @@ -75,7 +78,12 @@ class DesiredStatusHandler : private instancestatusprovider::ListenerItf { void Run(); void LogDesiredStatus(const DesiredStatus& desiredStatus) const; void SetState(const UpdateState& state); - Error DownloadUpdateItems(); + Error InstallUpdateItems(); + Error RecoverItems(); + void AddInstallTask(const UpdateItemInfo& item); + void SetItemsFailed(const Array& instancesStatuses); + void InstallItem(const UpdateItemInfo& item); + void RemoveItem(const UpdateItemStatus& itemStatus); Error InstallDesiredStatus(); Error LaunchInstances(); Error WaitInstancesActive(); @@ -87,22 +95,26 @@ class DesiredStatusHandler : private instancestatusprovider::ListenerItf { bool IsUpdateItemsRequired(const DesiredStatus& desiredStatus) const; bool IsUpdateInstancesRequired(const DesiredStatus& desiredStatus) const; - iamclient::NodeHandlerItf* mNodeHandler {}; - unitconfig::UnitConfigItf* mUnitConfig {}; - imagemanager::ImageManagerItf* mImageManager {}; - launcher::LauncherItf* mLauncher {}; - UnitStatusHandler* mUnitStatusHandler {}; - StorageItf* mStorage {}; - - Mutex mMutex; - ConditionalVariable mCondVar; - Thread<> mThread; - DesiredStatus mCurrentDesiredStatus; - DesiredStatus mPendingDesiredStatus; + iamclient::NodeHandlerItf* mNodeHandler {}; + unitconfig::UnitConfigItf* mUnitConfig {}; + imagemanager::ImageManagerItf* mImageManager {}; + common::imagemanager::ItemStatusProviderItf* mItemStatusProvider {}; + launcher::LauncherItf* mLauncher {}; + UnitStatusHandler* mUnitStatusHandler {}; + StorageItf* mStorage {}; + + Mutex mMutex; + ConditionalVariable mCondVar; + Thread<> mThread; + ThreadPool mThreadPool; + DesiredStatus mCurrentDesiredStatus; + DesiredStatus mPendingDesiredStatus; bool mIsRunning {}; bool mHasPendingDesiredStatus {}; bool mCancelCurrentUpdate {}; + bool mRecoveryRequired {}; + bool mRecoveryDone {}; UpdateState mUpdateState {}; AllocatorItf* mAllocator {}; diff --git a/src/core/cm/updatemanager/itf/storage.hpp b/src/core/cm/updatemanager/itf/storage.hpp index b72740ad0..58c635d25 100644 --- a/src/core/cm/updatemanager/itf/storage.hpp +++ b/src/core/cm/updatemanager/itf/storage.hpp @@ -28,6 +28,7 @@ class UpdateStateType { eInstalling, eLaunching, eWaitingActive, + eRecovering, eFinalizing, }; @@ -40,6 +41,7 @@ class UpdateStateType { "installing", "launching", "activating", + "recovering", "finalizing", }; diff --git a/src/core/cm/updatemanager/tests/mocks/imagemanagermock.hpp b/src/core/cm/updatemanager/tests/mocks/imagemanagermock.hpp index ac30487b6..7dbcb2794 100644 --- a/src/core/cm/updatemanager/tests/mocks/imagemanagermock.hpp +++ b/src/core/cm/updatemanager/tests/mocks/imagemanagermock.hpp @@ -18,15 +18,11 @@ namespace aos::cm::imagemanager { */ class ImageManagerMock : public ImageManagerItf { public: - MOCK_METHOD(Error, DownloadUpdateItems, - (const Array& itemsInfo, const Array&, - const Array&, Array&), + MOCK_METHOD(Error, SetCerts, (const Array&, const Array&), (override)); - MOCK_METHOD(Error, InstallUpdateItems, (const Array&, Array&), (override)); + MOCK_METHOD(Error, InstallItem, (const UpdateItemInfo&), (override)); + MOCK_METHOD(Error, RemoveItem, (const String&, const String&), (override)); MOCK_METHOD(Error, Cancel, (), (override)); - MOCK_METHOD(Error, GetUpdateItemsStatuses, (Array&), (override)); - MOCK_METHOD(Error, SubscribeListener, (ItemStatusListenerItf&), (override)); - MOCK_METHOD(Error, UnsubscribeListener, (ItemStatusListenerItf&), (override)); }; } // namespace aos::cm::imagemanager diff --git a/src/core/cm/updatemanager/tests/updatemanager.cpp b/src/core/cm/updatemanager/tests/updatemanager.cpp index 4969ccccb..0b6ae8f37 100644 --- a/src/core/cm/updatemanager/tests/updatemanager.cpp +++ b/src/core/cm/updatemanager/tests/updatemanager.cpp @@ -4,11 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include +#include +#include +#include + #include #include #include #include +#include #include #include #include @@ -254,6 +260,15 @@ void CreateInstanceStatus(InstanceStatus& instanceStatus, const String& itemID, instanceStatus.mError = unitInstanceStatus.mError; }; +StaticString CreateIndexDigest(const String& itemID, const String& version) +{ + StaticString digest; + + digest = (std::string("sha256:") + itemID.CStr() + "-" + version.CStr()).c_str(); + + return digest; +} + void CreateUpdateItemInfo( DesiredStatus& desiredStatus, const String& itemID, UpdateItemType updateItemType, const String& version) { @@ -261,9 +276,10 @@ void CreateUpdateItemInfo( auto& itemInfo = desiredStatus.mUpdateItems.Back(); - itemInfo.mItemID = itemID; - itemInfo.mType = updateItemType; - itemInfo.mVersion = version; + itemInfo.mItemID = itemID; + itemInfo.mType = updateItemType; + itemInfo.mVersion = version; + itemInfo.mIndexDigest = CreateIndexDigest(itemID, version); } void CreateRunRequest(const DesiredStatus& desiredStatus, Array& runRequest) @@ -277,6 +293,7 @@ void CreateRunRequest(const DesiredStatus& desiredStatus, ArraymVersion; + request.mIndexDigest = CreateIndexDigest(desiredInstance.mItemID, it->mVersion); request.mOwnerID = it->mOwnerID; request.mUpdateItemType = it->mType; } @@ -316,6 +333,7 @@ void ConvertInstancesStatuses( status.mRuntimeID = instanceStatus->mRuntimeID; status.mManifestDigest = instanceStatus->mManifestDigest; status.mState = instanceStatus->mState; + status.mError = instanceStatus->mError; status.mPreinstalled = unitInstanceStatus.mPreinstalled; instancesStatuses.PushBack(status); @@ -323,6 +341,16 @@ void ConvertInstancesStatuses( } } +void SetInstancesFailed(Array& instances, const String& itemID, const Error& error) +{ + for (auto& instance : instances) { + if (instance.mItemID == itemID) { + instance.mState = InstanceStateEnum::eFailed; + instance.mError = error; + } + } +} + } // namespace /*********************************************************************************************************************** @@ -345,7 +373,8 @@ class UpdateManagerTest : public Test { Config config {cUnitStatusSendTimeout}; auto err = mUpdateManager.Init(mAllocator, config, mIdentProviderMock, mNodeHandlerMock, mUnitConfigMock, - mNodeInfoProviderMock, mImageManagerMock, mLauncherMock, mCloudConnectionMock, mSenderStub, mStorageStub); + mNodeInfoProviderMock, mImageManagerMock, mItemStatusProviderMock, mLauncherMock, mCloudConnectionMock, + mSenderStub, mStorageStub); EXPECT_TRUE(err.IsNone()) << "Failed to initialize update manager: " << tests::utils::ErrorToStr(err); EXPECT_CALL(mCloudConnectionMock, SubscribeListener(_)) @@ -360,8 +389,8 @@ class UpdateManagerTest : public Test { return ErrorEnum::eNone; })); - EXPECT_CALL(mImageManagerMock, SubscribeListener(_)) - .WillRepeatedly(Invoke([&](imagemanager::ItemStatusListenerItf& listener) { + EXPECT_CALL(mItemStatusProviderMock, SubscribeListener(_)) + .WillRepeatedly(Invoke([&](common::imagemanager::ItemStatusListenerItf& listener) { mItemStatusListener = &listener; return ErrorEnum::eNone; @@ -401,8 +430,8 @@ class UpdateManagerTest : public Test { return ErrorEnum::eNone; })); - EXPECT_CALL(mImageManagerMock, UnsubscribeListener(_)) - .WillOnce(Invoke([&](imagemanager::ItemStatusListenerItf& listener) { + EXPECT_CALL(mItemStatusProviderMock, UnsubscribeListener(_)) + .WillOnce(Invoke([&](common::imagemanager::ItemStatusListenerItf& listener) { (void)listener; mItemStatusListener = nullptr; @@ -434,22 +463,23 @@ class UpdateManagerTest : public Test { // members are destroyed in reverse declaration order. HeapAllocator mAllocator; - UpdateManager mUpdateManager; - NiceMock mIdentProviderMock; - NiceMock mNodeHandlerMock; - NiceMock mUnitConfigMock; - NiceMock mNodeInfoProviderMock; - NiceMock mImageManagerMock; - NiceMock mLauncherMock; - NiceMock mCloudConnectionMock; - SenderStub mSenderStub; - StorageStub mStorageStub; - - cloudconnection::ConnectionListenerItf* mConnectionListener {}; - nodeinfoprovider::NodeInfoListenerItf* mNodeInfoListener {}; - imagemanager::ItemStatusListenerItf* mItemStatusListener {}; - instancestatusprovider::ListenerItf* mInstanceStatusListener {}; - iamclient::SubjectsListenerItf* mSubjectsListener {}; + UpdateManager mUpdateManager; + NiceMock mIdentProviderMock; + NiceMock mNodeHandlerMock; + NiceMock mUnitConfigMock; + NiceMock mNodeInfoProviderMock; + NiceMock mImageManagerMock; + NiceMock mItemStatusProviderMock; + NiceMock mLauncherMock; + NiceMock mCloudConnectionMock; + SenderStub mSenderStub; + StorageStub mStorageStub; + + cloudconnection::ConnectionListenerItf* mConnectionListener {}; + nodeinfoprovider::NodeInfoListenerItf* mNodeInfoListener {}; + common::imagemanager::ItemStatusListenerItf* mItemStatusListener {}; + instancestatusprovider::ListenerItf* mInstanceStatusListener {}; + iamclient::SubjectsListenerItf* mSubjectsListener {}; }; /*********************************************************************************************************************** @@ -509,7 +539,7 @@ TEST_F(UpdateManagerTest, SendUnitStatusOnCloudConnect) CreateUpdateItemStatus(*expectedUnitStatus, "item2", UpdateItemTypeEnum::eService, "1.0.0"); CreateUpdateItemStatus(*expectedUnitStatus, "item3", UpdateItemTypeEnum::eComponent, "1.0.0"); - EXPECT_CALL(mImageManagerMock, GetUpdateItemsStatuses(_)) + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) .WillOnce(DoAll(SetArgReferee<0>(expectedUnitStatus->mUpdateItems.GetValue()), Return(ErrorEnum::eNone))); // Set instances statuses @@ -600,14 +630,18 @@ TEST_F(UpdateManagerTest, SendDeltaUnitStatus) // Notify items statuses changed - mItemStatusListener->OnItemsStatusesChanged(*expectedUnitStatus->mUpdateItems); + for (const auto& itemStatus : *expectedUnitStatus->mUpdateItems) { + mItemStatusListener->OnItemStatusChanged(itemStatus); + } ChangeUpdateItemStatus(*expectedUnitStatus, "item3", "1.0.0", ItemStateEnum::eInstalled); ChangeUpdateItemStatus(*expectedUnitStatus, "item4", "1.0.0", ItemStateEnum::eInstalled); // Notify items statuses changed - mItemStatusListener->OnItemsStatusesChanged(*expectedUnitStatus->mUpdateItems); + for (const auto& itemStatus : *expectedUnitStatus->mUpdateItems) { + mItemStatusListener->OnItemStatusChanged(itemStatus); + } EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); @@ -751,6 +785,15 @@ TEST_F(UpdateManagerTest, ProcessFullDesiredStatus) CreateUpdateItemInfo(*desiredStatus, "item2", UpdateItemTypeEnum::eService, "1.0.0"); CreateUpdateItemInfo(*desiredStatus, "item3", UpdateItemTypeEnum::eService, "1.0.0"); + // Set desired certificates + + desiredStatus->mCertificates.EmplaceBack(); + desiredStatus->mCertificates.Back().mFingerprint = "fingerprint1"; + + desiredStatus->mCertificateChains.EmplaceBack(); + desiredStatus->mCertificateChains.Back().mName = "chain1"; + desiredStatus->mCertificateChains.Back().mFingerprints.EmplaceBack("fingerprint1"); + // Set desired instances desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 1, {}}); @@ -797,9 +840,31 @@ TEST_F(UpdateManagerTest, ProcessFullDesiredStatus) EXPECT_CALL(mNodeHandlerMock, ResumeNode(String("node2"))).Times(1); EXPECT_CALL(mUnitConfigMock, CheckUnitConfig(desiredStatus->mUnitConfig.GetValue())).Times(1); EXPECT_CALL(mUnitConfigMock, UpdateUnitConfig(desiredStatus->mUnitConfig.GetValue())).Times(1); - EXPECT_CALL(mImageManagerMock, DownloadUpdateItems(desiredStatus->mUpdateItems, _, _, _)).Times(1); - EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).Times(1); - EXPECT_CALL(mImageManagerMock, InstallUpdateItems(desiredStatus->mUpdateItems, _)).Times(1); + EXPECT_CALL(mImageManagerMock, SetCerts(desiredStatus->mCertificates, desiredStatus->mCertificateChains)) + .WillOnce(Return(ErrorEnum::eNone)); + + for (const auto& item : desiredStatus->mUpdateItems) { + EXPECT_CALL(mImageManagerMock, InstallItem(item)).WillOnce(Return(ErrorEnum::eNone)); + } + + std::atomic_bool instancesLaunched {false}; + + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).WillOnce(InvokeWithoutArgs([&]() { + instancesLaunched = true; + + return ErrorEnum::eNone; + })); + + // Set stored items: item4 is not in desired status and should be removed, item5 is already removed + + std::atomic_bool item4Removed {false}; + + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mImageManagerMock, RemoveItem(String("item4"), String("1.0.0"))).WillOnce(InvokeWithoutArgs([&]() { + item4Removed = true; + + return ErrorEnum::eNone; + })); EXPECT_CALL(mNodeInfoProviderMock, GetAllNodeIDs(_)).WillOnce(Invoke([&](Array>& nodeIDs) { nodeIDs.EmplaceBack("node1"); @@ -821,8 +886,26 @@ TEST_F(UpdateManagerTest, ProcessFullDesiredStatus) })); EXPECT_CALL(mUnitConfigMock, GetUnitConfigStatus(_)) .WillOnce(DoAll(SetArgReferee<0>(expectedUnitStatus->mUnitConfig.GetValue()[0]), Return(ErrorEnum::eNone))); - EXPECT_CALL(mImageManagerMock, GetUpdateItemsStatuses(_)) - .WillOnce(DoAll(SetArgReferee<0>(expectedUnitStatus->mUpdateItems.GetValue()), Return(ErrorEnum::eNone))); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + // Items are reported only after install, otherwise they are skipped by install step + + if (!instancesLaunched) { + return ErrorEnum::eNone; + } + + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + + if (!item4Removed) { + statuses.EmplaceBack(UpdateItemStatus {"item4", UpdateItemTypeEnum::eService, "1.0.0", false, + ItemStateEnum::eInstalled, ErrorEnum::eNone}); + } + + statuses.EmplaceBack(UpdateItemStatus { + "item5", UpdateItemTypeEnum::eService, "1.0.0", false, ItemStateEnum::eRemoved, ErrorEnum::eNone}); + + return ErrorEnum::eNone; + })); EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)) .Times(2) .WillRepeatedly(Invoke([&](Array& instances) { @@ -880,15 +963,15 @@ TEST_F(UpdateManagerTest, CancelCurrentUpdate) std::condition_variable cv; std::mutex mutex; - bool cancel = false; - bool downloadStarted = false; + bool cancel = false; + bool installStarted = false; - EXPECT_CALL(mImageManagerMock, DownloadUpdateItems(_, _, _, _)) - .WillOnce(Invoke([&](const Array&, const Array&, - const Array&, Array&) -> Error { + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])) + .WillOnce(Invoke([&](const UpdateItemInfo&) -> Error { std::unique_lock lock(mutex); - downloadStarted = true; + installStarted = true; cv.notify_one(); EXPECT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return cancel; })); @@ -908,12 +991,12 @@ TEST_F(UpdateManagerTest, CancelCurrentUpdate) auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); - // wait for download to start + // wait for install to start { std::unique_lock lock(mutex); - ASSERT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return downloadStarted; })); + ASSERT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return installStarted; })); } // Send new desired status to cancel current update @@ -930,10 +1013,30 @@ TEST_F(UpdateManagerTest, CancelCurrentUpdate) CreateRunRequest(*desiredStatus, *runRequest); - EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).Times(1); - EXPECT_CALL(mImageManagerMock, InstallUpdateItems(desiredStatus->mUpdateItems, _)).Times(1); - EXPECT_CALL(mImageManagerMock, GetUpdateItemsStatuses(_)) - .WillOnce(DoAll(SetArgReferee<0>(expectedUnitStatus->mUpdateItems.GetValue()), Return(ErrorEnum::eNone))); + std::atomic_bool instancesLaunched {false}; + + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).WillOnce(InvokeWithoutArgs([&]() { + instancesLaunched = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + if (instancesLaunched) { + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + + return ErrorEnum::eNone; + } + + // The canceled install is reported as failed with the canceled error and has to be installed again + + statuses.EmplaceBack( + UpdateItemStatus {desiredStatus->mUpdateItems[0].mItemID, desiredStatus->mUpdateItems[0].mType, + desiredStatus->mUpdateItems[0].mVersion, false, ItemStateEnum::eFailed, ErrorEnum::eCanceled}); + + return ErrorEnum::eNone; + })); EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)) .Times(2) .WillRepeatedly(Invoke([&](Array& instances) { @@ -974,15 +1077,15 @@ TEST_F(UpdateManagerTest, ResumeUpdateAfterRestart) std::condition_variable cv; std::mutex mutex; - bool continueUpdate = false; - bool downloadStarted = false; + bool continueUpdate = false; + bool installStarted = false; - EXPECT_CALL(mImageManagerMock, DownloadUpdateItems(_, _, _, _)) - .WillOnce(Invoke([&](const Array&, const Array&, - const Array&, Array&) -> Error { + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])) + .WillOnce(Invoke([&](const UpdateItemInfo&) -> Error { std::unique_lock lock(mutex); - downloadStarted = true; + installStarted = true; cv.notify_one(); EXPECT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return continueUpdate; })); @@ -993,12 +1096,12 @@ TEST_F(UpdateManagerTest, ResumeUpdateAfterRestart) err = mUpdateManager.Start(); EXPECT_TRUE(err.IsNone()) << "Failed to start update manager: " << tests::utils::ErrorToStr(err); - // wait for download to start + // wait for install to start { std::unique_lock lock(mutex); - ASSERT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return downloadStarted; })); + ASSERT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return installStarted; })); } // Notify cloud connection established and wait for unit status @@ -1017,9 +1120,9 @@ TEST_F(UpdateManagerTest, ResumeUpdateAfterRestart) CreateRunRequest(*desiredStatus, *runRequest); EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).Times(1); - EXPECT_CALL(mImageManagerMock, InstallUpdateItems(desiredStatus->mUpdateItems, _)).Times(1); - EXPECT_CALL(mImageManagerMock, GetUpdateItemsStatuses(_)) - .WillOnce(DoAll(SetArgReferee<0>(expectedUnitStatus->mUpdateItems.GetValue()), Return(ErrorEnum::eNone))); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(DoAll(SetArgReferee<0>(expectedUnitStatus->mUpdateItems.GetValue()), Return(ErrorEnum::eNone))); EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)) .Times(2) .WillRepeatedly(Invoke([&](Array& instances) { @@ -1129,4 +1232,1116 @@ TEST_F(UpdateManagerTest, SetUpdateStatuses) EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); } +TEST_F(UpdateManagerTest, InstallItemFailed) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + CreateUpdateItemInfo(*desiredStatus, "item2", UpdateItemTypeEnum::eComponent, "1.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 1, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses + + CreateUpdateItemStatus( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateUpdateItemStatus( + *expectedUnitStatus, "item2", UpdateItemTypeEnum::eComponent, "1.0.0", ItemStateEnum::eFailed); + CreateInstancesStatuses(*expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 1); + + // Create launcher run request + + auto runRequest = std::make_unique>(); + + CreateRunRequest(*desiredStatus, *runRequest); + + // Expect calls + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[1])).WillOnce(Return(ErrorEnum::eFailed)); + std::atomic_bool instancesLaunched {false}; + + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).WillOnce(InvokeWithoutArgs([&]() { + instancesLaunched = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + // Items are reported only after install, otherwise they are skipped by install step + + if (instancesLaunched) { + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + } + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + ConvertInstancesStatuses(expectedUnitStatus->mInstances.GetValue(), instances); + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); +} + +TEST_F(UpdateManagerTest, InstallSkipsInstalledAndFailedItems) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "installed", UpdateItemTypeEnum::eService, "1.0.0"); + CreateUpdateItemInfo(*desiredStatus, "failed", UpdateItemTypeEnum::eService, "1.0.0"); + CreateUpdateItemInfo(*desiredStatus, "missing", UpdateItemTypeEnum::eService, "1.0.0"); + CreateUpdateItemInfo(*desiredStatus, "removed", UpdateItemTypeEnum::eService, "1.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"installed", "subject1", 0, 1, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses + + CreateUpdateItemStatus( + *expectedUnitStatus, "installed", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateUpdateItemStatus( + *expectedUnitStatus, "failed", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eFailed); + CreateUpdateItemStatus( + *expectedUnitStatus, "missing", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateUpdateItemStatus( + *expectedUnitStatus, "removed", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateInstancesStatuses(*expectedUnitStatus, "installed", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 1); + + // Create launcher run request + + auto runRequest = std::make_unique>(); + + CreateRunRequest(*desiredStatus, *runRequest); + + // Expect calls: only missing and removed items are installed + + std::atomic_bool instancesLaunched {false}; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(_)).Times(0); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[2])).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[3])).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).WillOnce(InvokeWithoutArgs([&]() { + instancesLaunched = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + if (instancesLaunched) { + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + + return ErrorEnum::eNone; + } + + statuses.EmplaceBack(UpdateItemStatus {"installed", UpdateItemTypeEnum::eService, "1.0.0", false, + ItemStateEnum::eInstalled, ErrorEnum::eNone}); + statuses.EmplaceBack(UpdateItemStatus { + "failed", UpdateItemTypeEnum::eService, "1.0.0", false, ItemStateEnum::eFailed, ErrorEnum::eFailed}); + statuses.EmplaceBack(UpdateItemStatus { + "removed", UpdateItemTypeEnum::eService, "1.0.0", false, ItemStateEnum::eRemoved, ErrorEnum::eNone}); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + ConvertInstancesStatuses(expectedUnitStatus->mInstances.GetValue(), instances); + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); +} + +TEST_F(UpdateManagerTest, FailedItemInDesiredStatusDoesNotRequireUpdate) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + + // Expect calls: item is failed, it can be retried only after removing it + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).Times(0); + EXPECT_CALL(mImageManagerMock, InstallItem(_)).Times(0); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + statuses.EmplaceBack(UpdateItemStatus { + "item1", UpdateItemTypeEnum::eService, "1.0.0", false, ItemStateEnum::eFailed, ErrorEnum::eFailed}); + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mStorageStub.GetUpdateState().mValue, UpdateStateEnum::eNone); +} + +TEST_F(UpdateManagerTest, CanceledItemInDesiredStatusIsInstalled) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + + // Set expected statuses + + CreateUpdateItemStatus( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + + // Expect calls: only the canceled item state requires an update + + std::atomic_bool itemInstalled {false}; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])).WillOnce(InvokeWithoutArgs([&]() { + itemInstalled = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + statuses.EmplaceBack(UpdateItemStatus {"item1", UpdateItemTypeEnum::eService, "1.0.0", false, + itemInstalled ? ItemStateEnum::eInstalled : ItemStateEnum::eFailed, + itemInstalled ? ErrorEnum::eNone : ErrorEnum::eCanceled}); + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); +} + +TEST_F(UpdateManagerTest, FailedItemNotInDesiredStatusIsRemoved) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Expect calls: desired status without failed item removes it + + std::atomic_bool itemRemoved {false}; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(_)).Times(0); + EXPECT_CALL(mImageManagerMock, RemoveItem(String("item1"), String("1.0.0"))).WillOnce(InvokeWithoutArgs([&]() { + itemRemoved = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + if (!itemRemoved) { + statuses.EmplaceBack(UpdateItemStatus { + "item1", UpdateItemTypeEnum::eService, "1.0.0", false, ItemStateEnum::eFailed, ErrorEnum::eFailed}); + } + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); +} + +TEST_F(UpdateManagerTest, RecoverItemsWithIntegrityError) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + CreateUpdateItemInfo(*desiredStatus, "item2", UpdateItemTypeEnum::eService, "1.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 2, {}}); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item2", "subject1", 0, 1, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses + + CreateUpdateItemStatus( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateUpdateItemStatus( + *expectedUnitStatus, "item2", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateInstancesStatuses(*expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 2); + CreateInstancesStatuses(*expectedUnitStatus, "item2", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 1); + + auto runRequest = std::make_unique>(); + + CreateRunRequest(*desiredStatus, *runRequest); + + // Expect calls: item1 failed with the integrity error is installed again between the launches, item2 failed with + // another error is not + + std::atomic_int numRuns {0}; + std::vector item1InstallRuns; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])) + .Times(2) + .WillRepeatedly(InvokeWithoutArgs([&]() { + item1InstallRuns.push_back(numRuns); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[1])).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, SetItemFail(String("item1"), String("1.0.0"), _)) + .WillOnce(Invoke([](const String&, const String&, const Error& error) { + EXPECT_TRUE(error.Is(ErrorEnum::eIntegrity)); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemStatusProviderMock, SetItemFail(String("item2"), String("1.0.0"), _)) + .WillOnce(Invoke([](const String&, const String&, const Error& error) { + EXPECT_TRUE(error.Is(ErrorEnum::eFailed)); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).Times(2).WillRepeatedly(InvokeWithoutArgs([&]() { + numRuns++; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + if (numRuns > 0) { + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + } + + if (numRuns == 1) { + statuses[0].mState = ItemStateEnum::eFailed; + statuses[0].mError = ErrorEnum::eIntegrity; + statuses[1].mState = ItemStateEnum::eFailed; + statuses[1].mError = ErrorEnum::eFailed; + } + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + ConvertInstancesStatuses(expectedUnitStatus->mInstances.GetValue(), instances); + + if (numRuns == 1) { + // The first item1 instance error is overridden by the integrity error of the second one + + SetInstancesFailed(instances, "item1", ErrorEnum::eIntegrity); + instances[0].mError = ErrorEnum::eFailed; + + SetInstancesFailed(instances, "item2", ErrorEnum::eFailed); + } + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + EXPECT_EQ(mStorageStub.GetUpdateState().mValue, UpdateStateEnum::eNone); + EXPECT_EQ(item1InstallRuns, std::vector({0, 1})); +} + +TEST_F(UpdateManagerTest, OtherVersionInstanceDoesNotRecoverItems) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "2.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 1, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses: the instance reports another version than the desired item + + CreateUpdateItemStatus( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "2.0.0", ItemStateEnum::eInstalled); + CreateInstancesStatuses( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 1, InstanceStateEnum::eFailed); + + sInstanceStatusStorage.Back().mError = ErrorEnum::eIntegrity; + + auto runRequest = std::make_unique>(); + + CreateRunRequest(*desiredStatus, *runRequest); + + // Expect calls: the update is finished without recovering + + std::atomic_bool instancesLaunched {false}; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, SetItemFail(_, _, _)).Times(0); + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).WillOnce(InvokeWithoutArgs([&]() { + instancesLaunched = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + if (instancesLaunched) { + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + } + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + ConvertInstancesStatuses(expectedUnitStatus->mInstances.GetValue(), instances); + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + EXPECT_EQ(mStorageStub.GetUpdateState().mValue, UpdateStateEnum::eNone); +} + +TEST_F(UpdateManagerTest, PartiallyFailedInstances) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + CreateUpdateItemInfo(*desiredStatus, "item2", UpdateItemTypeEnum::eService, "1.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 2, {}}); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item2", "subject1", 0, 2, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses + + CreateUpdateItemStatus( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateUpdateItemStatus( + *expectedUnitStatus, "item2", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateInstancesStatuses(*expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 2); + CreateInstancesStatuses(*expectedUnitStatus, "item2", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 2); + + auto runRequest = std::make_unique>(); + + CreateRunRequest(*desiredStatus, *runRequest); + + // Expect calls: item1 has one instance failed with a non integrity error, so it is not set to failed. item2 has one + // instance failed with the integrity error, so it is set to failed and installed again. + + std::atomic_int numRuns {0}; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[1])) + .Times(2) + .WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, SetItemFail(_, _, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, SetItemFail(String("item2"), String("1.0.0"), _)) + .WillOnce(Invoke([](const String&, const String&, const Error& error) { + EXPECT_TRUE(error.Is(ErrorEnum::eIntegrity)); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).Times(2).WillRepeatedly(InvokeWithoutArgs([&]() { + numRuns++; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + if (numRuns > 0) { + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + } + + if (numRuns == 1) { + statuses[1].mState = ItemStateEnum::eFailed; + statuses[1].mError = ErrorEnum::eIntegrity; + } + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + ConvertInstancesStatuses(expectedUnitStatus->mInstances.GetValue(), instances); + + if (numRuns == 1) { + instances[0].mState = InstanceStateEnum::eFailed; + instances[0].mError = ErrorEnum::eFailed; + instances[3].mState = InstanceStateEnum::eFailed; + instances[3].mError = ErrorEnum::eIntegrity; + } + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + EXPECT_EQ(mStorageStub.GetUpdateState().mValue, UpdateStateEnum::eNone); +} + +TEST_F(UpdateManagerTest, RecoverItemsFailsAgainFinishesUpdate) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 1, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses: the item and the instance failed states and errors are reported + + CreateUpdateItemStatus(*expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eFailed); + + expectedUnitStatus->mUpdateItems->Back().mError = ErrorEnum::eIntegrity; + + CreateInstancesStatuses( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 1, InstanceStateEnum::eFailed); + + sInstanceStatusStorage.Back().mError = ErrorEnum::eIntegrity; + + auto runRequest = std::make_unique>(); + + CreateRunRequest(*desiredStatus, *runRequest); + + // Expect calls: the item is installed again and the instances are launched again only once + + std::atomic_bool instancesLaunched {false}; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])) + .Times(2) + .WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, SetItemFail(String("item1"), String("1.0.0"), _)) + .Times(2) + .WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).Times(2).WillRepeatedly(InvokeWithoutArgs([&]() { + instancesLaunched = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + if (instancesLaunched) { + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + } + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + ConvertInstancesStatuses(expectedUnitStatus->mInstances.GetValue(), instances); + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + EXPECT_EQ(mStorageStub.GetUpdateState().mValue, UpdateStateEnum::eNone); +} + +TEST_F(UpdateManagerTest, ResumeRecoveringAfterRestart) +{ + auto err = mUpdateManager.Stop(); + EXPECT_TRUE(err.IsNone()) << "Failed to stop update manager: " << tests::utils::ErrorToStr(err); + + auto failedUnitStatus = std::make_unique(); + auto activeUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*failedUnitStatus); + EmptyUnitStatus(*activeUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 1, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses + + CreateUpdateItemStatus(*failedUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eFailed); + + failedUnitStatus->mUpdateItems->Back().mError = ErrorEnum::eIntegrity; + + CreateInstancesStatuses( + *failedUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 1, InstanceStateEnum::eFailed); + + sInstanceStatusStorage.Back().mError = ErrorEnum::eIntegrity; + + CreateUpdateItemStatus( + *activeUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateInstancesStatuses(*activeUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 1); + + // Store desired status and update state to simulate restart during recovering + + err = mStorageStub.StoreDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to save desired status: " << tests::utils::ErrorToStr(err); + + err = mStorageStub.StoreUpdateState(UpdateStateEnum::eRecovering); + EXPECT_TRUE(err.IsNone()) << "Failed to save update state: " << tests::utils::ErrorToStr(err); + + auto runRequest = std::make_unique>(); + + CreateRunRequest(*desiredStatus, *runRequest); + + std::condition_variable cv; + std::mutex mutex; + bool continueUpdate = false; + bool installStarted = false; + std::atomic_bool instancesLaunched {false}; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])) + .WillOnce(Invoke([&](const UpdateItemInfo&) -> Error { + std::unique_lock lock(mutex); + + installStarted = true; + cv.notify_one(); + + EXPECT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return continueUpdate; })); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).WillOnce(InvokeWithoutArgs([&]() { + instancesLaunched = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + statuses = instancesLaunched ? activeUnitStatus->mUpdateItems.GetValue() + : failedUnitStatus->mUpdateItems.GetValue(); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + const auto& status = instancesLaunched ? activeUnitStatus : failedUnitStatus; + + ConvertInstancesStatuses(status->mInstances.GetValue(), instances); + + return ErrorEnum::eNone; + })); + + err = mUpdateManager.Start(); + EXPECT_TRUE(err.IsNone()) << "Failed to start update manager: " << tests::utils::ErrorToStr(err); + + // wait for install to start + + { + std::unique_lock lock(mutex); + + ASSERT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return installStarted; })); + } + + // Notify cloud connection established and wait for unit status + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *failedUnitStatus); + + // Continue update + + { + std::unique_lock lock(mutex); + + continueUpdate = true; + cv.notify_one(); + } + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *activeUnitStatus); + EXPECT_EQ(mStorageStub.GetUpdateState().mValue, UpdateStateEnum::eNone); +} + +TEST_F(UpdateManagerTest, NewDesiredStatusCancelsRecovering) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 1, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses + + CreateUpdateItemStatus( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateInstancesStatuses(*expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 2); + + std::condition_variable cv; + std::mutex mutex; + int numInstalls = 0; + bool canceled = false; + bool recovering = false; + std::atomic_int numRuns {0}; + + // Expect calls: install, canceled reinstall and install of the canceled item by the new update + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).Times(3).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(_)).Times(3).WillRepeatedly(Invoke([&](const UpdateItemInfo&) -> Error { + std::unique_lock lock(mutex); + + numInstalls++; + + if (numInstalls != 2) { + return ErrorEnum::eNone; + } + + recovering = true; + cv.notify_all(); + + EXPECT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return canceled; })); + + return ErrorEnum::eCanceled; + })); + EXPECT_CALL(mImageManagerMock, Cancel()).WillOnce(Invoke([&]() { + std::unique_lock lock(mutex); + + canceled = true; + cv.notify_all(); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, SetItemFail(String("item1"), String("1.0.0"), _)) + .WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mLauncherMock, RunInstances(_, _)).Times(2).WillRepeatedly(InvokeWithoutArgs([&]() { + numRuns++; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + std::unique_lock lock(mutex); + + if (numInstalls == 0) { + return ErrorEnum::eNone; + } + + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + + if (numRuns == 1 && !canceled) { + statuses[0].mState = ItemStateEnum::eFailed; + statuses[0].mError = ErrorEnum::eIntegrity; + } + + if (canceled && numInstalls < 3) { + statuses[0].mState = ItemStateEnum::eFailed; + statuses[0].mError = ErrorEnum::eCanceled; + } + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + ConvertInstancesStatuses(expectedUnitStatus->mInstances.GetValue(), instances); + + if (numRuns == 1) { + SetInstancesFailed(instances, "item1", ErrorEnum::eIntegrity); + } + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + // Wait for recovering to start + + { + std::unique_lock lock(mutex); + + ASSERT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return recovering; })); + } + + // Send new desired status to cancel recovering + + desiredStatus->mInstances[0].mNumInstances = 2; + + err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + EXPECT_EQ(mStorageStub.GetUpdateState().mValue, UpdateStateEnum::eNone); +} + +TEST_F(UpdateManagerTest, InstallItemsConcurrencyLimit) +{ + constexpr auto cMaxConcurrentItems = static_cast(cMaxNumConcurrentItems); + constexpr auto cNumItems = cMaxConcurrentItems * 2; + + static_assert(cNumItems <= cMaxNumUpdateItems, "too many update items"); + + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired update items + + for (size_t i = 0; i < cNumItems; i++) { + CreateUpdateItemInfo( + *desiredStatus, ("item" + std::to_string(i)).c_str(), UpdateItemTypeEnum::eService, "1.0.0"); + } + + std::condition_variable cv; + std::mutex mutex; + size_t numStarted = 0; + size_t numActive = 0; + size_t maxActive = 0; + std::set installedItems; + + EXPECT_CALL(mImageManagerMock, InstallItem(_)) + .Times(cNumItems) + .WillRepeatedly(Invoke([&](const UpdateItemInfo& item) -> Error { + std::unique_lock lock(mutex); + + numStarted++; + numActive++; + maxActive = std::max(maxActive, numActive); + + cv.notify_all(); + + EXPECT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return numStarted >= cMaxConcurrentItems; })); + + cv.wait_for(lock, std::chrono::milliseconds(100), [&]() { return numStarted > cMaxConcurrentItems; }); + + installedItems.insert(item.mItemID.CStr()); + numActive--; + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + std::unique_lock lock(mutex); + + EXPECT_EQ(maxActive, cMaxConcurrentItems); + EXPECT_EQ(installedItems.size(), cNumItems); +} + +TEST_F(UpdateManagerTest, RemoveItemsConcurrently) +{ + constexpr auto cMaxConcurrentItems = static_cast(cMaxNumConcurrentItems); + constexpr auto cNumItems = static_cast(cMaxNumUpdateItems) * 2; + + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + std::condition_variable cv; + std::mutex mutex; + size_t numStarted = 0; + size_t numActive = 0; + size_t maxActive = 0; + std::set removedItems; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)) + .Times(cNumItems) + .WillRepeatedly(Invoke([&](const String& itemID, const String&) -> Error { + std::unique_lock lock(mutex); + + numStarted++; + numActive++; + maxActive = std::max(maxActive, numActive); + + cv.notify_all(); + + EXPECT_TRUE(cv.wait_for(lock, cCVTimeout, [&]() { return numStarted >= cMaxConcurrentItems; })); + + // Keep the first items active, so tasks are queued up to the full queue size + + cv.wait_for(lock, std::chrono::milliseconds(100), [&]() { return numStarted > cMaxConcurrentItems; }); + + removedItems.insert(itemID.CStr()); + numActive--; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + std::unique_lock lock(mutex); + + if (numStarted == 0) { + for (size_t i = 0; i < cNumItems; i++) { + statuses.EmplaceBack(UpdateItemStatus {("item" + std::to_string(i)).c_str(), + UpdateItemTypeEnum::eService, "1.0.0", false, ItemStateEnum::eInstalled, ErrorEnum::eNone}); + } + } + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + std::unique_lock lock(mutex); + + EXPECT_EQ(maxActive, cMaxConcurrentItems); + EXPECT_EQ(removedItems.size(), cNumItems); +} + +TEST_F(UpdateManagerTest, FinalizeRemoveItemFails) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 1, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses: stale items item2 and item3 fail to be removed and stay installed + + CreateUpdateItemStatus( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateUpdateItemStatus( + *expectedUnitStatus, "item2", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateUpdateItemStatus( + *expectedUnitStatus, "item3", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateInstancesStatuses(*expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 1); + + // Create launcher run request + + auto runRequest = std::make_unique>(); + + CreateRunRequest(*desiredStatus, *runRequest); + + // Expect calls + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])).WillOnce(Return(ErrorEnum::eNone)); + std::atomic_bool instancesLaunched {false}; + + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).WillOnce(InvokeWithoutArgs([&]() { + instancesLaunched = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mImageManagerMock, RemoveItem(String("item2"), String("1.0.0"))).WillOnce(Return(ErrorEnum::eFailed)); + EXPECT_CALL(mImageManagerMock, RemoveItem(String("item3"), String("1.0.0"))).WillOnce(Return(ErrorEnum::eFailed)); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) { + // Items are reported only after install, otherwise they are skipped by install step + + if (instancesLaunched) { + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + } + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + ConvertInstancesStatuses(expectedUnitStatus->mInstances.GetValue(), instances); + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + EXPECT_EQ(mStorageStub.GetUpdateState().mValue, UpdateStateEnum::eNone); +} + +TEST_F(UpdateManagerTest, FinalizeGetStatusesFails) +{ + auto expectedUnitStatus = std::make_unique(); + auto desiredStatus = std::make_unique(); + + EmptyUnitStatus(*expectedUnitStatus); + + // Notify cloud connection established + + mConnectionListener->OnConnect(); + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + + // Set desired status + + CreateUpdateItemInfo(*desiredStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0"); + desiredStatus->mInstances.EmplaceBack(DesiredInstanceInfo {"item1", "subject1", 0, 1, {}}); + desiredStatus->mSubjects.EmplaceBack(SubjectInfo {"subject1", SubjectTypeEnum::eUser}); + + // Set expected statuses: finalize is aborted, so stale item2 is not removed + + CreateUpdateItemStatus( + *expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateUpdateItemStatus( + *expectedUnitStatus, "item2", UpdateItemTypeEnum::eService, "1.0.0", ItemStateEnum::eInstalled); + CreateInstancesStatuses(*expectedUnitStatus, "item1", UpdateItemTypeEnum::eService, "subject1", "1.0.0", 1); + + // Create launcher run request + + auto runRequest = std::make_unique>(); + + CreateRunRequest(*desiredStatus, *runRequest); + + // Expect calls: the first get items statuses after launching instances is done by finalize and fails + + std::atomic_bool instancesLaunched {false}; + std::atomic_bool finalizeFailed {false}; + + EXPECT_CALL(mImageManagerMock, SetCerts(_, _)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManagerMock, InstallItem(desiredStatus->mUpdateItems[0])).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mLauncherMock, RunInstances(*runRequest, _)).WillOnce(InvokeWithoutArgs([&]() { + instancesLaunched = true; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManagerMock, RemoveItem(_, _)).Times(0); + EXPECT_CALL(mItemStatusProviderMock, GetItemsStatuses(_)) + .WillRepeatedly(Invoke([&](Array& statuses) -> Error { + // Items are reported only after install, otherwise they are skipped by install step + + if (!instancesLaunched) { + return ErrorEnum::eNone; + } + + if (!finalizeFailed.exchange(true)) { + return ErrorEnum::eFailed; + } + + statuses = expectedUnitStatus->mUpdateItems.GetValue(); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mLauncherMock, GetInstancesStatuses(_)).WillRepeatedly(Invoke([&](Array& instances) { + ConvertInstancesStatuses(expectedUnitStatus->mInstances.GetValue(), instances); + + return ErrorEnum::eNone; + })); + + auto err = mUpdateManager.ProcessDesiredStatus(*desiredStatus); + EXPECT_TRUE(err.IsNone()) << "Failed to process desired status: " << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mSenderStub.WaitSendUnitStatus(), *expectedUnitStatus); + EXPECT_TRUE(finalizeFailed); + EXPECT_EQ(mStorageStub.GetUpdateState().mValue, UpdateStateEnum::eNone); +} + } // namespace aos::cm::updatemanager diff --git a/src/core/cm/updatemanager/unitstatushandler.cpp b/src/core/cm/updatemanager/unitstatushandler.cpp index a8d254d96..fd4724a63 100644 --- a/src/core/cm/updatemanager/unitstatushandler.cpp +++ b/src/core/cm/updatemanager/unitstatushandler.cpp @@ -16,7 +16,7 @@ namespace aos::cm::updatemanager { Error UnitStatusHandler::Init(AllocatorItf& allocator, const Config& config, iamclient::IdentProviderItf& identProvider, unitconfig::UnitConfigItf& unitConfig, nodeinfoprovider::NodeInfoProviderItf& nodeInfoProvider, - imagemanager::ItemStatusProviderItf& itemStatusProvider, + common::imagemanager::ItemStatusProviderItf& itemStatusProvider, instancestatusprovider::ProviderItf& instanceStatusProvider, cloudconnection::CloudConnectionItf& cloudConnection, SenderItf& sender) { @@ -239,44 +239,40 @@ void UnitStatusHandler::OnNodeInfoChanged(const UnitNodeInfo& info) StartTimer(); } -void UnitStatusHandler::OnItemsStatusesChanged(const Array& statuses) +void UnitStatusHandler::OnItemStatusChanged(const UpdateItemStatus& status) { LockGuard lock {mMutex}; - for (const auto& status : statuses) { - LOG_DBG() << "Item status changed" << Log::Field("id", status.mItemID) << Log::Field("type", status.mType) - << Log::Field("version", status.mVersion) << Log::Field("state", status.mState) - << Log::Field(status.mError); - } + LOG_DBG() << "Item status changed" << Log::Field("id", status.mItemID) << Log::Field("type", status.mType) + << Log::Field("version", status.mVersion) << Log::Field("state", status.mState) + << Log::Field(status.mError); if (!mCloudConnected || mIsStatusProcessing) { return; } - if (!mUnitStatus.mUpdateItems.HasValue() && !statuses.IsEmpty()) { + if (!mUnitStatus.mUpdateItems.HasValue()) { mUnitStatus.mUpdateItems.EmplaceValue(); } - for (const auto& status : statuses) { - auto it = mUnitStatus.mUpdateItems->FindIf([&status](const UpdateItemStatus& itemStatus) { + if (auto it = mUnitStatus.mUpdateItems->FindIf([&status](const UpdateItemStatus& itemStatus) { return itemStatus.mItemID == status.mItemID && itemStatus.mVersion == status.mVersion; }); - if (it == mUnitStatus.mUpdateItems->end()) { - if (auto err = mUnitStatus.mUpdateItems->EmplaceBack(status); !err.IsNone()) { - LOG_ERR() << "Failed to emplace update item status" << Log::Field(err); - return; - } - } - + it != mUnitStatus.mUpdateItems->end()) { *it = status; + } else if (auto err = mUnitStatus.mUpdateItems->EmplaceBack(status); !err.IsNone()) { + LOG_ERR() << "Failed to emplace update item status" << Log::Field(err); + + return; } StartTimer(); } -void UnitStatusHandler::OnItemRemoved(const String& itemID) +void UnitStatusHandler::OnItemRemovedFromCache(const String& id, const String& version) { - (void)itemID; + (void)id; + (void)version; } void UnitStatusHandler::OnInstancesStatusesChanged(const Array& statuses) @@ -457,7 +453,7 @@ Error UnitStatusHandler::SetUpdateItemsStatus() return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); } - if (auto err = mItemStatusProvider->GetUpdateItemsStatuses(*itemsStatuses); !err.IsNone()) { + if (auto err = mItemStatusProvider->GetItemsStatuses(*itemsStatuses); !err.IsNone()) { return AOS_ERROR_WRAP(err); } diff --git a/src/core/cm/updatemanager/unitstatushandler.hpp b/src/core/cm/updatemanager/unitstatushandler.hpp index 50692f498..c18f5119d 100644 --- a/src/core/cm/updatemanager/unitstatushandler.hpp +++ b/src/core/cm/updatemanager/unitstatushandler.hpp @@ -7,11 +7,11 @@ #ifndef AOS_CORE_CM_UPDATEMANAGER_UNITSTATUSHANDLER_HPP_ #define AOS_CORE_CM_UPDATEMANAGER_UNITSTATUSHANDLER_HPP_ -#include #include #include #include #include +#include #include #include #include @@ -31,7 +31,7 @@ namespace aos::cm::updatemanager { * Unit status handler. */ class UnitStatusHandler : private nodeinfoprovider::NodeInfoListenerItf, - private imagemanager::ItemStatusListenerItf, + private common::imagemanager::ItemStatusListenerItf, private instancestatusprovider::ListenerItf, private iamclient::SubjectsListenerItf, private cloudconnection::ConnectionListenerItf { @@ -52,8 +52,8 @@ class UnitStatusHandler : private nodeinfoprovider::NodeInfoListenerItf, */ Error Init(AllocatorItf& allocator, const Config& config, iamclient::IdentProviderItf& identProvider, unitconfig::UnitConfigItf& unitConfig, nodeinfoprovider::NodeInfoProviderItf& nodeInfoProvider, - imagemanager::ItemStatusProviderItf& itemStatusProvider, - instancestatusprovider::ProviderItf& instanceStatusProvider, + common::imagemanager::ItemStatusProviderItf& itemStatusProvider, + instancestatusprovider::ProviderItf& instanceStatusProvider, cloudconnection::CloudConnectionItf& cloudConnection, SenderItf& sender); /** @@ -96,9 +96,9 @@ class UnitStatusHandler : private nodeinfoprovider::NodeInfoListenerItf, // nodeinfoprovider::NodeInfoListenerItf implementation void OnNodeInfoChanged(const UnitNodeInfo& info) override; - // imagemanager::ItemStatusListenerItf implementation - void OnItemsStatusesChanged(const Array& statuses) override; - void OnItemRemoved(const String& itemID) override; + // common::imagemanager::ItemStatusListenerItf implementation + void OnItemStatusChanged(const UpdateItemStatus& status) override; + void OnItemRemovedFromCache(const String& id, const String& version) override; // instancestatusprovider::ListenerItf implementation void OnInstancesStatusesChanged(const Array& statuses) override; @@ -122,13 +122,13 @@ class UnitStatusHandler : private nodeinfoprovider::NodeInfoListenerItf, Error SetItemsForPreinstalledInstances(); void LogUnitStatus(); - iamclient::IdentProviderItf* mIdentProvider {}; - unitconfig::UnitConfigItf* mUnitConfig {}; - nodeinfoprovider::NodeInfoProviderItf* mNodeInfoProvider {}; - imagemanager::ItemStatusProviderItf* mItemStatusProvider {}; - instancestatusprovider::ProviderItf* mInstanceStatusProvider {}; - cloudconnection::CloudConnectionItf* mCloudConnection {}; - SenderItf* mSender {}; + iamclient::IdentProviderItf* mIdentProvider {}; + unitconfig::UnitConfigItf* mUnitConfig {}; + nodeinfoprovider::NodeInfoProviderItf* mNodeInfoProvider {}; + common::imagemanager::ItemStatusProviderItf* mItemStatusProvider {}; + instancestatusprovider::ProviderItf* mInstanceStatusProvider {}; + cloudconnection::CloudConnectionItf* mCloudConnection {}; + SenderItf* mSender {}; Mutex mMutex; UnitStatus mUnitStatus; diff --git a/src/core/cm/updatemanager/updatemanager.cpp b/src/core/cm/updatemanager/updatemanager.cpp index 00ad0de46..b447fb3b4 100644 --- a/src/core/cm/updatemanager/updatemanager.cpp +++ b/src/core/cm/updatemanager/updatemanager.cpp @@ -17,19 +17,19 @@ namespace aos::cm::updatemanager { Error UpdateManager::Init(AllocatorItf& allocator, const Config& config, iamclient::IdentProviderItf& identProvider, iamclient::NodeHandlerItf& nodeHandler, unitconfig::UnitConfigItf& unitConfig, nodeinfoprovider::NodeInfoProviderItf& nodeInfoProvider, imagemanager::ImageManagerItf& imageManager, - launcher::LauncherItf& launcher, cloudconnection::CloudConnectionItf& cloudConnection, SenderItf& sender, - StorageItf& storage) + common::imagemanager::ItemStatusProviderItf& itemStatusProvider, launcher::LauncherItf& launcher, + cloudconnection::CloudConnectionItf& cloudConnection, SenderItf& sender, StorageItf& storage) { LOG_DBG() << "Init update manager"; - if (auto err = mDesiredStatusHandler.Init( - allocator, nodeHandler, unitConfig, imageManager, launcher, mUnitStatusHandler, storage); + if (auto err = mDesiredStatusHandler.Init(allocator, nodeHandler, unitConfig, imageManager, itemStatusProvider, + launcher, mUnitStatusHandler, storage); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (auto err = mUnitStatusHandler.Init(allocator, config, identProvider, unitConfig, nodeInfoProvider, imageManager, - launcher, cloudConnection, sender); + if (auto err = mUnitStatusHandler.Init(allocator, config, identProvider, unitConfig, nodeInfoProvider, + itemStatusProvider, launcher, cloudConnection, sender); !err.IsNone()) { return AOS_ERROR_WRAP(err); } diff --git a/src/core/cm/updatemanager/updatemanager.hpp b/src/core/cm/updatemanager/updatemanager.hpp index 140efbf09..3c58a58fa 100644 --- a/src/core/cm/updatemanager/updatemanager.hpp +++ b/src/core/cm/updatemanager/updatemanager.hpp @@ -8,6 +8,7 @@ #define AOS_CORE_CM_UPDATEMANAGER_UPDATEMANAGER_HPP_ #include +#include #include "itf/updatemanager.hpp" @@ -34,6 +35,7 @@ class UpdateManager : public UpdateManagerItf { * @param unitConfig unit config interface. * @param nodeInfoProvider node info provider. * @param imageManager image manager. + * @param itemStatusProvider item status provider. * @param launcher launcher interface. * @param cloudConnection cloud connection. * @param sender unit status sender. @@ -43,8 +45,8 @@ class UpdateManager : public UpdateManagerItf { Error Init(AllocatorItf& allocator, const Config& config, iamclient::IdentProviderItf& identProvider, iamclient::NodeHandlerItf& nodeHandler, unitconfig::UnitConfigItf& unitConfig, nodeinfoprovider::NodeInfoProviderItf& nodeInfoProvider, imagemanager::ImageManagerItf& imageManager, - launcher::LauncherItf& launcher, cloudconnection::CloudConnectionItf& cloudConnection, SenderItf& sender, - StorageItf& storage); + common::imagemanager::ItemStatusProviderItf& itemStatusProvider, launcher::LauncherItf& launcher, + cloudconnection::CloudConnectionItf& cloudConnection, SenderItf& sender, StorageItf& storage); /** * Starts update manager. diff --git a/src/core/cm/updatemanager/updatemanager.md b/src/core/cm/updatemanager/updatemanager.md index 8abdc6b56..3e15f6fe9 100644 --- a/src/core/cm/updatemanager/updatemanager.md +++ b/src/core/cm/updatemanager/updatemanager.md @@ -28,3 +28,23 @@ classDiagram UpdateManager ..> SenderItf ``` + +## Items recovery + +After waiting for the instances to become active, the instances are checked per update item, using the item ID and +version: + +* if at least one instance has the `eIntegrity` error, the item is set to `eFailed` with this error, even if the other + instances are not failed; +* otherwise, if all the instances are failed, the item is set to `eFailed` with the error of the first one; +* otherwise, the item state is not changed. + +If there are items with the `eIntegrity` error, the update goes to the `eRecovering` state instead of `eFinalizing`: + +* the update items in `eFailed` state with the `eIntegrity` error are installed again. The image manager validates the + blobs of a stored item and downloads the missing or corrupted ones. The items failed with other errors are not; +* the update goes back to the `eLaunching` state, which runs all the instances again, and to `eWaitingActive`. + +The recovery is performed once per update. If the instances fail again, their items are set to `eFailed` again, the +update is finalized and their states are reported in the unit status. The recovery can be canceled the same way as the +install, and it is resumed after a restart based on the stored items statuses. diff --git a/src/core/common/CMakeLists.txt b/src/core/common/CMakeLists.txt index fa0a6b812..a83edee0c 100644 --- a/src/core/common/CMakeLists.txt +++ b/src/core/common/CMakeLists.txt @@ -31,7 +31,9 @@ set(INSTALL_HEADERS aos_core_common_version ) -set(INSTALL_LIBRARIES aos_core_common_crypto aos_core_common_monitoring aos_core_common_pkcs11 aos_core_common_tools) +set(INSTALL_LIBRARIES aos_core_common_crypto aos_core_common_imagemanager aos_core_common_monitoring + aos_core_common_pkcs11 aos_core_common_tools +) if(WITH_TEST) list(APPEND INSTALL_HEADERS aos_core_common_tests_mocks aos_core_common_tests_stubs) @@ -61,6 +63,7 @@ add_subdirectory(crypto) add_subdirectory(downloader) add_subdirectory(logging) add_subdirectory(iamclient) +add_subdirectory(imagemanager) add_subdirectory(instancestatusprovider) add_subdirectory(monitoring) add_subdirectory(networkmanager) diff --git a/src/core/common/downloader/itf/downloader.hpp b/src/core/common/downloader/itf/downloader.hpp index 20ec67ef8..25f5c883a 100644 --- a/src/core/common/downloader/itf/downloader.hpp +++ b/src/core/common/downloader/itf/downloader.hpp @@ -8,6 +8,9 @@ #ifndef AOS_CORE_COMMON_DOWNLOADER_ITF_DOWNLOADER_HPP_ #define AOS_CORE_COMMON_DOWNLOADER_ITF_DOWNLOADER_HPP_ +#include +#include +#include #include namespace aos::downloader { @@ -23,23 +26,20 @@ class DownloaderItf { virtual ~DownloaderItf() = default; /** - * Downloads file. + * Downloads file. Tries the URLs in order until the download succeeds. Each URL is retried with the downloader + * retry policy, then the next URL is tried. A partially downloaded file is resumed if the server supports range + * requests. The download checks the cancel context periodically and returns eCanceled if it is canceled. * - * @param digest image digest. - * @param url URL. + * @param ctx cancel context. + * @param digest file digest. + * @param urls file URLs. * @param path path to file. - * @return Error. + * @return Error: eCanceled if the download is canceled by the cancel context, eInvalidArgument if urls is empty, + * eAlreadyExist if a download of the same digest is in progress, otherwise the error of the last attempt. */ - virtual Error Download(const String& digest, const String& url, const String& path) = 0; - - /** - * Cancels ongoing download. - * - * @param digest image digest. - * - * @return Error. - */ - virtual Error Cancel(const String& digest) = 0; + virtual Error Download( + CancelContext& ctx, const String& digest, const Array>& urls, const String& path) + = 0; }; } // namespace aos::downloader diff --git a/src/core/common/imagemanager/CMakeLists.txt b/src/core/common/imagemanager/CMakeLists.txt new file mode 100644 index 000000000..1b3e3de9c --- /dev/null +++ b/src/core/common/imagemanager/CMakeLists.txt @@ -0,0 +1,59 @@ +# +# Copyright (C) 2026 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# ###################################################################################################################### +# Target name +# ###################################################################################################################### + +set(TARGET_NAME imagemanager) + +# ###################################################################################################################### +# Sources +# ###################################################################################################################### + +set(SOURCES imagemanager.cpp) + +# ###################################################################################################################### +# Headers +# ###################################################################################################################### + +set(HEADERS config.hpp imagemanager.hpp itf/blobinfoprovider.hpp itf/iteminfoprovider.hpp itf/itemstatusprovider.hpp + itf/storage.hpp +) + +# ###################################################################################################################### +# Libraries +# ###################################################################################################################### + +set(LIBRARIES aos::core::common::crypto aos::core::common::ocispec aos::core::common::spaceallocator + aos::core::common::tools +) + +# ###################################################################################################################### +# Target +# ###################################################################################################################### + +add_module( + TARGET_NAME + ${TARGET_NAME} + LOG_MODULE + STACK_USAGE + ${AOS_STACK_USAGE} + SOURCES + ${SOURCES} + HEADERS + ${HEADERS} + LIBRARIES + ${LIBRARIES} +) + +# ###################################################################################################################### +# Tests +# ###################################################################################################################### + +if(WITH_TEST) + add_subdirectory(tests) +endif() diff --git a/src/core/common/imagemanager/config.hpp b/src/core/common/imagemanager/config.hpp new file mode 100644 index 000000000..adb12e1fe --- /dev/null +++ b/src/core/common/imagemanager/config.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_CORE_COMMON_IMAGEMANAGER_CONFIG_HPP_ +#define AOS_CORE_COMMON_IMAGEMANAGER_CONFIG_HPP_ + +#include +#include +#include + +namespace aos::common::imagemanager { + +/** + * Image manager config. + */ +struct Config { + StaticString mImagePath; + size_t mPartLimit {}; + Duration mItemCacheTTL {}; + Duration mRemoveOutdatedPeriod {}; +}; + +} // namespace aos::common::imagemanager + +#endif diff --git a/src/core/common/imagemanager/imagemanager.cpp b/src/core/common/imagemanager/imagemanager.cpp new file mode 100644 index 000000000..aa9b104f3 --- /dev/null +++ b/src/core/common/imagemanager/imagemanager.cpp @@ -0,0 +1,1599 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "imagemanager.hpp" + +namespace aos::common::imagemanager { + +namespace { + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +RetWithError GetSupportedHash(const String& hashName) +{ + StaticString upperHash; + + if (auto err = upperHash.Assign(hashName); !err.IsNone()) { + return {{}, AOS_ERROR_WRAP(Error(ErrorEnum::eNotSupported, "unsupported hashing algorithm"))}; + } + + (void)upperHash.ToUpper(); + + if (upperHash == "SHA256") { + return {crypto::HashEnum::eSHA256, ErrorEnum::eNone}; + } else if (upperHash == "SHA512") { + return {crypto::HashEnum::eSHA512, ErrorEnum::eNone}; + } else { + return {{}, AOS_ERROR_WRAP(Error(ErrorEnum::eNotSupported, "unsupported hashing algorithm"))}; + } +} + +} // namespace + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +Error ImageManager::Start() +{ + LOG_DBG() << "Start image manager"; + + auto items = MakeUnique>(mAllocator); + if (!items) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (auto err = mStorage->GetAllDeployableItems(*items); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + for (auto& item : *items) { + if (item.mState == ItemStateEnum::eInstalling || item.mState == ItemStateEnum::eDownloading) { + if (auto err = CancelInterruptedItem(item); !err.IsNone()) { + return err; + } + } + + if (item.mState != ItemStateEnum::eRemoved) { + continue; + } + + if (auto err = mSpaceAllocator->AddOutdatedItem(item.mItemID, item.mVersion, item.mTimestamp); !err.IsNone()) { + LOG_ERR() << "Failed to add outdated item" << Log::Field("itemID", item.mItemID) + << Log::Field("version", item.mVersion) << Log::Field(err); + } + } + + { + LockGuard lock {mMutex}; + + mClose = false; + // The cleanup is done on start, after the outdated items are registered. + mRunCleanup = true; + } + + if (auto err = mTimer.Start( + mConfig.mRemoveOutdatedPeriod, + [this](void*) { + LockGuard lock {mMutex}; + + mRunCleanup = true; + + if (auto err = mCondVar.NotifyAll(); !err.IsNone()) { + LOG_ERR() << "Can't notify condition variable" << Log::Field(err); + } + }, + false); + !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mThread.Run([this](void*) { ProcessCleanup(); }); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::Stop() +{ + LOG_DBG() << "Stop image manager"; + + auto stopErr = Cancel(); + + if (auto err = mTimer.Stop(Timer::StopMode::WaitForCallbacks); !err.IsNone() && stopErr.IsNone()) { + stopErr = AOS_ERROR_WRAP(err); + } + + { + LockGuard lock {mMutex}; + + mClose = true; + + if (auto err = mCondVar.NotifyAll(); !err.IsNone()) { + LOG_ERR() << "Can't notify condition variable" << Log::Field(err); + } + } + + if (auto err = mThread.Join(); !err.IsNone() && stopErr.IsNone()) { + stopErr = AOS_ERROR_WRAP(err); + } + + return stopErr; +} + +Error ImageManager::GetItemsStatuses(Array& statuses) +{ + LockGuard lock {mMutex}; + + LOG_DBG() << "Get items statuses"; + + auto items = MakeUnique>(mAllocator); + if (!items) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (auto err = mStorage->GetAllDeployableItems(*items); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + for (const auto& item : *items) { + UpdateItemStatus status = item; // NOSONAR cpp:S5912 - intentional copy of the base part only + + const auto* itemInProgress = FindInProgressItem(item.mItemID, item.mVersion); + + // The item is left in eInstalling state by InstallItem that failed to update the item state. + if (itemInProgress == nullptr && status.mState == ItemStateEnum::eInstalling) { + status.mState = ItemStateEnum::eFailed; + } + + if (itemInProgress != nullptr && itemInProgress->mState != ItemStateEnum::eUnknown) { + status.mState = itemInProgress->mState; + status.mError = itemInProgress->mError; + } + + if (auto err = statuses.PushBack(status); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + } + + return ErrorEnum::eNone; +} + +Error ImageManager::SetItemFail(const String& itemID, const String& version, const Error& error) +{ + LOG_INF() << "Set item failed" << Log::Field("itemID", itemID) << Log::Field("version", version) + << Log::Field(error); + + if (auto err = LockItem(itemID, version); !err.IsNone()) { + return err; + } + + auto unlockItem = DeferRelease([this, &itemID, &version]() { UnlockItem(itemID, version); }); + + DeployableItemInfo storedInfo; + + if (auto err = mStorage->GetDeployableItem(itemID, version, storedInfo); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (storedInfo.mState == ItemStateEnum::eRemoved) { + return AOS_ERROR_WRAP(ErrorEnum::eWrongState); + } + + if (auto err = SetStoredItemState(storedInfo, ItemStateEnum::eFailed, error); !err.IsNone()) { + return err; + } + + return ErrorEnum::eNone; +} + +Error ImageManager::SubscribeListener(ItemStatusListenerItf& listener) +{ + LockGuard lock {mListenersMutex}; + + LOG_DBG() << "Subscribe listener"; + + if (mListeners.Contains(&listener)) { + return AOS_ERROR_WRAP(ErrorEnum::eAlreadyExist); + } + + if (auto err = mListeners.PushBack(&listener); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::UnsubscribeListener(ItemStatusListenerItf& listener) +{ + LockGuard lock {mListenersMutex}; + + LOG_DBG() << "Unsubscribe listener"; + + if (mListeners.Remove(&listener) == 0) { + return AOS_ERROR_WRAP(ErrorEnum::eNotFound); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::GetBlobPath(const String& digest, String& path) const +{ + LOG_DBG() << "Get blob path" << Log::Field("digest", digest); + + StaticString algorithm; + StaticString hash; + + if (auto err = ParseDigest(digest, algorithm, hash); !err.IsNone()) { + return err; + } + + if (auto err = path.Assign(fs::JoinPath(mConfig.mImagePath, cBlobsFolder, algorithm, hash)); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::GetLayerPath(const String& diffID, String& path) const +{ + (void)diffID; + (void)path; + + return ErrorEnum::eNotSupported; +} + +/*********************************************************************************************************************** + * Protected + **********************************************************************************************************************/ + +Error ImageManager::ParseDigest(const String& digest, String& algorithm, String& hash) +{ + // The blob paths are limited by the whole digest length, not by the lengths of its parts. + if (digest.Size() > oci::cDigestLen) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "malformed digest")); + } + + auto [pos, err] = digest.FindSubstr(0, ":"); + if (!err.IsNone() || pos == 0 || pos + 1 >= digest.Size()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "malformed digest")); + } + + if (err = algorithm.Assign(String(digest.CStr(), pos)); !err.IsNone()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "malformed digest")); + } + + if (err = hash.Assign(String(digest.CStr() + pos + 1, digest.Size() - pos - 1)); !err.IsNone()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "malformed digest")); + } + + const auto isLowerAlnum = [](char ch) { return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'); }; + + if (!isLowerAlnum(algorithm[0])) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "malformed digest")); + } + + for (const auto ch : algorithm) { + if (!(isLowerAlnum(ch) || ch == '+' || ch == '.' || ch == '_' || ch == '-')) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "malformed digest")); + } + } + + for (const auto ch : hash) { + if (!isLowerAlnum(ch)) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "malformed digest")); + } + } + + return ErrorEnum::eNone; +} + +Error ImageManager::Init(const Config& config, BlobInfoProviderItf& blobInfoProvider, StorageItf& storage, + spaceallocator::SpaceAllocatorItf& spaceAllocator, fs::FileInfoProviderItf& fileInfoProvider, + oci::OCISpecItf& ociSpec, fs::FSPlatformItf& fsPlatform, AllocatorItf& allocator) +{ + LOG_DBG() << "Init image manager"; + + mConfig = config; + mBlobInfoProvider = &blobInfoProvider; + mStorage = &storage; + mSpaceAllocator = &spaceAllocator; + mFileInfoProvider = &fileInfoProvider; + mOCISpec = &ociSpec; + mFSPlatform = &fsPlatform; + mAllocator = &allocator; + + LOG_DBG() << "Config" << Log::Field("imagePath", mConfig.mImagePath) << Log::Field("partLimit", mConfig.mPartLimit) + << Log::Field("itemCacheTTL", mConfig.mItemCacheTTL) + << Log::Field("removeOutdatedPeriod", mConfig.mRemoveOutdatedPeriod); + + // The longest blob path is /blobs//, / has the digest length. + if (mConfig.mImagePath.Size() + 1 + String(cBlobsFolder).Size() + 1 + oci::cDigestLen > cFilePathLen) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "image path is too long")); + } + + if (auto err = fs::MakeDirAll(fs::JoinPath(mConfig.mImagePath, cBlobsFolder)); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::InstallItem(const InstallItemInfo& itemInfo) +{ + LOG_INF() << "Install item" << Log::Field("itemID", itemInfo.mItemID) << Log::Field("version", itemInfo.mVersion) + << Log::Field("type", itemInfo.mType) << Log::Field("rootDigest", itemInfo.mRootDigest); + + if (auto err = LockItem(itemInfo.mItemID, itemInfo.mVersion); !err.IsNone()) { + return err; + } + + auto unlockItem = DeferRelease([this, &itemInfo]() { UnlockItem(itemInfo.mItemID, itemInfo.mVersion); }); + + DeployableItemInfo storedInfo; + + if (auto err = StartItemInstall(itemInfo, storedInfo); !err.IsNone()) { + return err; + } + + auto err = InstallItemBlobs(itemInfo); + + if (auto syncErr = mFSPlatform->Sync(mConfig.mImagePath); !syncErr.IsNone()) { + LOG_ERR() << "Failed to sync image path" << Log::Field(syncErr); + + if (err.IsNone()) { + err = AOS_ERROR_WRAP(syncErr); + } + } + + if (err.IsNone()) { + err = SetStoredItemState(storedInfo, ItemStateEnum::eInstalled); + } + + if (!err.IsNone()) { + if (auto setStateErr = SetStoredItemState(storedInfo, ItemStateEnum::eFailed, err); !setStateErr.IsNone()) { + LOG_ERR() << "Failed to set stored item state" << Log::Field(setStateErr); + + if (setStateErr = SetInProgressItemState( + itemInfo.mItemID, itemInfo.mVersion, itemInfo.mType, ItemStateEnum::eFailed, err); + !setStateErr.IsNone()) { + LOG_ERR() << "Failed to set in-progress item state" << Log::Field(setStateErr); + } + } + } + + return err; +} + +Error ImageManager::RemoveItem(const String& itemID, const String& version) +{ + LOG_INF() << "Remove item" << Log::Field("itemID", itemID) << Log::Field("version", version); + + if (auto err = LockItem(itemID, version); !err.IsNone()) { + return err; + } + + auto unlockItem = DeferRelease([this, &itemID, &version]() { UnlockItem(itemID, version); }); + + DeployableItemInfo storedInfo; + + if (auto err = mStorage->GetDeployableItem(itemID, version, storedInfo); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (storedInfo.mState == ItemStateEnum::eRemoved) { + LOG_DBG() << "Item already removed" << Log::Field("itemID", itemID) << Log::Field("version", version); + + return ErrorEnum::eNone; + } + + const UpdateItemStatus prevStatus = storedInfo; // NOSONAR cpp:S5912 - intentional copy of the base part only + + if (auto err = SetInProgressItemState(itemID, version, storedInfo.mType, ItemStateEnum::eRemoving); !err.IsNone()) { + LOG_ERR() << "Failed to set in-progress item state" << Log::Field(err); + } + + if (auto err = SetStoredItemState(storedInfo, ItemStateEnum::eRemoved); !err.IsNone()) { + RestoreRemovingItemState(prevStatus, err); + + return err; + } + + // The space allocator can remove the outdated item right away, which skips the locked item. + unlockItem.Reset(); + + if (auto err = mSpaceAllocator->AddOutdatedItem(itemID, version, storedInfo.mTimestamp); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::Cancel() +{ + // Serialized, otherwise one call can reset the context while another one still waits for the items. + LockGuard cancelLock {mCancelMutex}; + + LOG_DBG() << "Cancel"; + + // The items started while Cancel waits are canceled as well, so Cancel doesn't wait for their install. + if (auto err = mCancelContext.Cancel(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + UniqueLock lock {mMutex}; + + Error cancelErr; + + // The items waiting in LockItem are not in progress yet, but they were requested before this cancel. + if (auto err = mCondVar.Wait(lock, [this]() { return mItemsInProgress.IsEmpty() && mNumItemsWaiting == 0; }); + !err.IsNone()) { + cancelErr = AOS_ERROR_WRAP(err); + } + + // Reset under the lock, so the items started after it are not canceled. + mCancelContext.Reset(); + + return cancelErr; +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ + +void ImageManager::NotifyItemStatusChanged(const UpdateItemStatus& status) +{ + LockGuard lock {mListenersMutex}; + + LOG_DBG() << "Item status changed" << Log::Field("itemID", status.mItemID) << Log::Field("type", status.mType) + << Log::Field("version", status.mVersion) << Log::Field("state", status.mState) + << Log::Field(status.mError); + + for (auto* listener : mListeners) { + listener->OnItemStatusChanged(status); + } +} + +void ImageManager::NotifyItemRemovedFromCache(const String& itemID, const String& version) +{ + LockGuard lock {mListenersMutex}; + + LOG_DBG() << "Item removed from cache" << Log::Field("itemID", itemID) << Log::Field("version", version); + + for (auto* listener : mListeners) { + listener->OnItemRemovedFromCache(itemID, version); + } +} + +Error ImageManager::ValidateBlob(const String& path, const String& digest, size_t size) +{ + LOG_DBG() << "Validate blob" << Log::Field("digest", digest) << Log::Field("path", path); + + StaticString algStr; + StaticString hash; + + if (auto err = ParseDigest(digest, algStr, hash); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + auto [alg, err] = GetSupportedHash(algStr); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + auto hashBytes = MakeUnique>(mAllocator); + if (!hashBytes) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (err = hash.HexToByteArray(*hashBytes); !err.IsNone()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "malformed digest hash")); + } + + bool exists = false; + + if (Tie(exists, err) = fs::FileExist(path); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (!exists) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "file not found")); + } + + fs::FileInfo fileInfo; + + if (err = mFileInfoProvider->GetFileInfo(path, fileInfo, alg); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (size && fileInfo.mSize != size) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidChecksum, "size mismatch")); + } + + if (fileInfo.mCheckSum != *hashBytes) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidChecksum, "checksum mismatch")); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::GetBlobInfo(const String& digest, BlobInfo& info) +{ + while (true) { + if (auto err = mBlobInfoProvider->GetBlobInfo(digest, info); !err.IsNone()) { + LOG_ERR() << "Failed to get blob info" << Log::Field("digest", digest) << Log::Field(err); + + // The provider is available, but it doesn't have the blob: retrying doesn't help. + if (err.Is(ErrorEnum::eNotFound)) { + return AOS_ERROR_WRAP(err); + } + + if (err = mCancelContext.Wait(cRetryTimeout); !err.Is(ErrorEnum::eTimeout)) { + return AOS_ERROR_WRAP(err); + } + + LOG_DBG() << "Retrying get blob info" << Log::Field("digest", digest); + + continue; + } + + break; + } + + return ErrorEnum::eNone; +} + +Error ImageManager::DownloadItemBlob(const InstallItemInfo& itemInfo, const String& path, const String& digest) +{ + auto blobInfo = MakeUnique(mAllocator); + if (!blobInfo) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (auto err = GetBlobInfo(digest, *blobInfo); !err.IsNone()) { + return err; + } + + if (auto err = fs::MakeDirAll(fs::Dir(path)); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + auto [space, err] = mSpaceAllocator->AllocateSpace(blobInfo->mSize); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (err = SetInProgressItemState(itemInfo.mItemID, itemInfo.mVersion, itemInfo.mType, ItemStateEnum::eDownloading); + !err.IsNone()) { + LOG_ERR() << "Failed to set in-progress item state" << Log::Field(err); + } + + LOG_DBG() << "Download blob" << Log::Field("digest", digest) << Log::Field("size", blobInfo->mSize); + + err = DownloadBlob(mCancelContext, path, *blobInfo); + + // Done on a failed download as well, as the partial file may stay if it can't be removed. + if (auto resizeErr = ResizeBlobSpace(*space, path); !resizeErr.IsNone() && err.IsNone()) { + err = resizeErr; + } + + if (!err.IsNone()) { + if (auto removeErr = RemoveBlob(path, digest).mError; !removeErr.IsNone()) { + LOG_ERR() << "Failed to remove blob" << Log::Field("digest", digest) << Log::Field(removeErr); + + // The file stays, so its space stays used until the blob is processed again. + if (auto acceptErr = space->Accept(); !acceptErr.IsNone()) { + LOG_ERR() << "Failed to accept space" << Log::Field("digest", digest) << Log::Field(acceptErr); + } + + return err; + } + + if (auto releaseErr = space->Release(); !releaseErr.IsNone()) { + LOG_ERR() << "Failed to release space" << Log::Field("digest", digest) << Log::Field(releaseErr); + } + + return err; + } + + if (err = space->Accept(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::ResizeBlobSpace(spaceallocator::SpaceItf& space, const String& path) +{ + // The blob removal frees the size of the file, so the accepted space has to match it. + auto [size, err] = fs::CalculateSize(*mAllocator, path); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (err = space.Resize(size); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::InstallItemBlob( + const InstallItemInfo& itemInfo, const oci::ContentDescriptor& descriptor, String& path) +{ + LOG_DBG() << "Install blob" << Log::Field("digest", descriptor.mDigest) + << Log::Field("mediaType", descriptor.mMediaType) << Log::Field("size", descriptor.mSize); + + if (auto err = mCancelContext.Err(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = GetBlobPath(descriptor.mDigest, path); !err.IsNone()) { + return err; + } + + if (auto err = LockBlob(descriptor.mDigest); !err.IsNone()) { + return err; + } + + auto unlockBlob = DeferRelease([this, digest = &descriptor.mDigest]() { UnlockBlob(*digest); }); + + if (auto err = AddItemBlob(itemInfo.mItemID, itemInfo.mVersion, descriptor); !err.IsNone()) { + return err; + } + + if (auto err = ProcessBlob(itemInfo, path, descriptor); !err.IsNone()) { + return err; + } + + return ErrorEnum::eNone; +} + +Error ImageManager::InstallLayerBlob( + const InstallItemInfo& itemInfo, const oci::ContentDescriptor& descriptor, const String& diffID) +{ + LOG_DBG() << "Install layer blob" << Log::Field("digest", descriptor.mDigest) + << Log::Field("mediaType", descriptor.mMediaType) << Log::Field("size", descriptor.mSize) + << Log::Field("diffID", diffID); + + if (auto err = mCancelContext.Err(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + StaticString path; + + if (auto err = GetBlobPath(descriptor.mDigest, path); !err.IsNone()) { + return err; + } + + if (auto err = LockBlob(descriptor.mDigest); !err.IsNone()) { + return err; + } + + auto unlockBlob = DeferRelease([this, digest = &descriptor.mDigest]() { UnlockBlob(*digest); }); + + if (auto err = AddItemBlob(itemInfo.mItemID, itemInfo.mVersion, descriptor, true, diffID); !err.IsNone()) { + return err; + } + + if (auto err = ProcessLayer(itemInfo, path, descriptor, diffID); !err.IsNone()) { + return err; + } + + return ErrorEnum::eNone; +} + +Error ImageManager::ProcessBlob( + const InstallItemInfo& itemInfo, const String& path, const oci::ContentDescriptor& descriptor) +{ + auto err = ValidateBlob(path, descriptor.mDigest, descriptor.mSize); + if (err.IsNone()) { + return ErrorEnum::eNone; + } + + // Only a missing or corrupted blob is repaired, the other errors don't mean that the blob is invalid. + if (!err.Is(ErrorEnum::eNotFound) && !err.Is(ErrorEnum::eInvalidChecksum) && !err.Is(ErrorEnum::eIntegrity)) { + return err; + } + + if (!err.Is(ErrorEnum::eNotFound)) { + LOG_ERR() << "Blob validation failed" << Log::Field("digest", descriptor.mDigest) << Log::Field(err); + } + + // The unpacked layer data is removed by ProcessLayer. + if (err = RemoveItemBlob(path, descriptor); !err.IsNone()) { + return err; + } + + if (err = DownloadItemBlob(itemInfo, path, descriptor.mDigest); !err.IsNone()) { + return err; + } + + if (err = ValidateBlob(path, descriptor.mDigest, descriptor.mSize); !err.IsNone()) { + return err; + } + + return ErrorEnum::eNone; +} + +Error ImageManager::ProcessLayer( + const InstallItemInfo& itemInfo, const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) +{ + auto err = ValidateLayer(path, descriptor, diffID); + if (err.IsNone()) { + return ErrorEnum::eNone; + } + + // eNotSupported means the layer is kept as packed blob only: there is no unpacked layer data to handle. + if (err.Is(ErrorEnum::eNotSupported)) { + if (err = ProcessBlob(itemInfo, path, descriptor); !err.IsNone()) { + return err; + } + + return ErrorEnum::eNone; + } + + if (!err.Is(ErrorEnum::eNotFound)) { + LOG_ERR() << "Layer validation failed" << Log::Field("digest", descriptor.mDigest) << Log::Field(err); + } + + size_t layerSize = 0; + + Tie(layerSize, err) = RemoveLayer(path, descriptor, diffID); + if (!err.IsNone() && !err.Is(ErrorEnum::eNotSupported)) { + return err; + } + + if (layerSize > 0) { + mSpaceAllocator->FreeSpace(layerSize); + } + + if (err = ProcessBlob(itemInfo, path, descriptor); !err.IsNone()) { + return err; + } + + if (err = UnpackLayer(path, descriptor, diffID); !err.IsNone() && !err.Is(ErrorEnum::eNotSupported)) { + return err; + } + + return ErrorEnum::eNone; +} + +Error ImageManager::RemoveItemBlob( + const String& path, const oci::ContentDescriptor& descriptor, bool isLayer, const String& diffID) +{ + Error err; + size_t layerSize = 0; + size_t blobSize = 0; + + if (isLayer) { + Tie(layerSize, err) = RemoveLayer(path, descriptor, diffID); + if (!err.IsNone() && !err.Is(ErrorEnum::eNotSupported)) { + return err; + } + } + + Tie(blobSize, err) = RemoveBlob(path, descriptor.mDigest); + if (!err.IsNone()) { + return err; + } + + if (layerSize + blobSize > 0) { + mSpaceAllocator->FreeSpace(layerSize + blobSize); + } + + return ErrorEnum::eNone; +} + +RetWithError ImageManager::RemoveBlob(const String& path, const String& digest) +{ + LOG_DBG() << "Remove blob" << Log::Field("digest", digest) << Log::Field("path", path); + + auto [exists, err] = fs::FileExist(path); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (!exists) { + return {0, ErrorEnum::eNone}; + } + + size_t size = 0; + + if (Tie(size, err) = fs::CalculateSize(*mAllocator, path); !err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (err = fs::RemoveAll(path); !err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + return {size, ErrorEnum::eNone}; +} + +RetWithError ImageManager::RemoveOutdatedItem(const String& itemID, const String& version) +{ + LOG_DBG() << "Remove outdated item" << Log::Field("itemID", itemID) << Log::Field("version", version); + + // The item remover is called with the space allocator lock held, so it never waits for the item or its blobs: the + // waiting thread may hold them. The item which can't be locked is skipped. + if (!TryLockItem(itemID, version)) { + LOG_DBG() << "Can't lock outdated item" << Log::Field("itemID", itemID) << Log::Field("version", version); + + return {0, ErrorEnum::eNone}; + } + + auto unlockItem = DeferRelease([this, &itemID, &version]() { UnlockItem(itemID, version); }); + + DeployableItemInfo item; + + auto err = mStorage->GetDeployableItem(itemID, version, item); + if (!err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { + return {0, AOS_ERROR_WRAP(err)}; + } + + // The item could be removed from the cache or installed again after it was added as outdated. + if (err.Is(ErrorEnum::eNotFound) || item.mState != ItemStateEnum::eRemoved) { + LOG_DBG() << "Outdated item is not removed" << Log::Field("itemID", itemID) << Log::Field("version", version); + + return {0, ErrorEnum::eNone}; + } + + return RemoveItemFromCache(itemID, version); +} + +RetWithError ImageManager::RemoveItemFromCache(const String& itemID, const String& version) +{ + auto blobs = MakeUnique>(mAllocator); + if (!blobs) { + return {0, AOS_ERROR_WRAP(ErrorEnum::eNoMemory)}; + } + + if (auto err = mStorage->GetItemBlobs(itemID, version, *blobs); !err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (auto err = mStorage->RemoveDeployableItem(itemID, version); !err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + size_t freedSize = 0; + + for (const auto& blob : *blobs) { + // The blob which is failed to remove becomes orphan and is removed by the orphan blobs cleanup. + auto [blobSize, err] = RemoveUnusedItemBlob(blob); + if (!err.IsNone()) { + LOG_ERR() << "Failed to remove item blob" << Log::Field("digest", blob.mDigest) << Log::Field(err); + } + + freedSize += blobSize; + } + + LOG_INF() << "Item removed from cache" << Log::Field("itemID", itemID) << Log::Field("version", version) + << Log::Field("freedSize", freedSize); + + NotifyItemRemovedFromCache(itemID, version); + + return {freedSize, ErrorEnum::eNone}; +} + +void ImageManager::ProcessCleanup() +{ + while (true) { + { + UniqueLock lock {mMutex}; + + if (auto err = mCondVar.Wait(lock, [this]() { return mClose || mRunCleanup; }); !err.IsNone()) { + LOG_ERR() << "Wait failed" << Log::Field(err); + + continue; + } + + if (mClose) { + break; + } + + mRunCleanup = false; + } + + if (auto err = RemoveExpiredItems(); !err.IsNone()) { + LOG_ERR() << "Failed to remove expired items" << Log::Field(err); + } + + size_t freedSize = 0; + + auto [blobsSize, blobsErr] = RemoveOrphanBlobs(); + if (!blobsErr.IsNone()) { + LOG_ERR() << "Failed to remove orphan blobs" << Log::Field(blobsErr); + } + + freedSize += blobsSize; + + auto [layersSize, layersErr] = RemoveOrphanLayers(); + if (!layersErr.IsNone() && !layersErr.Is(ErrorEnum::eNotSupported)) { + LOG_ERR() << "Failed to remove orphan layers" << Log::Field(layersErr); + } + + freedSize += layersSize; + + // The size of the blobs and layers removed before an error is freed as well. + if (freedSize > 0) { + mSpaceAllocator->FreeSpace(freedSize); + } + } +} + +RetWithError ImageManager::RemoveOrphanBlobs() +{ + LOG_DBG() << "Remove orphan blobs"; + + const auto blobsPath = fs::JoinPath(mConfig.mImagePath, cBlobsFolder); + size_t freedSize = 0; + Error firstErr; + + // The blob which is failed to remove is logged and removed in the next period, the first error is returned. + const auto removeBlob = [this, &firstErr, &freedSize](const String& path, const String& digest) { + auto [size, err] = RemoveOrphanBlob(path, digest); + if (!err.IsNone()) { + LOG_ERR() << "Failed to remove orphan blob" << Log::Field("path", path) << Log::Field(err); + + if (firstErr.IsNone()) { + firstErr = err; + } + } + + freedSize += size; + }; + + for (auto algIt = fs::DirIterator(blobsPath); algIt.Next();) { + const auto algPath = fs::JoinPath(blobsPath, algIt->mPath); + + // The blobs folder contains only the algorithm folders. + if (!algIt->mIsDir) { + removeBlob(algPath, ""); + + continue; + } + + for (auto blobIt = fs::DirIterator(algPath); blobIt.Next();) { + StaticString digest; + + // The digest is left empty if it doesn't fit, so the file is removed as invalid. + if (algIt->mPath.Size() + 1 + blobIt->mPath.Size() <= digest.MaxSize()) { + (void)digest.Append(algIt->mPath).Append(":").Append(blobIt->mPath); + } + + removeBlob(fs::JoinPath(algPath, blobIt->mPath), digest); + } + } + + return {freedSize, firstErr}; +} + +RetWithError ImageManager::RemoveOrphanBlob(const String& path, const String& digest) +{ + StaticString algorithm; + StaticString hash; + + // The file which doesn't match a valid digest is not a blob and is removed. + if (digest.IsEmpty() || !ParseDigest(digest, algorithm, hash).IsNone()) { + LOG_WRN() << "Remove invalid blob file" << Log::Field("path", path); + + return RemoveBlob(path, digest); + } + + // A locked blob is being processed by an item being installed, so it is in use. + if (!TryLockBlob(digest)) { + return {0, ErrorEnum::eNone}; + } + + auto unlockBlob = DeferRelease([this, &digest]() { UnlockBlob(digest); }); + + auto [used, err] = mStorage->IsBlobUsed(digest); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (used) { + return {0, ErrorEnum::eNone}; + } + + LOG_DBG() << "Remove orphan blob" << Log::Field("digest", digest); + + return RemoveBlob(path, digest); +} + +Error ImageManager::RemoveExpiredItems() +{ + LOG_DBG() << "Remove expired items"; + + auto items = MakeUnique>(mAllocator); + if (!items) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (auto err = mStorage->GetAllDeployableItems(*items); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + for (const auto& item : *items) { + if (!IsItemExpired(item)) { + continue; + } + + // The failed item stays in the eRemoved state and is checked again in the next period. + if (auto err = RemoveExpiredItem(item.mItemID, item.mVersion, item.mTimestamp); !err.IsNone()) { + LOG_ERR() << "Failed to remove expired item" << Log::Field("itemID", item.mItemID) + << Log::Field("version", item.mVersion) << Log::Field(err); + } + } + + return ErrorEnum::eNone; +} + +bool ImageManager::IsItemExpired(const DeployableItemInfo& item) const +{ + return item.mState == ItemStateEnum::eRemoved && Time::Now().Sub(item.mTimestamp) >= mConfig.mItemCacheTTL; +} + +Error ImageManager::RemoveExpiredItem(const String& itemID, const String& version, const Time& timestamp) +{ + LOG_DBG() << "Remove expired item" << Log::Field("itemID", itemID) << Log::Field("version", version); + + // The item being installed or removed is skipped: after it, the item is either not removed or not expired, and + // otherwise it is checked again in the next period. + if (!TryLockItem(itemID, version)) { + LOG_DBG() << "Can't lock expired item" << Log::Field("itemID", itemID) << Log::Field("version", version); + + return ErrorEnum::eNone; + } + + auto unlockItem = DeferRelease([this, &itemID, &version]() { UnlockItem(itemID, version); }); + + auto err = mSpaceAllocator->RestoreOutdatedItem(itemID, version); + if (!err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { + LOG_ERR() << "Failed to restore outdated item" << Log::Field(err); + } + + const auto restored = err.IsNone(); + + DeployableItemInfo item; + + // The item which stays in the eRemoved state is added as outdated again. + const auto addOutdatedItem = [this, &restored, &unlockItem, &itemID, &version, &item]() { + if (!restored) { + return; + } + + // The space allocator can remove the outdated item right away, which skips the locked item. + unlockItem.Reset(); + + if (auto addErr = mSpaceAllocator->AddOutdatedItem(itemID, version, item.mTimestamp); !addErr.IsNone()) { + LOG_ERR() << "Failed to add outdated item" << Log::Field(addErr); + } + }; + + // If the storage can't be read, the item is checked again in the next period. + if (err = mStorage->GetDeployableItem(itemID, version, item); !err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { + item.mTimestamp = timestamp; + + addOutdatedItem(); + + return AOS_ERROR_WRAP(err); + } + + // The items are read before the item is locked: in the meantime, the space allocator could remove the item from + // the cache, or the item could be installed again. + if (err.Is(ErrorEnum::eNotFound) || item.mState != ItemStateEnum::eRemoved) { + return ErrorEnum::eNone; + } + + // The item could also be installed and removed again, so it is not expired anymore. + if (!IsItemExpired(item)) { + addOutdatedItem(); + + return ErrorEnum::eNone; + } + + size_t freedSize = 0; + + if (Tie(freedSize, err) = RemoveItemFromCache(itemID, version); !err.IsNone()) { + addOutdatedItem(); + + return err; + } + + if (freedSize > 0) { + mSpaceAllocator->FreeSpace(freedSize); + } + + return ErrorEnum::eNone; +} + +RetWithError ImageManager::RemoveUnusedItemBlob(const ItemBlobInfo& blob) +{ + // A locked blob is being processed by an item being installed, so it is in use. The blob which can't be locked is + // kept and becomes orphan if it is not used. + if (!TryLockBlob(blob.mDigest)) { + return {0, ErrorEnum::eNone}; + } + + auto unlockBlob = DeferRelease([this, &blob]() { UnlockBlob(blob.mDigest); }); + + auto [used, err] = mStorage->IsBlobUsed(blob.mDigest); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (used) { + return {0, ErrorEnum::eNone}; + } + + StaticString path; + + if (err = GetBlobPath(blob.mDigest, path); !err.IsNone()) { + return {0, err}; + } + + size_t layerSize = 0; + + if (blob.mIsLayer) { + if (Tie(layerSize, err) = RemoveLayer(path, blob, blob.mDiffID); + !err.IsNone() && !err.Is(ErrorEnum::eNotSupported)) { + LOG_ERR() << "Failed to remove layer" << Log::Field("digest", blob.mDigest) << Log::Field(err); + } + + if (!err.IsNone()) { + layerSize = 0; + } + } + + size_t blobSize = 0; + + if (Tie(blobSize, err) = RemoveBlob(path, blob.mDigest); !err.IsNone()) { + return {layerSize, err}; + } + + return {layerSize + blobSize, ErrorEnum::eNone}; +} + +Error ImageManager::LockItem(const String& itemID, const String& version) +{ + UniqueLock lock {mMutex}; + + mNumItemsWaiting++; + + auto err = mCondVar.Wait(lock, [&]() { + return FindInProgressItem(itemID, version) == nullptr && mItemsInProgress.Size() < cMaxNumConcurrentItems; + }); + + mNumItemsWaiting--; + + if (err.IsNone()) { + UpdateItemStatus item; + + item.mItemID = itemID; + item.mVersion = version; + + err = mItemsInProgress.PushBack(item); + } + + if (!err.IsNone()) { + // Cancel may wait for this item. + if (auto notifyErr = mCondVar.NotifyAll(); !notifyErr.IsNone()) { + LOG_ERR() << "Can't notify condition variable" << Log::Field(notifyErr); + } + + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +bool ImageManager::TryLockItem(const String& itemID, const String& version) +{ + LockGuard lock {mMutex}; + + if (FindInProgressItem(itemID, version) != nullptr) { + return false; + } + + if (mItemsInProgress.IsFull()) { + LOG_ERR() << "Items in progress list is full" << Log::Field("itemID", itemID) << Log::Field("version", version); + + return false; + } + + UpdateItemStatus item; + + item.mItemID = itemID; + item.mVersion = version; + + if (auto err = mItemsInProgress.PushBack(item); !err.IsNone()) { + return false; + } + + return true; +} + +void ImageManager::UnlockItem(const String& itemID, const String& version) +{ + LockGuard lock {mMutex}; + + (void)mItemsInProgress.RemoveIf( + [&](const UpdateItemStatus& item) { return item.mItemID == itemID && item.mVersion == version; }); + + if (auto err = mCondVar.NotifyAll(); !err.IsNone()) { + LOG_ERR() << "Can't notify condition variable" << Log::Field(err); + } +} + +UpdateItemStatus* ImageManager::FindInProgressItem(const String& itemID, const String& version) +{ + auto it = mItemsInProgress.FindIf( + [&](const UpdateItemStatus& item) { return item.mItemID == itemID && item.mVersion == version; }); + + return it != mItemsInProgress.end() ? &*it : nullptr; +} + +Error ImageManager::LockBlob(const String& digest) +{ + UniqueLock lock {mMutex}; + + if (auto err = mCondVar.Wait( + lock, [&]() { return !IsBlobInProgress(digest) && mBlobsInProgress.Size() < cMaxNumConcurrentItems; }); + !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + // The item could be canceled while it was waiting for the blob. + if (auto err = mCancelContext.Err(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mBlobsInProgress.PushBack(digest); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +bool ImageManager::TryLockBlob(const String& digest) +{ + LockGuard lock {mMutex}; + + if (IsBlobInProgress(digest)) { + return false; + } + + if (mBlobsInProgress.IsFull()) { + LOG_ERR() << "Blobs in progress list is full" << Log::Field("digest", digest); + + return false; + } + + if (auto err = mBlobsInProgress.PushBack(digest); !err.IsNone()) { + return false; + } + + return true; +} + +void ImageManager::UnlockBlob(const String& digest) +{ + LockGuard lock {mMutex}; + + (void)mBlobsInProgress.RemoveIf([&digest](const String& blob) { return blob == digest; }); + + if (auto err = mCondVar.NotifyAll(); !err.IsNone()) { + LOG_ERR() << "Can't notify condition variable" << Log::Field(err); + } +} + +Error ImageManager::StartItemInstall(const InstallItemInfo& itemInfo, DeployableItemInfo& storedInfo) +{ + auto err = mStorage->GetDeployableItem(itemInfo.mItemID, itemInfo.mVersion, storedInfo); + if (!err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { + return AOS_ERROR_WRAP(err); + } + + if (err.IsNone()) { + if (storedInfo.mRootDigest != itemInfo.mRootDigest) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "root digest mismatch")); + } + + const auto prevState = storedInfo.mState; + + if (err = SetStoredItemState(storedInfo, ItemStateEnum::eInstalling, ErrorEnum::eNone); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + // The item is restored after it leaves the eRemoved state: if it stays outdated, it is not removed from the + // cache as it is not in the eRemoved state anymore. + if (prevState == ItemStateEnum::eRemoved) { + if (err = mSpaceAllocator->RestoreOutdatedItem(itemInfo.mItemID, itemInfo.mVersion); + !err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { + LOG_ERR() << "Failed to restore outdated item" << Log::Field(err); + } + } + + return ErrorEnum::eNone; + } + + storedInfo.mItemID = itemInfo.mItemID; + storedInfo.mType = itemInfo.mType; + storedInfo.mVersion = itemInfo.mVersion; + storedInfo.mRootDigest = itemInfo.mRootDigest; + storedInfo.mState = ItemStateEnum::eInstalling; + storedInfo.mTimestamp = Time::Now(); + storedInfo.mError = ErrorEnum::eNone; + + if (err = AddStoredItem(storedInfo); !err.IsNone()) { + return err; + } + + if (err = SetInProgressItemState(itemInfo.mItemID, itemInfo.mVersion, itemInfo.mType, ItemStateEnum::eInstalling); + !err.IsNone()) { + LOG_ERR() << "Failed to set in-progress item state" << Log::Field(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::AddStoredItem(const DeployableItemInfo& item) +{ + // Serializes the storage count check with adding the item, so concurrent installs don't exceed the storage limit. + // The item remover doesn't take this mutex, so it can be held while the space allocator is called. + LockGuard lock {mStorageMutex}; + + while (true) { + auto [count, err] = mStorage->GetDeployableItemsCount(); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (count < cMaxNumStoredItems) { + break; + } + + LOG_DBG() << "Storage is full, remove outdated item" << Log::Field("count", count); + + // The space allocator drops the outdated item whatever the result, so the loop ends when no outdated item is + // left. + if (err = mSpaceAllocator->RemoveOutdatedItem().mError; !err.IsNone()) { + if (err.Is(ErrorEnum::eNotFound)) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNoMemory, "storage is full")); + } + + return AOS_ERROR_WRAP(err); + } + } + + if (auto err = mStorage->AddDeployableItem(item); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::InstallItemBlobs(const InstallItemInfo& itemInfo) +{ + if (!itemInfo.mIsRootIndex) { + if (auto err = InstallManifestBlobs( + itemInfo, oci::ContentDescriptor {oci::cMediaTypeImageManifest, itemInfo.mRootDigest, 0}); + !err.IsNone()) { + return err; + } + + return ErrorEnum::eNone; + } + + StaticString path; + + if (auto err + = InstallItemBlob(itemInfo, oci::ContentDescriptor {oci::cMediaTypeImageIndex, itemInfo.mRootDigest, 0}, path); + !err.IsNone()) { + return err; + } + + auto index = MakeUnique(mAllocator); + if (!index) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (auto err = mOCISpec->LoadImageIndex(path, *index); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + for (const auto& manifest : index->mManifests) { + if (auto err = InstallManifestBlobs(itemInfo, manifest); !err.IsNone()) { + return err; + } + } + + return ErrorEnum::eNone; +} + +Error ImageManager::InstallManifestBlobs(const InstallItemInfo& itemInfo, const oci::ContentDescriptor& descriptor) +{ + LOG_DBG() << "Install manifest" << Log::Field("digest", descriptor.mDigest); + + StaticString path; + + if (auto err = InstallItemBlob(itemInfo, descriptor, path); !err.IsNone()) { + return err; + } + + auto manifest = MakeUnique(mAllocator); + if (!manifest) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (auto err = mOCISpec->LoadImageManifest(path, *manifest); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (manifest->mItemConfig.HasValue()) { + if (auto err = InstallItemBlob(itemInfo, *manifest->mItemConfig, path); !err.IsNone()) { + return err; + } + } + + UniquePtr imageConfig; + + if (!manifest->mConfig.mDigest.IsEmpty()) { + if (auto err = InstallItemBlob(itemInfo, manifest->mConfig, path); !err.IsNone()) { + return err; + } + + // The empty config, for example of a component, has no diff IDs. + if (manifest->mConfig.mMediaType != oci::cMediaTypeEmptyBlob) { + imageConfig = MakeUnique(mAllocator); + if (!imageConfig) { + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + } + + if (auto err = mOCISpec->LoadImageConfig(path, *imageConfig); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + // A component config may have no rootfs: its layers are kept without diff IDs as for the empty config. + if (imageConfig->mRootfs.mDiffIDs.IsEmpty()) { + imageConfig.Reset(); + } else if (imageConfig->mRootfs.mDiffIDs.Size() < manifest->mLayers.Size()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "diff ID's size mismatch")); + } + } + } + + for (size_t i = 0; i < manifest->mLayers.Size(); ++i) { + StaticString diffID; + + if (imageConfig) { + diffID = imageConfig->mRootfs.mDiffIDs[i]; + } + + if (auto err = InstallLayerBlob(itemInfo, manifest->mLayers[i], diffID); !err.IsNone()) { + return err; + } + } + + return ErrorEnum::eNone; +} + +Error ImageManager::AddItemBlob(const String& itemID, const String& version, const oci::ContentDescriptor& descriptor, + bool isLayer, const String& diffID) +{ + if (auto err = mStorage->AddItemBlob(itemID, version, ItemBlobInfo {descriptor, isLayer, diffID}); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::SetInProgressItemState( + const String& itemID, const String& version, const UpdateItemType& type, ItemStateEnum state, const Error& error) +{ + UpdateItemStatus status; + bool stateChanged = false; + + { + LockGuard lock {mMutex}; + + auto itemInProgress = FindInProgressItem(itemID, version); + if (!itemInProgress) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "item not in progress")); + } + + stateChanged = itemInProgress->mState != state; + + itemInProgress->mType = type; + itemInProgress->mState = state; + itemInProgress->mError = error; + + status = *itemInProgress; + } + + if (stateChanged) { + NotifyItemStatusChanged(status); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::CancelInterruptedItem(DeployableItemInfo& item) +{ + LOG_WRN() << "Cancel interrupted item" << Log::Field("itemID", item.mItemID) + << Log::Field("version", item.mVersion); + + // The install was interrupted, so the item is handled the same way as a canceled one and keeps its blobs. + // The item status is not notified on start, it is reported by GetItemsStatuses. + item.mState = ItemStateEnum::eFailed; + item.mTimestamp = Time::Now(); + item.mError = ErrorEnum::eCanceled; + + if (auto err = mStorage->UpdateDeployableItem(item); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +void ImageManager::RestoreRemovingItemState(const UpdateItemStatus& prevStatus, const Error& error) +{ + { + LockGuard lock {mMutex}; + + // The stored state is reported again once the item is not being removed. + if (auto itemInProgress = FindInProgressItem(prevStatus.mItemID, prevStatus.mVersion); itemInProgress) { + itemInProgress->mState = ItemStateEnum::eUnknown; + itemInProgress->mError = ErrorEnum::eNone; + } + } + + auto status = prevStatus; + + status.mError = error; + + NotifyItemStatusChanged(status); +} + +Error ImageManager::SetStoredItemState(DeployableItemInfo& item, ItemStateEnum state, const Error& error) +{ + // An item left in eInstalling is reported as eFailed, so its next install has to be notified. + auto stateChanged = (item.mState != state) || (state == ItemStateEnum::eInstalling); + + item.mState = state; + item.mTimestamp = Time::Now(); + item.mError = error; + + if (auto err = mStorage->UpdateDeployableItem(item); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + { + LockGuard lock {mMutex}; + + if (auto itemInProgress = FindInProgressItem(item.mItemID, item.mVersion); itemInProgress) { + itemInProgress->mType = item.mType; + itemInProgress->mState = state; + itemInProgress->mError = error; + } + } + + if (stateChanged) { + NotifyItemStatusChanged(item); + } + + return ErrorEnum::eNone; +} + +} // namespace aos::common::imagemanager diff --git a/src/core/common/imagemanager/imagemanager.hpp b/src/core/common/imagemanager/imagemanager.hpp new file mode 100644 index 000000000..5f3a70d35 --- /dev/null +++ b/src/core/common/imagemanager/imagemanager.hpp @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_CORE_COMMON_IMAGEMANAGER_IMAGEMANAGER_HPP_ +#define AOS_CORE_COMMON_IMAGEMANAGER_IMAGEMANAGER_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "itf/blobinfoprovider.hpp" +#include "itf/iteminfoprovider.hpp" +#include "itf/itemstatusprovider.hpp" +#include "itf/storage.hpp" + +#include "config.hpp" + +namespace aos::common::imagemanager { + +/** + * Common image manager. + * + * Implements the part of the image manager shared by CM and SM. CM and SM image managers are derived from this class + * and specialize blob download and layer handling. + */ +class ImageManager : public ItemStatusProviderItf, public ItemInfoProviderItf, private NonCopyable { +public: + /** + * Destructor. + */ + virtual ~ImageManager() = default; + + /** + * Starts image manager. Sets the items whose install was interrupted (left in the eInstalling or eDownloading + * state) to the eFailed state with the eCanceled error, keeping their blobs, and adds all the items in the eRemoved + * state as outdated to the space allocator. No item status is notified. Then starts removing the expired items, + * i.e. the items in the eRemoved state for longer than mItemCacheTTL, and then the orphan blobs, i.e. the blobs not + * used by any item, right away and every mRemoveOutdatedPeriod. The items and blobs being processed are skipped. + * + * @return Error: storage errors as is. Errors of adding an outdated item are only logged. + */ + virtual Error Start(); + + /** + * Stops image manager. Stops removing the expired items and the orphan blobs and waits until the removal in + * progress is finished. + * + * @return Error. + */ + virtual Error Stop(); + + /** + * Retrieves deployable items statuses. Reports one status per item version in the storage. An item version being + * installed, whose eInstalling state is already notified, is reported with its last notified state (eInstalling or + * eDownloading) until its final state is notified. An item version being removed, whose eRemoving state is already + * notified, is reported as eRemoving until its eRemoved state is set in the storage or the storage update fails. + * Otherwise the stored state is reported. mError is set only for eFailed items and is taken from the storage. + * mPreinstalled is always false. ePending is never reported. + * + * @note The statuses are appended to the output array, which is not cleared. + * + * @param[out] statuses list of deployable items statuses. + * @return Error: eNoMemory if the statuses don't fit the output array, storage errors as is. + */ + Error GetItemsStatuses(Array& statuses) override; + + /** + * Sets item to the eFailed state with the error and notifies it if the state is changed. Waits while the same item + * version is being installed or removed, or while cMaxNumConcurrentItems items are being processed. + * + * @param itemID item ID. + * @param version item version. + * @param error failure reason. + * @return Error: eNotFound if the item version is not stored, eWrongState if the item is in the eRemoved state, + * other errors as is. + */ + Error SetItemFail(const String& itemID, const String& version, const Error& error) override; + + /** + * Subscribes status notifications. + * + * @note Must not be called from a listener notification callback: listeners are notified under the listeners + * lock, so the call deadlocks. + * + * @param listener status listener. + * @return Error: eAlreadyExist if the listener is already subscribed. + */ + Error SubscribeListener(ItemStatusListenerItf& listener) override; + + /** + * Unsubscribes from status notifications. + * + * @note Must not be called from a listener notification callback: listeners are notified under the listeners + * lock, so the call deadlocks. + * + * @param listener status listener. + * @return Error: eNotFound if the listener is not subscribed. + */ + Error UnsubscribeListener(ItemStatusListenerItf& listener) override; + + /** + * Returns local blob path. + * + * @param digest blob digest. + * @param[out] path local blob path. + * @return Error. + */ + Error GetBlobPath(const String& digest, String& path) const override; + + /** + * Returns unpacked layer path. Returns eNotSupported by default, as the common image manager doesn't keep the + * unpacked layers. + * + * @param diffID layer diff ID. + * @param[out] path unpacked layer path. + * @return Error. + */ + Error GetLayerPath(const String& diffID, String& path) const override; + + /** + * Returns item remover which should be passed to the space allocator to remove outdated items. + * + * @return spaceallocator::ItemRemoverItf&. + */ + spaceallocator::ItemRemoverItf& GetItemRemover() { return mItemRemover; } + +protected: + /** + * Install item info. + */ + struct InstallItemInfo { + StaticString mItemID; + UpdateItemType mType; + StaticString mVersion; + StaticString mRootDigest; + bool mIsRootIndex {}; + }; + + /** + * Initializes image manager. Derived classes call it from their own Init. + * + * @note The allocator must provide, per concurrent InstallItem (up to cMaxNumConcurrentItems), one + * oci::ImageIndex, one oci::ImageManifest, one oci::ImageConfig and one blob hash buffer of cBlobHashSize at the + * same time, plus two StaticArray for Start or the expired items removal + * and GetItemsStatuses, and two StaticArray for the item remover and the expired + * items removal. + * + * @param config image manager config. + * @param blobInfoProvider blob info provider. + * @param storage image manager storage. + * @param spaceAllocator space allocator. + * @param fileInfoProvider file info provider. + * @param ociSpec OCI spec interface. + * @param fsPlatform file system platform interface. + * @param allocator allocator for temporary objects. + * @return Error. + */ + Error Init(const Config& config, BlobInfoProviderItf& blobInfoProvider, StorageItf& storage, + spaceallocator::SpaceAllocatorItf& spaceAllocator, fs::FileInfoProviderItf& fileInfoProvider, + oci::OCISpecItf& ociSpec, fs::FSPlatformItf& fsPlatform, AllocatorItf& allocator); + + /** + * Installs item. Waits while the same item version is being installed or removed, or while + * cMaxNumConcurrentItems items are being processed. + * + * @note The final eInstalled or eFailed status is notified while the item is still in progress, so a listener + * must not call InstallItem or RemoveItem synchronously from the callback. + * + * @param itemInfo item info. + * @return Error: eInvalidArgument if the root digest is malformed, the item version is stored with a different + * root digest or a diff ID is missing in the image config, eNoMemory if the storage is full and there is no + * outdated item to remove, other errors as is. + */ + Error InstallItem(const InstallItemInfo& itemInfo); + + /** + * Removes item. Sets the item state to eRemoved in the storage, keeping the item blobs, and adds the item as + * outdated to the space allocator. Waits while the same item version is being installed or removed, or while + * cMaxNumConcurrentItems items are being processed. Does nothing if the item is already in the eRemoved state. + * + * @note The eRemoved status is notified while the item is still in progress, so a listener must not call + * InstallItem or RemoveItem synchronously from the callback. + * + * @param itemID item ID. + * @param version item version. + * @return Error: eNotFound if the item version is not stored, other errors as is. + */ + Error RemoveItem(const String& itemID, const String& version); + + /** + * Cancels all items being installed: cancels the cancel context passed to DownloadBlob and makes the items fail + * with eCanceled before the next blob. Doesn't cancel RemoveItem. Waits until all InstallItem and RemoveItem calls + * in progress or waiting for their turn return, the items started while it waits are canceled as well. RemoveItem + * may still be adding the unlocked item as outdated when Cancel returns. InstallItem called after Cancel returns is + * not affected. + * + * @return Error. + */ + Error Cancel(); + + /** + * Parses digest into the algorithm and the hash. The derived image managers use it to build the paths from digests. + * + * @param digest digest in the : format. + * @param[out] algorithm digest algorithm. + * @param[out] hash digest hash. + * @return Error: eInvalidArgument if the digest is malformed or longer than oci::cDigestLen. + */ + static Error ParseDigest(const String& digest, String& algorithm, String& hash); + + /** + * Returns space allocator. + * + * @return spaceallocator::SpaceAllocatorItf&. + */ + spaceallocator::SpaceAllocatorItf& GetSpaceAllocator() const { return *mSpaceAllocator; } + +private: + static constexpr auto cBlobsFolder = "blobs"; + static constexpr auto cBlobHashSize = Max(crypto::cSHA256Size, crypto::cSHA512Size); + static constexpr auto cMaxNumListeners = 4; + static constexpr auto cMaxNumStoredItems = cMaxNumUpdateItems * 2; + static constexpr auto cMaxNumItemBlobs = 1 + oci::cMaxNumManifests * (3 + oci::cMaxNumLayers); + static constexpr auto cRetryTimeout = Time::cSeconds * 2; + // Extra slots for the item remover and the expired items removal, which don't wait for the items and blobs. The + // item remover is called synchronously by the space allocator, so it must not wait or take the slot of another one. + static constexpr auto cMaxNumInProgress = cMaxNumConcurrentItems + 2; + + /** + * Downloads blob. Called by InstallItem, derived classes implement it but must not call it directly. The download + * checks the cancel context periodically, also before it is started, and returns eCanceled if it is canceled. + * + * @param ctx cancel context. + * @param path destination blob path. + * @param blobInfo blob information. + * @return Error. + */ + virtual Error DownloadBlob(CancelContext& ctx, const String& path, const BlobInfo& blobInfo) = 0; + + /** + * Validates installed layer. Called by the image manager before the layer blob is validated, derived classes can + * override it to validate unpacked layer data. If it succeeds, the layer blob is not validated. Any error other + * than eNotSupported means the unpacked layer data is missing or invalid: the image manager calls RemoveLayer and + * UnpackLayer for the layer. Returns eNotSupported by default, which the image manager treats as the layer is kept + * as packed blob only (skip): RemoveLayer and UnpackLayer are not called. Derived classes which override + * UnpackLayer must override ValidateLayer and RemoveLayer as well. + * + * @param path layer blob path. + * @param descriptor layer descriptor. + * @param diffID diff ID, empty if item has no image config. + * @return Error. + */ + virtual Error ValidateLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) + { + (void)path; + (void)descriptor; + (void)diffID; + + return ErrorEnum::eNotSupported; + } + + /** + * Unpacks installed layer. Called by the image manager after the layer blob is installed if ValidateLayer fails, + * derived classes can override it to unpack layer data. Returns eNotSupported by default, which the image manager + * treats as nothing to do (skip). + * + * @param path layer blob path. + * @param descriptor layer descriptor. + * @param diffID diff ID, empty if item has no image config. + * @return Error. + */ + virtual Error UnpackLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) + { + (void)path; + (void)descriptor; + (void)diffID; + + return ErrorEnum::eNotSupported; + } + + /** + * Removes installed layer. Called by the image manager before the layer is unpacked if ValidateLayer fails, and + * before the layer blob is removed, derived classes can override it to remove unpacked layer data. Returns + * eNotSupported by default, which the image manager treats as nothing to do (skip). + * + * @param path layer blob path. + * @param descriptor layer descriptor. + * @param diffID diff ID, empty if item has no image config. + * @return RetWithError: size of the unpacked layer data freed. + */ + virtual RetWithError RemoveLayer( + const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) + { + (void)path; + (void)descriptor; + (void)diffID; + + return {0, ErrorEnum::eNotSupported}; + } + + /** + * Removes orphan unpacked layers, i.e. the unpacked layers not used by any layer blob. Called by the image manager + * after the orphan blobs are removed, on start and every mRemoveOutdatedPeriod, derived classes which keep the + * unpacked layers override it. Returns eNotSupported by default, which the image manager treats as nothing to do + * (skip). The image manager frees the returned size in the space allocator, so derived classes must not do it. + * + * @return RetWithError: size of the removed orphan layers, freed even if an error is returned. + */ + virtual RetWithError RemoveOrphanLayers() { return {0, ErrorEnum::eNotSupported}; } + + class ItemRemover : public spaceallocator::ItemRemoverItf { + public: + explicit ItemRemover(ImageManager& imageManager) + : mImageManager(imageManager) + { + } + + RetWithError RemoveItem(const String& itemID, const String& version) override + { + return mImageManager.RemoveOutdatedItem(itemID, version); + } + + private: + ImageManager& mImageManager; + }; + + RetWithError RemoveOutdatedItem(const String& itemID, const String& version); + RetWithError RemoveItemFromCache(const String& itemID, const String& version); + void ProcessCleanup(); + Error RemoveExpiredItems(); + Error RemoveExpiredItem(const String& itemID, const String& version, const Time& timestamp); + bool IsItemExpired(const DeployableItemInfo& item) const; + RetWithError RemoveOrphanBlobs(); + RetWithError RemoveOrphanBlob(const String& path, const String& digest); + RetWithError RemoveUnusedItemBlob(const ItemBlobInfo& blob); + + Error ValidateBlob(const String& path, const String& digest, size_t size = 0); + Error GetBlobInfo(const String& digest, BlobInfo& info); + Error DownloadItemBlob(const InstallItemInfo& itemInfo, const String& path, const String& digest); + Error ResizeBlobSpace(spaceallocator::SpaceItf& space, const String& path); + RetWithError RemoveBlob(const String& path, const String& digest); + Error RemoveItemBlob( + const String& path, const oci::ContentDescriptor& descriptor, bool isLayer = false, const String& diffID = ""); + Error InstallItemBlob(const InstallItemInfo& itemInfo, const oci::ContentDescriptor& descriptor, String& path); + Error InstallLayerBlob( + const InstallItemInfo& itemInfo, const oci::ContentDescriptor& descriptor, const String& diffID); + Error ProcessBlob(const InstallItemInfo& itemInfo, const String& path, const oci::ContentDescriptor& descriptor); + Error ProcessLayer(const InstallItemInfo& itemInfo, const String& path, const oci::ContentDescriptor& descriptor, + const String& diffID); + Error InstallItemBlobs(const InstallItemInfo& itemInfo); + Error InstallManifestBlobs(const InstallItemInfo& itemInfo, const oci::ContentDescriptor& descriptor); + Error StartItemInstall(const InstallItemInfo& itemInfo, DeployableItemInfo& storedInfo); + Error AddStoredItem(const DeployableItemInfo& item); + Error AddItemBlob(const String& itemID, const String& version, const oci::ContentDescriptor& descriptor, + bool isLayer = false, const String& diffID = ""); + + Error SetInProgressItemState(const String& itemID, const String& version, const UpdateItemType& type, + ItemStateEnum state, const Error& error = ErrorEnum::eNone); + Error CancelInterruptedItem(DeployableItemInfo& item); + void RestoreRemovingItemState(const UpdateItemStatus& prevStatus, const Error& error); + Error SetStoredItemState(DeployableItemInfo& item, ItemStateEnum state, const Error& error = ErrorEnum::eNone); + + void NotifyItemStatusChanged(const UpdateItemStatus& status); + void NotifyItemRemovedFromCache(const String& itemID, const String& version); + + Error LockItem(const String& itemID, const String& version); + bool TryLockItem(const String& itemID, const String& version); + void UnlockItem(const String& itemID, const String& version); + UpdateItemStatus* FindInProgressItem(const String& itemID, const String& version); + Error LockBlob(const String& digest); + bool TryLockBlob(const String& digest); + void UnlockBlob(const String& digest); + + bool IsBlobInProgress(const String& digest) const + { + return mBlobsInProgress.Find(digest) != mBlobsInProgress.end(); + } + + Config mConfig; + BlobInfoProviderItf* mBlobInfoProvider {}; + StorageItf* mStorage {}; + spaceallocator::SpaceAllocatorItf* mSpaceAllocator {}; + fs::FileInfoProviderItf* mFileInfoProvider {}; + oci::OCISpecItf* mOCISpec {}; + fs::FSPlatformItf* mFSPlatform {}; + AllocatorItf* mAllocator {}; + + Mutex mListenersMutex; + StaticArray mListeners; + + Mutex mStorageMutex; + Mutex mCancelMutex; + + Mutex mMutex; + ConditionalVariable mCondVar; + StaticList mItemsInProgress; + size_t mNumItemsWaiting {}; + CancelContext mCancelContext; + StaticList, cMaxNumInProgress> mBlobsInProgress; + + Timer mTimer; + Thread<> mThread; + bool mClose {}; + bool mRunCleanup {}; + + ItemRemover mItemRemover {*this}; +}; + +} // namespace aos::common::imagemanager + +#endif diff --git a/src/core/common/imagemanager/imagemanager.md b/src/core/common/imagemanager/imagemanager.md new file mode 100644 index 000000000..4a5087c39 --- /dev/null +++ b/src/core/common/imagemanager/imagemanager.md @@ -0,0 +1,380 @@ +# Image manager + +Common image manager implements the part of the image manager shared by CM and SM: installing and removing a single +version of a deployable item, tracking the blobs used by each item version and cleaning up the unused ones. + +It is the base class of the CM and SM image managers. They call its protected `Init`, `InstallItem`, `RemoveItem` and +`Cancel` methods from their own API and specialize the blob download and the layer handling. + +It implements the following interfaces: + +* [aos::common::imagemanager::ItemStatusProviderItf](itf/itemstatusprovider.hpp) - provides deployable items statuses; +* [aos::common::imagemanager::ItemInfoProviderItf](itf/iteminfoprovider.hpp) - provides local blob and layer paths; +* [aos::spaceallocator::ItemRemoverItf](../spaceallocator/itf/spaceallocator.hpp) - removes outdated items, it is + returned by `GetItemRemover` and should be passed to the space allocator. + +It requires the following interfaces: + +* [aos::common::imagemanager::StorageItf](itf/storage.hpp) - stores deployable items and their blob references; +* [aos::common::imagemanager::BlobInfoProviderItf](itf/blobinfoprovider.hpp) - gets the info to download a blob; +* [aos::spaceallocator::SpaceAllocatorItf](../spaceallocator/itf/spaceallocator.hpp) - allocates the space for blobs + and keeps the outdated items; +* [aos::fs::FileInfoProviderItf](../tools/fs.hpp) - gets blob file size and checksum; +* [aos::oci::OCISpecItf](../ocispec/itf/ocispec.hpp) - parses image index, manifest and config; +* [aos::fs::FSPlatformItf](../tools/fs.hpp) - flushes the file system containing the blobs. + +It notifies the following interface: + +* [aos::common::imagemanager::ItemStatusListenerItf](itf/itemstatusprovider.hpp) - receives item status changes and + item removals from the cache. + +```mermaid +classDiagram + direction LR + + class ImageManager ["aos::common::imagemanager::ImageManager"] { + <> + } + + class CMImageManager ["CM image manager"] { + } + + class SMImageManager ["SM image manager"] { + } + + class ItemStatusProviderItf ["aos::common::imagemanager::ItemStatusProviderItf"] { + <> + } + + class ItemInfoProviderItf ["aos::common::imagemanager::ItemInfoProviderItf"] { + <> + } + + class ItemRemoverItf ["aos::spaceallocator::ItemRemoverItf"] { + <> + } + + class ItemStatusListenerItf ["aos::common::imagemanager::ItemStatusListenerItf"] { + <> + } + + class StorageItf ["aos::common::imagemanager::StorageItf"] { + <> + } + + class BlobInfoProviderItf ["aos::common::imagemanager::BlobInfoProviderItf"] { + <> + } + + class SpaceAllocatorItf ["aos::spaceallocator::SpaceAllocatorItf"] { + <> + } + + class FileInfoProviderItf ["aos::fs::FileInfoProviderItf"] { + <> + } + + class OCISpecItf ["aos::oci::OCISpecItf"] { + <> + } + + class FSPlatformItf ["aos::fs::FSPlatformItf"] { + <> + } + + CMImageManager --|> ImageManager + SMImageManager --|> ImageManager + + ImageManager ..|> ItemStatusProviderItf + ImageManager ..|> ItemInfoProviderItf + ImageManager ..|> ItemRemoverItf + + ImageManager ..> ItemStatusListenerItf + ImageManager ..> StorageItf + ImageManager ..> BlobInfoProviderItf + ImageManager ..> SpaceAllocatorItf + ImageManager ..> FileInfoProviderItf + ImageManager ..> OCISpecItf + ImageManager ..> FSPlatformItf +``` + +## Overview + +A deployable item version is identified by its item ID and version. The root digest of a stored version never changes: +installing the same version with another root digest fails. Handling of a set of items is up to the caller. + +Blobs are stored by digest and can be shared between items and versions: + +```console +/blobs/sha256/3588d02542238316759cbf24502f4344ffcc8a60c803870022f335d1390c13b4 +/blobs/sha512/... +``` + +The storage keeps a reference from each item version to each blob it uses. A blob is used if at least one stored item +references it, whatever the state of the item. The reference is added before the blob is validated or downloaded, so +the item being installed protects its blobs from being removed. + +A removed item stays in the storage with its blobs in the `eRemoved` state, and is added as outdated to the space +allocator. It is completely removed from the cache when its space or storage record is needed, or when its cache TTL +expires. + +## Item states + +| State | Reported when | +| -------------- | --------------------------------------------------------------------------------------------- | +| `eInstalling` | the install is started and no blob is being downloaded yet | +| `eDownloading` | the install has started downloading a blob | +| `eInstalled` | the item is installed | +| `eRemoving` | the removal is in progress | +| `eRemoved` | the item is removed and kept in the cache | +| `eFailed` | the last install failed, `mError` contains the error | + +`ePending` is not used by the image manager. + +## Configuration + +* `mImagePath` - path where the blobs are stored. `Init` fails with `eInvalidArgument` if it is too long to hold the + longest blob path; +* `mPartLimit` - limit of the space used by the blobs, in percent of the partition. It isn't used by the image manager + itself: the caller creates the space allocator with this limit; +* `mItemCacheTTL` - how long a removed item is kept in the cache; +* `mRemoveOutdatedPeriod` - how often the expired items and the orphan blobs are removed. + +## Initialization + +`Init` stores the configuration and the interfaces, and creates the blobs folder. It is called by the derived classes +from their own `Init`. + +### Start + +During start: + +* items left in `eInstalling` or `eDownloading` state are set to `eFailed` with the `eCanceled` error: their install was + interrupted, so they are handled as canceled items. Their blobs are kept and aren't downloaded again if the item is + installed again; +* items in `eRemoved` state are added as outdated to the space allocator. An error of adding an item is only logged; +* the cleanup thread and its timer are started, see [Cleanup](#cleanup). `Start` doesn't wait for the cleanup. + +No item status is notified on start: the changed items are reported by `GetItemsStatuses`. `Start` fails if the storage +can't be read or updated. + +### Stop + +`Stop` calls [Cancel](#cancel), then stops the timer and the cleanup thread, and waits until the cleanup in progress is +finished. + +## Installing item + +### InstallItem + +Installs a single version of a deployable item. Up to `cMaxNumConcurrentItems` items are processed at the same time. +The call waits while the same item version is being installed or removed. + +It performs the following steps: + +* adds the item to the storage in `eInstalling` state, or sets the stored item to this state. An item in `eRemoved` + state is restored from the outdated items of the space allocator. If the storage is full, the outdated items are + removed. If there is nothing to remove, the call fails with `eNoMemory`; +* processes the blobs starting from `mRootDigest`, see [Processing blob](#processing-blob). If `mIsRootIndex` is set, + the root blob is an image index and each of its manifests is processed. Otherwise, the root blob is an image manifest, + which is used by SM that manages a dedicated architecture; +* for each manifest, processes the manifest blob, the item config, the image config and then the layers, see + [Processing layer](#processing-layer). An empty config (`oci::cMediaTypeEmptyBlob`) is not parsed and is handled as no + image config; +* flushes the file system containing `mImagePath`; +* sets the item state to `eInstalled`. + +Errors detected before the install is started, such as a different root digest or a full storage, are only returned: +the storage isn't changed and no status is notified. + +Any later failure sets the item to `eFailed` with the error, including the cancel. The item stays in the storage with +its processed blobs until it is installed again or removed. If the `eFailed` state can't be stored, the item stays in +`eInstalling` state in the storage and is reported as `eFailed` until the next start. + +Installing a stored version repairs it: its blobs are validated and the missing or corrupted ones are downloaded again. + +### Processing blob + +Each blob is locked in the list of the blobs being processed, as it can be shared with another item being installed, +and is added to the storage as a blob of the item. Then: + +* the blob is validated, see [Blob validation](#blob-validation). A valid blob is not downloaded; +* if the validation fails with `eNotFound`, `eInvalidChecksum` or `eIntegrity`, the blob is repaired. Any other error + doesn't mean that the blob is invalid, so it is kept and the install fails; +* the existing blob file is removed and its space is freed in the space allocator; +* the blob info is requested from `BlobInfoProviderItf`, see [Getting blob info](#getting-blob-info); +* the space is allocated, `eDownloading` is notified and `DownloadBlob` is called; +* the space is resized to the size of the downloaded file. On success, it is accepted. On failure, the file is removed + and the space is released. If the file can't be removed, the space is accepted, as the file still uses it; +* the blob is validated again. If it fails, the install fails and the invalid blob is repaired on the next install. + +### Blob validation + +The digest is split to the algorithm and the hash, and the file is compared with them. The validation fails with: + +* `eInvalidArgument` if the digest is malformed; +* `eNotSupported` if the algorithm is neither `sha256` nor `sha512`; +* `eNotFound` if the blob file doesn't exist; +* `eInvalidChecksum` if the checksum doesn't match the hash, or the size is known from the descriptor and doesn't match + the file size; +* any other error of `FileInfoProviderItf` as is, `eIntegrity` if the file can't be read because of the integrity + checked storage. + +### Getting blob info + +`eNotFound` of `GetBlobInfo` fails the install, as the provider doesn't have the blob. So the provider must return it +only if the blob is really missing, not for a temporary failure. On any other error, the request is retried every +`cRetryTimeout` until it succeeds or is canceled, as the provider may be unavailable for a while. + +### Processing layer + +The layers are handled through the virtual methods, see [Derived image managers](#derived-image-managers): + +* `ValidateLayer` is called first. On success, the layer is done and its blob isn't validated, as the derived class may + remove the blob after unpacking; +* `eNotSupported` means that the layer is kept as a packed blob only, so only the layer blob is processed; +* any other error means that the unpacked layer is missing or invalid: `RemoveLayer` is called and the returned size is + freed in the space allocator, the layer blob is processed and `UnpackLayer` is called. + +### Cancel + +Cancels all items being installed and waits until all `InstallItem` and `RemoveItem` calls in progress or waiting for +their item lock return. The removal itself is not canceled. + +The image manager keeps one cancel context. It is passed to each `DownloadBlob`, checked before each blob and after +waiting for a blob being processed by another item, and interrupts the blob info retries. Canceled items fail as +described in [InstallItem](#installitem), usually with `eCanceled`. + +The context is reset when the calls return, so items started after `Cancel` returns are not affected. `Cancel` calls are +serialized, as a call must not reset the context while another one still waits. + +## Removing item + +### RemoveItem + +Removes a single version of a deployable item, waiting while the same item version is being installed. It fails with +`eNotFound` if the version is not stored, and does nothing if the item is already in `eRemoved` state. + +It performs the following steps: + +* notifies `eRemoving`; +* sets the item state to `eRemoved` with the current timestamp and notifies it. If the state can't be stored, the + previous state is notified with the error and the call fails; +* unlocks the item and adds it as outdated to the space allocator. If adding fails, the call fails, but the item stays + in `eRemoved` state and is added again on the next start. + +The blobs and the blob references are kept, so they aren't downloaded again if the item is installed again. + +### Removing outdated items + +The space allocator removes the oldest outdated item with `aos::spaceallocator::ItemRemoverItf::RemoveItem`: when it +needs the space, when its list of outdated items is full, or when `InstallItem` needs a storage record. + +An item is skipped if it is locked, was already removed from the storage or isn't in the `eRemoved` state anymore. +Otherwise, it is removed from the cache: + +* the item and its blob references are removed from the storage; +* each blob which is not used by other items and is not being processed is removed, with `RemoveLayer` called for the + layers. A blob failed to be removed is logged and removed later as orphan; +* `OnItemRemovedFromCache` is notified and the freed size is returned to the space allocator. + +## Cleanup + +The cleanup runs in a separate thread, right after `Start` and then every `mRemoveOutdatedPeriod`. It can run at the +same time as `InstallItem` and `RemoveItem`, so it skips the items and blobs being processed. Its errors are logged, and +the failed items and blobs are processed again in the next period. + +It performs the following steps: + +* removes the expired items, i.e. the items in `eRemoved` state for at least `mItemCacheTTL`. They are removed from the + outdated items of the space allocator and from the cache as described in + [Removing outdated items](#removing-outdated-items). An item which stays is added as outdated again; +* removes the orphan blobs, i.e. the files in the blobs folder not referenced by any stored item. The files which don't + match a valid digest are removed as well; +* calls `RemoveOrphanLayers`, so the derived class removes its unused unpacked layers; +* frees the removed size in the space allocator. + +## Locking + +The space allocator calls the item remover synchronously with its own lock held, and the item remover takes the image +manager mutex. To avoid a deadlock: + +* the space allocator is never called with `mMutex` held; +* the item remover never calls the space allocator and never waits for an item or a blob: the locked ones are skipped; +* `mStorageMutex` serializes the check of the stored items count with adding an item. It can be held while the space + allocator is called, as the item remover doesn't take it. + +The locks are taken in the following order: `mStorageMutex`, the space allocator lock, `mMutex`. + +`mCancelMutex` serializes `Cancel` calls and is taken before `mMutex`. `mListenersMutex` is held while the listeners are +notified, and `mMutex` is not. + +## Derived image managers + +The following private virtual methods specialize the install. They are called only by the common image manager: + +* `DownloadBlob` - downloads the blob to the given path. It is pure virtual. It checks the cancel context before and + during the download, and returns `eCanceled` if it is canceled. It doesn't allocate the space and isn't required to + validate the blob; +* `ValidateLayer` - validates the unpacked layer. It only reads the layer; +* `UnpackLayer` - unpacks the validated layer blob. On failure, it leaves no partially unpacked content; +* `RemoveLayer` - removes the unpacked layer and returns its size. It must not call the space allocator or wait for + blobs and layers being processed, as it can be called by the item remover; +* `RemoveOrphanLayers` - removes the unpacked layers not used by any layer blob and returns their size, which is freed + by the common image manager. + +The layer methods return `eNotSupported` by default, which is treated as nothing to do. A derived class which overrides +`UnpackLayer` must override `ValidateLayer` and `RemoveLayer` as well. + +The derived classes can also use `GetSpaceAllocator` and the static `ParseDigest`, and override `GetLayerPath`. + +## aos::common::imagemanager::ItemStatusProviderItf + +### GetItemsStatuses + +Appends one status per item version in the storage to the output array, which is not cleared. An item being processed +is reported with its last notified state, otherwise the stored state is reported. An item left in `eInstalling` state +without an install in progress is reported as `eFailed`. + +### SetItemFail + +Sets the item to `eFailed` with the error provided by an external module, for example when the item instances fail. It +waits while the same item version is being processed. It fails with `eNotFound` if the version is not stored and with +`eWrongState` if the item is in `eRemoved` state. The item is handled as any other failed item: it stays in the storage +with its blobs until it is installed again or removed. + +### SubscribeListener + +Subscribes to item status changes. It returns `eAlreadyExist` if the listener is already subscribed. + +### UnsubscribeListener + +Unsubscribes from item status changes. It returns `eNotFound` if the listener is not subscribed. + +## aos::common::imagemanager::ItemStatusListenerItf + +Listeners are notified with the listeners mutex held, so a listener must not subscribe or unsubscribe from the callback. + +### OnItemStatusChanged + +Called for each listener when an item status is changed. The final status of the install is notified while the item is +still locked, so the listener must not call `InstallItem` or `RemoveItem` synchronously. + +### OnItemRemovedFromCache + +Called for each listener when an item is completely removed from the cache. It is not called when the item is set to +`eRemoved` state. It can be called from the item remover with the space allocator lock held, so the listener must not +call the image manager or the space allocator. + +## aos::common::imagemanager::ItemInfoProviderItf + +### GetBlobPath + +Returns the blob path `/blobs//` by digest. It fails with `eInvalidArgument` if the digest +is malformed. It doesn't check that the blob exists: a blob of an item in `eRemoved` state can be removed at any time, +so a consumer must handle a missing blob as an error. + +### GetLayerPath + +Returns the unpacked layer path by diff ID. It returns `eNotSupported`, as the common image manager doesn't keep the +unpacked layers. The derived class which keeps them overrides it. diff --git a/src/core/common/imagemanager/itf/blobinfoprovider.hpp b/src/core/common/imagemanager/itf/blobinfoprovider.hpp new file mode 100644 index 000000000..e64811ea3 --- /dev/null +++ b/src/core/common/imagemanager/itf/blobinfoprovider.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_CORE_COMMON_IMAGEMANAGER_ITF_BLOBINFOPROVIDER_HPP_ +#define AOS_CORE_COMMON_IMAGEMANAGER_ITF_BLOBINFOPROVIDER_HPP_ + +#include + +namespace aos::common::imagemanager { + +/** + * Interface that provides download blob info. + */ +class BlobInfoProviderItf { +public: + /** + * Destructor. + */ + virtual ~BlobInfoProviderItf() = default; + + /** + * Returns blob info. + * + * @param digest blob digest. + * @param[out] blobInfo blob info. + * @return Error. + */ + virtual Error GetBlobInfo(const String& digest, BlobInfo& blobInfo) = 0; +}; + +} // namespace aos::common::imagemanager + +#endif diff --git a/src/core/common/imagemanager/itf/iteminfoprovider.hpp b/src/core/common/imagemanager/itf/iteminfoprovider.hpp new file mode 100644 index 000000000..c27a63b07 --- /dev/null +++ b/src/core/common/imagemanager/itf/iteminfoprovider.hpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_CORE_COMMON_IMAGEMANAGER_ITF_ITEMINFOPROVIDER_HPP_ +#define AOS_CORE_COMMON_IMAGEMANAGER_ITF_ITEMINFOPROVIDER_HPP_ + +#include +#include + +namespace aos::common::imagemanager { + +/** + * Interface that provides local blob and unpacked layer info. + */ +class ItemInfoProviderItf { +public: + /** + * Destructor. + */ + virtual ~ItemInfoProviderItf() = default; + + /** + * Returns local blob path. + * + * @param digest blob digest. + * @param[out] path local blob path. + * @return Error. + */ + virtual Error GetBlobPath(const String& digest, String& path) const = 0; + + /** + * Returns unpacked layer path. + * + * @param diffID layer diff ID. + * @param[out] path unpacked layer path. + * @return Error: eNotSupported if the image manager doesn't keep the unpacked layers. + */ + virtual Error GetLayerPath(const String& diffID, String& path) const = 0; +}; + +} // namespace aos::common::imagemanager + +#endif diff --git a/src/core/common/imagemanager/itf/itemstatusprovider.hpp b/src/core/common/imagemanager/itf/itemstatusprovider.hpp new file mode 100644 index 000000000..5bd37675b --- /dev/null +++ b/src/core/common/imagemanager/itf/itemstatusprovider.hpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_CORE_COMMON_IMAGEMANAGER_ITF_ITEMSTATUSPROVIDER_HPP_ +#define AOS_CORE_COMMON_IMAGEMANAGER_ITF_ITEMSTATUSPROVIDER_HPP_ + +#include + +namespace aos::common::imagemanager { + +/** + * Interface for receiving notification about update image statuses. + */ +class ItemStatusListenerItf { +public: + /** + * Destructor. + */ + virtual ~ItemStatusListenerItf() = default; + + /** + * Notifies about item status change. + * + * @param status update item status. + */ + virtual void OnItemStatusChanged(const UpdateItemStatus& status) = 0; + + /** + * Notifies about item removal from cache. + * + * @param id removed item id. + * @param version removed item version. + */ + virtual void OnItemRemovedFromCache(const String& id, const String& version) = 0; +}; + +/** + * Interface to provide deployable item statuses. + */ +class ItemStatusProviderItf { +public: + /** + * Destructor. + */ + virtual ~ItemStatusProviderItf() = default; + + /** + * Retrieves deployable items statuses. + * + * @param[out] statuses list of deployable items statuses. + * @return Error. + */ + virtual Error GetItemsStatuses(Array& statuses) = 0; + + /** + * Sets item to failed state. Waits while the item version is being processed. + * + * @param itemID item ID. + * @param version item version. + * @param error failure reason. + * @return Error. + */ + virtual Error SetItemFail(const String& itemID, const String& version, const Error& error) = 0; + + /** + * Subscribes status notifications. + * + * @param listener status listener. + * @return Error. + */ + virtual Error SubscribeListener(ItemStatusListenerItf& listener) = 0; + + /** + * Unsubscribes from status notifications. + * + * @param listener status listener. + * @return Error. + */ + virtual Error UnsubscribeListener(ItemStatusListenerItf& listener) = 0; +}; + +} // namespace aos::common::imagemanager + +#endif diff --git a/src/core/common/imagemanager/itf/storage.hpp b/src/core/common/imagemanager/itf/storage.hpp new file mode 100644 index 000000000..892fc87b5 --- /dev/null +++ b/src/core/common/imagemanager/itf/storage.hpp @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_CORE_COMMON_IMAGEMANAGER_ITF_STORAGE_HPP_ +#define AOS_CORE_COMMON_IMAGEMANAGER_ITF_STORAGE_HPP_ + +#include +#include +#include + +namespace aos::common::imagemanager { + +/** + * Deployable item info. + */ +struct DeployableItemInfo : public UpdateItemStatus { + StaticString mRootDigest; + Time mTimestamp; + + /** + * Compares deployable item info. + * + * @param lhs left object. + * @param rhs right object. + * @return bool. + */ + friend bool operator==(const DeployableItemInfo& lhs, const DeployableItemInfo& rhs) + { + return (static_cast(lhs) == static_cast(rhs)) + && lhs.mRootDigest == rhs.mRootDigest && lhs.mTimestamp == rhs.mTimestamp; + } + + /** + * Compares deployable item info. + * + * @param lhs left object. + * @param rhs right object. + * @return bool. + */ + friend bool operator!=(const DeployableItemInfo& lhs, const DeployableItemInfo& rhs) { return !(lhs == rhs); } +}; + +/** + * Item blob info. + */ +struct ItemBlobInfo : public oci::ContentDescriptor { + bool mIsLayer {}; + StaticString mDiffID; + + /** + * Compares item blob info. + * + * @param lhs left object. + * @param rhs right object. + * @return bool. + */ + friend bool operator==(const ItemBlobInfo& lhs, const ItemBlobInfo& rhs) + { + return (static_cast(lhs) == static_cast(rhs)) + && lhs.mIsLayer == rhs.mIsLayer && lhs.mDiffID == rhs.mDiffID; + } + + /** + * Compares item blob info. + * + * @param lhs left object. + * @param rhs right object. + * @return bool. + */ + friend bool operator!=(const ItemBlobInfo& lhs, const ItemBlobInfo& rhs) { return !(lhs == rhs); } +}; + +/** + * Image manager storage interface. + */ +class StorageItf { +public: + /** + * Destructor. + */ + virtual ~StorageItf() = default; + + /** + * Adds deployable item. + * + * @param item deployable item info. + * @return Error: eAlreadyExist if item with the same ID and version exists. + */ + virtual Error AddDeployableItem(const DeployableItemInfo& item) = 0; + + /** + * Updates deployable item. Blob references of the item are kept. + * + * @param item deployable item info. + * @return Error: eNotFound if item doesn't exist. + */ + virtual Error UpdateDeployableItem(const DeployableItemInfo& item) = 0; + + /** + * Removes deployable item and all its blob references. + * + * @param itemID item ID. + * @param version item version. + * @return Error: eNotFound if item doesn't exist. + */ + virtual Error RemoveDeployableItem(const String& itemID, const String& version) = 0; + + /** + * Returns deployable item. + * + * @param itemID item ID. + * @param version item version. + * @param[out] item deployable item info. + * @return Error: eNotFound if item doesn't exist. + */ + virtual Error GetDeployableItem(const String& itemID, const String& version, DeployableItemInfo& item) = 0; + + /** + * Returns all deployable items. + * + * @param[out] items deployable items. + * @return Error. + */ + virtual Error GetAllDeployableItems(Array& items) = 0; + + /** + * Returns number of stored deployable items in all states. + * + * @return RetWithError. + */ + virtual RetWithError GetDeployableItemsCount() = 0; + + /** + * Adds blob reference to deployable item. Does nothing if the reference already exists. + * + * @param itemID item ID. + * @param version item version. + * @param blob blob info. + * @return Error: eNotFound if item doesn't exist. + */ + virtual Error AddItemBlob(const String& itemID, const String& version, const ItemBlobInfo& blob) = 0; + + /** + * Returns blobs referenced by deployable item. + * + * @param itemID item ID. + * @param version item version. + * @param[out] blobs blobs info. + * @return Error: eNotFound if item doesn't exist. + */ + virtual Error GetItemBlobs(const String& itemID, const String& version, Array& blobs) = 0; + + /** + * Checks if blob is referenced by any existing deployable item, regardless of the item state. Blob references + * left without an item are ignored. + * + * @param digest blob digest. + * @return RetWithError. + */ + virtual RetWithError IsBlobUsed(const String& digest) = 0; +}; + +} // namespace aos::common::imagemanager + +#endif diff --git a/src/core/common/imagemanager/tests/CMakeLists.txt b/src/core/common/imagemanager/tests/CMakeLists.txt new file mode 100644 index 000000000..09df6587d --- /dev/null +++ b/src/core/common/imagemanager/tests/CMakeLists.txt @@ -0,0 +1,35 @@ +# +# Copyright (C) 2026 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +set(TARGET_NAME imagemanager_test) + +# ###################################################################################################################### +# Sources +# ###################################################################################################################### + +set(SOURCES imagemanager.cpp) + +# ###################################################################################################################### +# Libraries +# ###################################################################################################################### + +set(LIBRARIES aos::core::common::imagemanager aos::core::common::tests::mocks aos::core::common::tests::utils + GTest::gmock_main +) + +# ###################################################################################################################### +# Target +# ###################################################################################################################### + +add_test( + TARGET_NAME + ${TARGET_NAME} + LOG_MODULE + SOURCES + ${SOURCES} + LIBRARIES + ${LIBRARIES} +) diff --git a/src/core/common/imagemanager/tests/imagemanager.cpp b/src/core/common/imagemanager/tests/imagemanager.cpp new file mode 100644 index 000000000..64a6e997f --- /dev/null +++ b/src/core/common/imagemanager/tests/imagemanager.cpp @@ -0,0 +1,3668 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "stubs/storagestub.hpp" + +using namespace testing; + +namespace aos::common::imagemanager { + +namespace { + +/*********************************************************************************************************************** + * Consts + **********************************************************************************************************************/ + +constexpr auto cTestDir = "/tmp/common_imagemanager_test"; +constexpr auto cTestImagePath = "/tmp/common_imagemanager_test/images"; +constexpr auto cTestHash = "a3b2c1d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90"; +constexpr auto cTestDigest = "sha256:a3b2c1d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90"; +constexpr auto cOtherHash = "0000000000000000000000000000000000000000000000000000000000000000"; +constexpr auto cOtherDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; +constexpr auto cTestBlobPath = "/tmp/common_imagemanager_test/images/blobs/sha256/" + "a3b2c1d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90"; +constexpr auto cTestLayerType = "application/vnd.oci.image.layer.v1.tar+gzip"; + +constexpr auto cMaxImagePathSize = cFilePathLen - (sizeof("/blobs/") - 1) - oci::cDigestLen; + +constexpr char cTestBlobContent[] = "test blob content"; +constexpr size_t cTestBlobSize = sizeof(cTestBlobContent) - 1; + +constexpr auto cTestItemID = "item1"; +constexpr auto cTestVersion = "1.0.0"; +constexpr auto cImageConfigType = "application/vnd.oci.image.config.v1+json"; +constexpr auto cItemConfigType = "application/vnd.aos.item.config.v1+json"; +constexpr auto cInstallBlobSize = 64; +constexpr auto cItemCacheTTL = 10 * Time::cSeconds; +constexpr auto cWaitTimeout = std::chrono::seconds(5); + +// Same as the image manager private constants. +constexpr auto cMaxNumStoredItems = cMaxNumUpdateItems * 2; +constexpr auto cMaxNumItemBlobs = 1 + oci::cMaxNumManifests * (3 + oci::cMaxNumLayers); + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +void CreateFile(const String& path, const String& content) +{ + auto err = fs::MakeDirAll(fs::Dir(path)); + ASSERT_TRUE(err.IsNone()) << "Failed to create dir: " << tests::utils::ErrorToStr(err); + + err = fs::WriteStringToFile(path, content, 0600); + ASSERT_TRUE(err.IsNone()) << "Failed to create file: " << tests::utils::ErrorToStr(err); +} + +bool FileExists(const String& path) +{ + auto [exists, err] = fs::FileExist(path); + EXPECT_TRUE(err.IsNone()) << "Failed to check file: " << tests::utils::ErrorToStr(err); + + return exists; +} + +DeployableItemInfo CreateDeployableItem( + const String& itemID, const String& version, ItemStateEnum state, const Error& error = ErrorEnum::eNone) +{ + DeployableItemInfo item; + + item.mItemID = itemID; + item.mType = UpdateItemTypeEnum::eService; + item.mVersion = version; + item.mRootDigest = cTestDigest; + item.mState = state; + item.mTimestamp = Time::Now(); + item.mError = error; + + return item; +} + +std::string MakeImagePath(size_t size) +{ + auto path = std::string(cTestDir) + "/"; + + // Split the path into short folders to fit the file name limit. + while (path.size() < size) { + path += (path.size() % 100 == 0 && path.size() + 1 < size) ? '/' : 'a'; + } + + return path; +} + +UpdateItemStatus CreateItemStatus(const String& itemID, const String& version, ItemStateEnum state) +{ + UpdateItemStatus status; + + status.mItemID = itemID; + status.mType = UpdateItemTypeEnum::eService; + status.mVersion = version; + status.mState = state; + + return status; +} + +const UpdateItemStatus* FindStatus(const Array& statuses, const String& itemID, const String& version) +{ + auto it = statuses.FindIf([&itemID, &version](const UpdateItemStatus& status) { + return status.mItemID == itemID && status.mVersion == version; + }); + + return it != statuses.end() ? it : nullptr; +} + +/** + * Test blob: the blob content is its hash, so the checksum is calculated from the content. + */ +struct TestBlob { + std::string mHash; + std::string mDigest; + std::string mPath; +}; + +TestBlob MakeTestBlob(uint32_t id) +{ + char hash[65]; + + std::snprintf(hash, sizeof(hash), "%064x", id); + + TestBlob blob; + + blob.mHash = hash; + blob.mDigest = std::string("sha256:") + hash; + blob.mPath = std::string(cTestImagePath) + "/blobs/sha256/" + hash; + + return blob; +} + +oci::ContentDescriptor MakeDescriptor(const TestBlob& blob, const char* mediaType) +{ + return oci::ContentDescriptor {mediaType, blob.mDigest.c_str(), cInstallBlobSize}; +} + +std::string ReadFile(const std::string& path) +{ + std::ifstream file(path); + std::stringstream content; + + content << file.rdbuf(); + + return content.str(); +} + +Error GetFileInfoFromContent(const String& path, fs::FileInfo& info, const crypto::Hash&) +{ + auto content = ReadFile(path.CStr()); + + info.mCheckSum.Clear(); + info.mSize = content.size(); + + // Content which is not a hash gives an empty checksum, so the validation fails. + if (content.size() == crypto::cSHA256Size * 2) { + StaticString hash {content.c_str()}; + + if (!hash.HexToByteArray(info.mCheckSum).IsNone()) { + info.mCheckSum.Clear(); + } + } + + return ErrorEnum::eNone; +} + +Error GetTestBlobInfo(const String& digest, BlobInfo& blobInfo) +{ + blobInfo.mDigest = digest; + blobInfo.mSize = cInstallBlobSize; + + return ErrorEnum::eNone; +} + +Error DownloadTestBlob(const String& path, const BlobInfo& blobInfo) +{ + // Write the blob hash as the blob content. + return fs::WriteStringToFile(path, String(blobInfo.mDigest.CStr() + std::strlen("sha256:")), 0600); +} + +/*********************************************************************************************************************** + * Test image manager + **********************************************************************************************************************/ + +class TestImageManager : public ImageManager { +public: + using ImageManager::Init; + using ImageManager::InstallItemInfo; + + struct LayerCall { + std::string mDigest; + std::string mDiffDigest; + + bool operator==(const LayerCall& rhs) const { return mDigest == rhs.mDigest && mDiffDigest == rhs.mDiffDigest; } + }; + + Error Install(const InstallItemInfo& itemInfo) { return InstallItem(itemInfo); } + Error Remove(const String& itemID, const String& version) { return RemoveItem(itemID, version); } + Error CancelItems() { return Cancel(); } + static Error ParseDigestItem(const String& digest, String& algorithm, String& hash) + { + return ParseDigest(digest, algorithm, hash); + } + + Error ValidateLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) override + { + { + std::lock_guard lock {mLayerCallsMutex}; + + mValidateLayerCalls.push_back({descriptor.mDigest.CStr(), diffID.CStr()}); + } + + return CheckLayer(path, descriptor, diffID); + } + + std::vector GetValidateLayerCalls() + { + std::lock_guard lock {mLayerCallsMutex}; + + return mValidateLayerCalls; + } + + Error DownloadBlob(CancelContext& ctx, const String& path, const BlobInfo& blobInfo) override + { + mDownloadContext = &ctx; + + return DownloadBlob(path, blobInfo); + } + + // Returns the cancel context of the download in progress, called from the DownloadBlob action. + CancelContext& GetDownloadContext() { return *mDownloadContext; } + + MOCK_METHOD(Error, DownloadBlob, (const String& path, const BlobInfo& blobInfo)); + MOCK_METHOD( + Error, CheckLayer, (const String& path, const oci::ContentDescriptor& descriptor, const String& diffID)); + MOCK_METHOD(Error, UnpackLayer, + (const String& path, const oci::ContentDescriptor& descriptor, const String& diffID), (override)); + MOCK_METHOD((RetWithError), RemoveLayer, + (const String& path, const oci::ContentDescriptor& descriptor, const String& diffID), (override)); + MOCK_METHOD((RetWithError), RemoveOrphanLayers, (), (override)); + +private: + std::mutex mLayerCallsMutex; + std::vector mValidateLayerCalls; + CancelContext* mDownloadContext {}; +}; + +} // namespace + +/*********************************************************************************************************************** + * Suite + **********************************************************************************************************************/ + +class CommonImageManagerTest : public Test { +protected: + static void SetUpTestSuite() { tests::utils::InitLog(); } + + void SetUp() override + { + auto err = fs::RemoveAll(cTestDir); + ASSERT_TRUE(err.IsNone()) << "Failed to remove test dir: " << tests::utils::ErrorToStr(err); + + // By default, the layers are kept as packed blobs only. + ON_CALL(mImageManager, CheckLayer(_, _, _)).WillByDefault(Return(ErrorEnum::eNotSupported)); + ON_CALL(mImageManager, UnpackLayer(_, _, _)).WillByDefault(Return(ErrorEnum::eNotSupported)); + ON_CALL(mImageManager, RemoveLayer(_, _, _)) + .WillByDefault(Return(RetWithError(0, ErrorEnum::eNotSupported))); + ON_CALL(mImageManager, RemoveOrphanLayers()) + .WillByDefault(Return(RetWithError(0, ErrorEnum::eNotSupported))); + } + + void TearDown() override + { + auto err = mImageManager.Stop(); + EXPECT_TRUE(err.IsNone()) << "Failed to stop image manager: " << tests::utils::ErrorToStr(err); + + err = fs::RemoveAll(cTestDir); + EXPECT_TRUE(err.IsNone()) << "Failed to remove test dir: " << tests::utils::ErrorToStr(err); + } + + Error InitImageManager(const String& imagePath = cTestImagePath, Duration removeOutdatedPeriod = 5 * Time::cSeconds) + { + Config config {imagePath, 0, cItemCacheTTL, removeOutdatedPeriod}; + + return mImageManager.Init( + config, mBlobInfoProvider, mStorage, mSpaceAllocator, mFileInfoProvider, mOCISpec, mFSPlatform, mAllocator); + } + + void AddStoredItem( + const String& itemID, const String& version, ItemStateEnum state, const Error& error = ErrorEnum::eNone) + { + auto err = mStorage.AddDeployableItem(CreateDeployableItem(itemID, version, state, error)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + } + + // mAllocator must be declared before any member that allocates from it, so it is destroyed after them, since + // members are destroyed in reverse declaration order. + HeapAllocator mAllocator; + NiceMock mBlobInfoProvider; + StorageStub mStorage; + NiceMock mSpaceAllocator; + NiceMock mFileInfoProvider; + NiceMock mOCISpec; + NiceMock mFSPlatform; + NiceMock mListener; + NiceMock mImageManager; +}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(CommonImageManagerTest, Init) +{ + auto err = InitImageManager(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + auto [exists, existErr] = fs::DirExist(fs::JoinPath(cTestImagePath, "blobs")); + ASSERT_TRUE(existErr.IsNone()) << tests::utils::ErrorToStr(existErr); + EXPECT_TRUE(exists); +} + +TEST_F(CommonImageManagerTest, InitFailsIfImageDirCantBeCreated) +{ + const auto filePath = fs::JoinPath(cTestDir, "file"); + + CreateFile(filePath, "content"); + + auto err = InitImageManager(fs::JoinPath(filePath, "images")); + EXPECT_FALSE(err.IsNone()); +} + +TEST_F(CommonImageManagerTest, InitFailsIfImagePathTooLong) +{ + const auto imagePath = MakeImagePath(cMaxImagePathSize + 1); + + auto err = InitImageManager(imagePath.c_str()); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerTest, GetBlobPathLongestImagePathAndDigest) +{ + const auto imagePath = MakeImagePath(cMaxImagePathSize); + const auto hash = std::string(oci::cDigestLen - strlen("sha256:"), '0'); + + auto err = InitImageManager(imagePath.c_str()); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + StaticString path; + + err = mImageManager.GetBlobPath(("sha256:" + hash).c_str(), path); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_EQ(path.Size(), cFilePathLen); + EXPECT_STREQ(path.CStr(), (imagePath + "/blobs/sha256/" + hash).c_str()); +} + +TEST_F(CommonImageManagerTest, GetBlobPathDigestTooLong) +{ + const auto imagePath = MakeImagePath(cMaxImagePathSize); + const auto digest = "sha256:" + std::string(oci::cDigestLen, '0'); + + auto err = InitImageManager(imagePath.c_str()); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + StaticString path; + + err = mImageManager.GetBlobPath(digest.c_str(), path); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerTest, GetBlobPath) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + StaticString path; + + auto err = mImageManager.GetBlobPath(cTestDigest, path); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_EQ(path, cTestBlobPath); + + EXPECT_FALSE(FileExists(path)); +} + +TEST_F(CommonImageManagerTest, GetLayerPathNotSupported) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + StaticString path; + + auto err = mImageManager.GetLayerPath(cTestDigest, path); + EXPECT_TRUE(err.Is(ErrorEnum::eNotSupported)) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerTest, GetBlobPathMalformedDigest) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + for (const auto& digest : {"", "sha256", "sha256:", ":abc", "sha256:ab:cd", "sha256:../abc", "../x:abc", + "sha256:ab/cd", "SHA256:abc", ".:abc", "sha256:ab-cd", "sha256:ab_cd", "sha256:abc="}) { + StaticString path; + + auto err = mImageManager.GetBlobPath(digest, path); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << digest << ": " << tests::utils::ErrorToStr(err); + } +} + +TEST_F(CommonImageManagerTest, ParseDigest) +{ + StaticString algorithm; + StaticString hash; + + auto err = TestImageManager::ParseDigestItem(cTestDigest, algorithm, hash); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_STREQ(algorithm.CStr(), "sha256"); + EXPECT_STREQ(hash.CStr(), cTestHash); + + for (const auto* digest : {"", "sha256", ":hash", "sha256:", "SHA256:hash", "sha256:../hash", "../sha256:hash"}) { + err = TestImageManager::ParseDigestItem(digest, algorithm, hash); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << digest; + } + + // The whole digest has to fit the digest length, not only its parts. + const auto tooLong = "sha256:" + std::string(oci::cDigestLen, '0'); + + err = TestImageManager::ParseDigestItem(tooLong.c_str(), algorithm, hash); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); + + const auto longest = "sha256:" + std::string(oci::cDigestLen - strlen("sha256:"), '0'); + + err = TestImageManager::ParseDigestItem(longest.c_str(), algorithm, hash); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerTest, SubscribeListenerTwice) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + auto err = mImageManager.SubscribeListener(mListener); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mImageManager.SubscribeListener(mListener); + EXPECT_TRUE(err.Is(ErrorEnum::eAlreadyExist)) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerTest, UnsubscribeUnknownListener) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + auto err = mImageManager.UnsubscribeListener(mListener); + EXPECT_TRUE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerTest, SubscribeUnsubscribeListener) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + auto err = mImageManager.SubscribeListener(mListener); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mImageManager.UnsubscribeListener(mListener); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mImageManager.UnsubscribeListener(mListener); + EXPECT_TRUE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); + + err = mImageManager.SubscribeListener(mListener); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerTest, StartCancelsInterruptedItemsAndAddsOutdatedItems) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + ASSERT_TRUE(mImageManager.SubscribeListener(mListener).IsNone()); + + AddStoredItem("item1", "1.0.0", ItemStateEnum::eInstalled); + AddStoredItem("item1", "2.0.0", ItemStateEnum::eInstalling); + AddStoredItem("item2", "1.0.0", ItemStateEnum::eRemoved); + + ASSERT_TRUE( + mStorage.AddItemBlob("item1", "2.0.0", ItemBlobInfo {{cTestLayerType, cTestDigest, 1}, true, ""}).IsNone()); + AddStoredItem("item3", "1.0.0", ItemStateEnum::eFailed, Error(ErrorEnum::eFailed, "download failed")); + AddStoredItem("item4", "1.0.0", ItemStateEnum::eRemoved); + AddStoredItem("item5", "1.0.0", ItemStateEnum::eDownloading); + + std::map storedItems; + + for (const auto& [itemID, version] : std::vector>( + {{"item1", "1.0.0"}, {"item2", "1.0.0"}, {"item3", "1.0.0"}, {"item4", "1.0.0"}})) { + DeployableItemInfo item; + + ASSERT_TRUE(mStorage.GetDeployableItem(itemID, version, item).IsNone()); + + storedItems[std::string(itemID) + ":" + version] = item; + } + + EXPECT_CALL(mListener, OnItemStatusChanged(_)).Times(0); + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(_, _, _)).Times(0); + EXPECT_CALL( + mSpaceAllocator, AddOutdatedItem(String("item2"), String("1.0.0"), storedItems["item2:1.0.0"].mTimestamp)) + .WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL( + mSpaceAllocator, AddOutdatedItem(String("item4"), String("1.0.0"), storedItems["item4:1.0.0"].mTimestamp)) + .WillOnce(Return(ErrorEnum::eNone)); + + auto err = mImageManager.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + DeployableItemInfo item; + + ASSERT_TRUE(mStorage.GetDeployableItem("item1", "2.0.0", item).IsNone()); + EXPECT_EQ(item.mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item.mError.Is(ErrorEnum::eCanceled)) << tests::utils::ErrorToStr(item.mError); + + ASSERT_TRUE(mStorage.GetDeployableItem("item5", "1.0.0", item).IsNone()); + EXPECT_EQ(item.mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item.mError.Is(ErrorEnum::eCanceled)) << tests::utils::ErrorToStr(item.mError); + + // The blob references of the interrupted item are kept. + auto blobs = std::make_unique>(); + + ASSERT_TRUE(mStorage.GetItemBlobs("item1", "2.0.0", *blobs).IsNone()); + EXPECT_EQ(blobs->Size(), 1); + + // The other items are not changed. + for (const auto& [key, storedItem] : storedItems) { + ASSERT_TRUE(mStorage.GetDeployableItem(storedItem.mItemID, storedItem.mVersion, item).IsNone()); + EXPECT_EQ(item, storedItem) << key; + } +} + +TEST_F(CommonImageManagerTest, StartAddOutdatedItemFails) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + AddStoredItem("item1", "1.0.0", ItemStateEnum::eRemoved); + AddStoredItem("item2", "1.0.0", ItemStateEnum::eRemoved); + + // The failed item is only logged and the next items are still added. + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String("item1"), String("1.0.0"), _)) + .WillOnce(Return(ErrorEnum::eFailed)); + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String("item2"), String("1.0.0"), _)) + .WillOnce(Return(ErrorEnum::eNone)); + + auto err = mImageManager.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerTest, StartUpdateItemFails) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + AddStoredItem("item1", "1.0.0", ItemStateEnum::eInstalling); + + mStorage.SetFailUpdateStates({ItemStateEnum::eFailed}); + + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(_, _, _)).Times(0); + + auto err = mImageManager.Start(); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + DeployableItemInfo item; + + ASSERT_TRUE(mStorage.GetDeployableItem("item1", "1.0.0", item).IsNone()); + EXPECT_EQ(item.mState, ItemStateEnum::eInstalling); +} + +TEST_F(CommonImageManagerTest, GetItemsStatusesEmpty) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + StaticArray statuses; + + auto err = mImageManager.GetItemsStatuses(statuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_TRUE(statuses.IsEmpty()); +} + +TEST_F(CommonImageManagerTest, GetItemsStatusesStoredStates) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + AddStoredItem("item1", "1.0.0", ItemStateEnum::eInstalled); + AddStoredItem("item1", "2.0.0", ItemStateEnum::eRemoved); + AddStoredItem("item2", "1.0.0", ItemStateEnum::eFailed, Error(ErrorEnum::eFailed, "download failed")); + AddStoredItem("item3", "1.0.0", ItemStateEnum::eInstalling); + + StaticArray statuses; + + // The statuses are appended to the output array. + const auto preFilledStatus = CreateItemStatus("item0", "1.0.0", ItemStateEnum::eInstalled); + + ASSERT_TRUE(statuses.PushBack(preFilledStatus).IsNone()); + + auto err = mImageManager.GetItemsStatuses(statuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + ASSERT_EQ(statuses.Size(), 5); + EXPECT_EQ(statuses[0], preFilledStatus); + + for (const auto& status : statuses) { + EXPECT_EQ(status.mType, UpdateItemTypeEnum::eService); + EXPECT_FALSE(status.mPreinstalled); + } + + const auto* status = FindStatus(statuses, "item1", "1.0.0"); + ASSERT_NE(status, nullptr); + EXPECT_EQ(status->mState, ItemStateEnum::eInstalled); + EXPECT_TRUE(status->mError.IsNone()); + + status = FindStatus(statuses, "item1", "2.0.0"); + ASSERT_NE(status, nullptr); + EXPECT_EQ(status->mState, ItemStateEnum::eRemoved); + EXPECT_TRUE(status->mError.IsNone()); + + status = FindStatus(statuses, "item2", "1.0.0"); + ASSERT_NE(status, nullptr); + EXPECT_EQ(status->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(status->mError.Is(ErrorEnum::eFailed)); + EXPECT_STREQ(status->mError.Message(), "download failed"); + + // The item left in eInstalling state and not being installed is reported as eFailed without an error. + status = FindStatus(statuses, "item3", "1.0.0"); + ASSERT_NE(status, nullptr); + EXPECT_EQ(status->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(status->mError.IsNone()); +} + +TEST_F(CommonImageManagerTest, GetItemsStatusesNoMemory) +{ + ASSERT_TRUE(InitImageManager().IsNone()); + + AddStoredItem("item1", "1.0.0", ItemStateEnum::eInstalled); + AddStoredItem("item1", "2.0.0", ItemStateEnum::eInstalled); + + StaticArray statuses; + + auto err = mImageManager.GetItemsStatuses(statuses); + EXPECT_TRUE(err.Is(ErrorEnum::eNoMemory)) << tests::utils::ErrorToStr(err); +} + +/*********************************************************************************************************************** + * Install item suite + **********************************************************************************************************************/ + +class CommonImageManagerInstallTest : public CommonImageManagerTest { +protected: + struct TestManifest { + TestBlob mManifest; + TestBlob mImageConfig; + TestBlob mItemConfig; + std::vector mLayers; + std::vector mDiffIDs; + }; + + void SetUp() override + { + CommonImageManagerTest::SetUp(); + + ON_CALL(mFileInfoProvider, GetFileInfo(_, _, _)).WillByDefault(Invoke(GetFileInfoFromContent)); + ON_CALL(mBlobInfoProvider, GetBlobInfo(_, _)).WillByDefault(Invoke(GetTestBlobInfo)); + ON_CALL(mImageManager, DownloadBlob(_, _)).WillByDefault(Invoke(DownloadTestBlob)); + + ON_CALL(mSpaceAllocator, AllocateSpace(_)) + .WillByDefault(Invoke([this](size_t) -> RetWithError> { + auto space = MakeUnique>(&mAllocator); + + ON_CALL(*space, Accept()).WillByDefault(Invoke([this]() { + mAcceptCount++; + + return ErrorEnum::eNone; + })); + ON_CALL(*space, Release()).WillByDefault(Invoke([this]() { + mReleaseCount++; + + return ErrorEnum::eNone; + })); + ON_CALL(*space, Resize(_)).WillByDefault(Invoke([this](size_t size) -> Error { + mLastResizeSize = size; + + return mResizeError; + })); + + return {Move(space), ErrorEnum::eNone}; + })); + + ON_CALL(mOCISpec, LoadImageIndex(_, _)) + .WillByDefault(Invoke([this](const String& path, oci::ImageIndex& index) -> Error { + std::lock_guard lock {mOCIMutex}; + + auto it = mIndexes.find(path.CStr()); + if (it == mIndexes.end()) { + return ErrorEnum::eNotFound; + } + + index = *it->second; + + return ErrorEnum::eNone; + })); + ON_CALL(mOCISpec, LoadImageManifest(_, _)) + .WillByDefault(Invoke([this](const String& path, oci::ImageManifest& manifest) -> Error { + std::lock_guard lock {mOCIMutex}; + + auto it = mManifests.find(path.CStr()); + if (it == mManifests.end()) { + return ErrorEnum::eNotFound; + } + + manifest = *it->second; + + return ErrorEnum::eNone; + })); + ON_CALL(mOCISpec, LoadImageConfig(_, _)) + .WillByDefault(Invoke([this](const String& path, oci::ImageConfig& config) -> Error { + std::lock_guard lock {mOCIMutex}; + + auto it = mImageConfigs.find(path.CStr()); + if (it == mImageConfigs.end()) { + return ErrorEnum::eNotFound; + } + + config = *it->second; + + return ErrorEnum::eNone; + })); + + ON_CALL(mListener, OnItemStatusChanged(_)).WillByDefault(Invoke([this](const UpdateItemStatus& status) { + std::lock_guard lock {mStatusesMutex}; + + mStatuses.push_back(status); + })); + + ASSERT_TRUE(InitImageManager().IsNone()); + ASSERT_TRUE(mImageManager.SubscribeListener(mListener).IsNone()); + } + + // Registers manifest with image config, optional item config and layers. Blob IDs start from base. + TestManifest AddManifest(uint32_t base, size_t numLayers, size_t numDiffIDs, bool withItemConfig = false, + const std::vector& sharedLayers = {}) + { + TestManifest testManifest; + + testManifest.mManifest = MakeTestBlob(base); + testManifest.mImageConfig = MakeTestBlob(base + 1); + testManifest.mItemConfig = MakeTestBlob(base + 2); + + auto manifest = std::make_shared(); + + manifest->mMediaType = oci::cMediaTypeImageManifest; + manifest->mConfig = MakeDescriptor(testManifest.mImageConfig, cImageConfigType); + + if (withItemConfig) { + manifest->mItemConfig.SetValue(MakeDescriptor(testManifest.mItemConfig, cItemConfigType)); + } + + testManifest.mLayers = sharedLayers; + + for (size_t i = 0; i < numLayers; ++i) { + testManifest.mLayers.push_back(MakeTestBlob(base + 10 + i)); + } + + for (const auto& layer : testManifest.mLayers) { + EXPECT_TRUE(manifest->mLayers.PushBack(MakeDescriptor(layer, cTestLayerType)).IsNone()); + } + + auto imageConfig = std::make_shared(); + + for (size_t i = 0; i < numDiffIDs; ++i) { + testManifest.mDiffIDs.push_back(MakeTestBlob(base + 100 + i)); + + EXPECT_TRUE( + imageConfig->mRootfs.mDiffIDs.EmplaceBack(testManifest.mDiffIDs.back().mDigest.c_str()).IsNone()); + } + + std::lock_guard lock {mOCIMutex}; + + mManifests[testManifest.mManifest.mPath] = manifest; + mImageConfigs[testManifest.mImageConfig.mPath] = imageConfig; + + return testManifest; + } + + TestBlob AddIndex(uint32_t id, const std::vector& manifests) + { + auto blob = MakeTestBlob(id); + auto index = std::make_shared(); + + index->mMediaType = oci::cMediaTypeImageIndex; + + for (const auto& manifest : manifests) { + oci::IndexContentDescriptor descriptor; + + static_cast(descriptor) + = MakeDescriptor(manifest.mManifest, oci::cMediaTypeImageManifest); + + EXPECT_TRUE(index->mManifests.PushBack(descriptor).IsNone()); + } + + std::lock_guard lock {mOCIMutex}; + + mIndexes[blob.mPath] = index; + + return blob; + } + + void CreateValidBlobs(const TestManifest& manifest) + { + for (const auto* blob : {&manifest.mManifest, &manifest.mImageConfig, &manifest.mItemConfig}) { + CreateFile(blob->mPath.c_str(), blob->mHash.c_str()); + } + + for (const auto& layer : manifest.mLayers) { + CreateFile(layer.mPath.c_str(), layer.mHash.c_str()); + } + } + + static TestImageManager::InstallItemInfo MakeInstallInfo(const TestBlob& root, bool isRootIndex = false, + UpdateItemTypeEnum type = UpdateItemTypeEnum::eService, const char* itemID = cTestItemID, + const char* version = cTestVersion) + { + TestImageManager::InstallItemInfo itemInfo; + + itemInfo.mItemID = itemID; + itemInfo.mType = type; + itemInfo.mVersion = version; + itemInfo.mRootDigest = root.mDigest.c_str(); + itemInfo.mIsRootIndex = isRootIndex; + + return itemInfo; + } + + void StoreItem(const String& itemID, const String& version, const String& rootDigest, ItemStateEnum state) + { + auto item = CreateDeployableItem(itemID, version, state); + + item.mRootDigest = rootDigest; + + auto err = mStorage.AddDeployableItem(item); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + } + + std::optional GetStoredItem(const String& itemID, const String& version) + { + DeployableItemInfo item; + + if (!mStorage.GetDeployableItem(itemID, version, item).IsNone()) { + return std::nullopt; + } + + return item; + } + + std::vector GetNotifiedStatuses() + { + std::lock_guard lock {mStatusesMutex}; + + return mStatuses; + } + + std::vector GetNotifiedStates() + { + std::vector states; + + for (const auto& status : GetNotifiedStatuses()) { + states.push_back(status.mState.GetValue()); + } + + return states; + } + + std::optional GetReportedState(const String& itemID, const String& version) + { + auto statuses = std::make_unique>(); + + auto err = mImageManager.GetItemsStatuses(*statuses); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + const auto* status = FindStatus(*statuses, itemID, version); + if (status == nullptr) { + return std::nullopt; + } + + return status->mState.GetValue(); + } + + static ItemBlobInfo MakeBlobInfo(const TestBlob& blob, const char* mediaType, bool isLayer = false, + const std::string& diffID = "", size_t size = cInstallBlobSize) + { + ItemBlobInfo info; + + info.mDigest = blob.mDigest.c_str(); + info.mSize = size; + info.mMediaType = mediaType; + info.mDiffID = diffID.c_str(); + info.mIsLayer = isLayer; + + return info; + } + + std::atomic mAcceptCount {0}; + std::atomic mReleaseCount {0}; + std::atomic mLastResizeSize {0}; + Error mResizeError; + + std::mutex mOCIMutex; + std::map> mIndexes; + std::map> mManifests; + std::map> mImageConfigs; + + std::mutex mStatusesMutex; + std::vector mStatuses; +}; + +TEST_F(CommonImageManagerInstallTest, InstallNotifiesSubscribedListeners) +{ + const auto manifest = AddManifest(1, 1, 1); + + NiceMock otherListener; + + ASSERT_TRUE(mImageManager.SubscribeListener(otherListener).IsNone()); + + const auto expectedStates = std::vector( + {ItemStateEnum::eInstalling, ItemStateEnum::eDownloading, ItemStateEnum::eInstalled}); + + std::vector otherStates; + + EXPECT_CALL(otherListener, OnItemStatusChanged(_)).WillRepeatedly(Invoke([&](const UpdateItemStatus& status) { + otherStates.push_back(status.mState.GetValue()); + })); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), expectedStates); + EXPECT_EQ(otherStates, expectedStates); + + ASSERT_TRUE(mImageManager.UnsubscribeListener(otherListener).IsNone()); + + const auto otherManifest = AddManifest(2, 1, 1); + + err = mImageManager.Install(MakeInstallInfo(otherManifest.mManifest, false, UpdateItemTypeEnum::eService, "item2")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates().size(), 2 * expectedStates.size()); + EXPECT_EQ(otherStates, expectedStates); +} + +TEST_F(CommonImageManagerInstallTest, InstallManifestRoot) +{ + const auto manifest = AddManifest(1, 2, 2, true); + const auto itemInfo = MakeInstallInfo(manifest.mManifest); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(5); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(cInstallBlobSize)).Times(5); + EXPECT_CALL(mFSPlatform, Sync(String(cTestImagePath), true)).Times(1); + + auto err = mImageManager.Install(itemInfo); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), + std::vector( + {ItemStateEnum::eInstalling, ItemStateEnum::eDownloading, ItemStateEnum::eInstalled})); + + for (const auto& status : GetNotifiedStatuses()) { + EXPECT_EQ(status.mItemID, cTestItemID); + EXPECT_EQ(status.mVersion, cTestVersion); + EXPECT_EQ(status.mType, UpdateItemTypeEnum::eService); + EXPECT_TRUE(status.mError.IsNone()); + } + + EXPECT_EQ(mStorage.GetBlobs(cTestItemID, cTestVersion), + std::vector({ + MakeBlobInfo(manifest.mManifest, oci::cMediaTypeImageManifest, false, "", 0), + MakeBlobInfo(manifest.mItemConfig, cItemConfigType), + MakeBlobInfo(manifest.mImageConfig, cImageConfigType), + MakeBlobInfo(manifest.mLayers[0], cTestLayerType, true, manifest.mDiffIDs[0].mDigest), + MakeBlobInfo(manifest.mLayers[1], cTestLayerType, true, manifest.mDiffIDs[1].mDigest), + })); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); + EXPECT_EQ(item->mRootDigest, manifest.mManifest.mDigest.c_str()); + EXPECT_EQ(item->mType, UpdateItemTypeEnum::eService); + EXPECT_TRUE(item->mError.IsNone()); + + EXPECT_EQ(mAcceptCount.load(), 5); + EXPECT_EQ(mReleaseCount.load(), 0); + + for (const auto& layer : manifest.mLayers) { + EXPECT_TRUE(FileExists(layer.mPath.c_str())); + } + + EXPECT_EQ(GetReportedState(cTestItemID, cTestVersion), ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerInstallTest, InstallIndexRoot) +{ + const auto manifest = AddManifest(1, 1, 1); + const auto index = AddIndex(1000, {manifest}); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(4); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(cInstallBlobSize)).Times(4); + + auto err = mImageManager.Install(MakeInstallInfo(index, true)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), + std::vector( + {ItemStateEnum::eInstalling, ItemStateEnum::eDownloading, ItemStateEnum::eInstalled})); + + EXPECT_EQ(mStorage.GetBlobs(cTestItemID, cTestVersion), + std::vector({ + MakeBlobInfo(index, oci::cMediaTypeImageIndex, false, "", 0), + MakeBlobInfo(manifest.mManifest, oci::cMediaTypeImageManifest), + MakeBlobInfo(manifest.mImageConfig, cImageConfigType), + MakeBlobInfo(manifest.mLayers[0], cTestLayerType, true, manifest.mDiffIDs[0].mDigest), + })); + + EXPECT_TRUE(FileExists(index.mPath.c_str())); + EXPECT_TRUE(FileExists(manifest.mManifest.mPath.c_str())); + EXPECT_TRUE(FileExists(manifest.mImageConfig.mPath.c_str())); + EXPECT_TRUE(FileExists(manifest.mLayers[0].mPath.c_str())); + EXPECT_EQ(mAcceptCount.load(), 4); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerInstallTest, InstallAllBlobsValid) +{ + const auto manifest = AddManifest(1, 2, 2, true); + + CreateValidBlobs(manifest); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(_)).Times(0); + EXPECT_CALL(mSpaceAllocator, FreeSpace(_)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eInstalled})); + EXPECT_EQ(mStorage.GetBlobs(cTestItemID, cTestVersion).size(), 5); +} + +TEST_F(CommonImageManagerInstallTest, InstallInvalidLayer) +{ + const auto manifest = AddManifest(1, 2, 2); + + CreateValidBlobs(manifest); + CreateFile(manifest.mLayers[1].mPath.c_str(), cOtherHash); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mImageManager, DownloadBlob(String(manifest.mLayers[1].mPath.c_str()), _)).Times(1); + EXPECT_CALL(mSpaceAllocator, FreeSpace(cInstallBlobSize)).Times(1); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(cInstallBlobSize)).Times(1); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(ReadFile(manifest.mLayers[1].mPath), manifest.mLayers[1].mHash); + EXPECT_EQ(mAcceptCount.load(), 1); + EXPECT_EQ(GetNotifiedStates(), + std::vector( + {ItemStateEnum::eInstalling, ItemStateEnum::eDownloading, ItemStateEnum::eInstalled})); +} + +TEST_F(CommonImageManagerInstallTest, InstallInvalidMetadataBlob) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + CreateFile(manifest.mImageConfig.mPath.c_str(), cOtherHash); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mImageManager, DownloadBlob(String(manifest.mImageConfig.mPath.c_str()), _)).Times(1); + EXPECT_CALL(mSpaceAllocator, FreeSpace(cInstallBlobSize)).Times(1); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(cInstallBlobSize)).Times(1); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(ReadFile(manifest.mImageConfig.mPath), manifest.mImageConfig.mHash); + EXPECT_EQ(mAcceptCount.load(), 1); +} + +TEST_F(CommonImageManagerInstallTest, InstallServiceDiffIDs) +{ + const auto manifest = AddManifest(1, 2, 2); + + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(manifest.mLayers[1].mPath.c_str()).IsNone()); + + EXPECT_CALL(mOCISpec, LoadImageConfig(String(manifest.mImageConfig.mPath.c_str()), _)).Times(1); + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mImageManager, DownloadBlob(String(manifest.mLayers[1].mPath.c_str()), _)).Times(1); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + using LayerCall = TestImageManager::LayerCall; + + EXPECT_EQ(mImageManager.GetValidateLayerCalls(), + std::vector({{manifest.mLayers[0].mDigest, manifest.mDiffIDs[0].mDigest}, + {manifest.mLayers[1].mDigest, manifest.mDiffIDs[1].mDigest}})); +} + +TEST_F(CommonImageManagerInstallTest, InstallLayerValid) +{ + const auto manifest = AddManifest(1, 1, 1); + const auto& layer = manifest.mLayers[0]; + + // The layer blob is removed after the layer is unpacked. + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(layer.mPath.c_str()).IsNone()); + + EXPECT_CALL(mImageManager, CheckLayer(String(layer.mPath.c_str()), _, String(manifest.mDiffIDs[0].mDigest.c_str()))) + .WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mImageManager, RemoveLayer(_, _, _)).Times(0); + EXPECT_CALL(mImageManager, UnpackLayer(_, _, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(FileExists(layer.mPath.c_str())); +} + +TEST_F(CommonImageManagerInstallTest, InstallLayerNotSupported) +{ + const auto manifest = AddManifest(1, 2, 2); + + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(manifest.mLayers[1].mPath.c_str()).IsNone()); + + EXPECT_CALL(mImageManager, CheckLayer(_, _, _)).Times(2); + EXPECT_CALL(mImageManager, DownloadBlob(String(manifest.mLayers[1].mPath.c_str()), _)).Times(1); + EXPECT_CALL(mImageManager, RemoveLayer(_, _, _)).Times(0); + EXPECT_CALL(mImageManager, UnpackLayer(_, _, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerInstallTest, InstallLayerInvalid) +{ + const auto manifest = AddManifest(1, 1, 1); + const auto& layer = manifest.mLayers[0]; + const auto diffID = String(manifest.mDiffIDs[0].mDigest.c_str()); + + constexpr size_t cUnpackedSize = 100; + + CreateValidBlobs(manifest); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + + { + InSequence seq; + + EXPECT_CALL(mImageManager, CheckLayer(String(layer.mPath.c_str()), _, diffID)) + .WillOnce(Return(ErrorEnum::eInvalidChecksum)); + EXPECT_CALL(mImageManager, RemoveLayer(String(layer.mPath.c_str()), _, diffID)) + .WillOnce(Return(RetWithError(cUnpackedSize))); + EXPECT_CALL(mSpaceAllocator, FreeSpace(cUnpackedSize)); + EXPECT_CALL(mImageManager, UnpackLayer(String(layer.mPath.c_str()), _, diffID)) + .WillOnce(Return(ErrorEnum::eNone)); + } + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerInstallTest, InstallLayerMissing) +{ + const auto manifest = AddManifest(1, 1, 1); + const auto& layer = manifest.mLayers[0]; + + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(layer.mPath.c_str()).IsNone()); + + { + InSequence seq; + + EXPECT_CALL(mImageManager, CheckLayer(String(layer.mPath.c_str()), _, _)) + .WillOnce(Return(ErrorEnum::eNotFound)); + EXPECT_CALL(mImageManager, RemoveLayer(String(layer.mPath.c_str()), _, _)); + EXPECT_CALL(mImageManager, DownloadBlob(String(layer.mPath.c_str()), _)); + EXPECT_CALL(mImageManager, UnpackLayer(String(layer.mPath.c_str()), _, _)).WillOnce(Return(ErrorEnum::eNone)); + } + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerInstallTest, InstallRemoveLayerFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + EXPECT_CALL(mImageManager, CheckLayer(_, _, _)).WillOnce(Return(ErrorEnum::eNotFound)); + EXPECT_CALL(mImageManager, RemoveLayer(_, _, _)) + .WillOnce(Return(RetWithError(0, Error(ErrorEnum::eFailed, "remove failed")))); + EXPECT_CALL(mImageManager, UnpackLayer(_, _, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_STREQ(item->mError.Message(), "remove failed"); +} + +TEST_F(CommonImageManagerInstallTest, InstallUnpackLayerFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + EXPECT_CALL(mImageManager, CheckLayer(_, _, _)).WillOnce(Return(ErrorEnum::eNotFound)); + EXPECT_CALL(mImageManager, UnpackLayer(_, _, _)).WillOnce(Return(Error(ErrorEnum::eFailed, "unpack failed"))); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_STREQ(item->mError.Message(), "unpack failed"); +} + +TEST_F(CommonImageManagerInstallTest, InstallWithoutImageConfig) +{ + const auto manifest = AddManifest(1, 2, 0); + + mManifests[manifest.mManifest.mPath]->mConfig = oci::ContentDescriptor {}; + + EXPECT_CALL(mOCISpec, LoadImageConfig(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(FileExists(manifest.mImageConfig.mPath.c_str())); + + using LayerCall = TestImageManager::LayerCall; + + const std::vector expectedCalls {{manifest.mLayers[0].mDigest, ""}, {manifest.mLayers[1].mDigest, ""}}; + + EXPECT_EQ(mImageManager.GetValidateLayerCalls(), expectedCalls); + + EXPECT_EQ(mStorage.GetBlobs(cTestItemID, cTestVersion), + std::vector({ + MakeBlobInfo(manifest.mManifest, oci::cMediaTypeImageManifest, false, "", 0), + MakeBlobInfo(manifest.mLayers[0], cTestLayerType, true), + MakeBlobInfo(manifest.mLayers[1], cTestLayerType, true), + })); +} + +TEST_F(CommonImageManagerInstallTest, InstallEmptyConfig) +{ + const auto manifest = AddManifest(1, 1, 0); + + CreateValidBlobs(manifest); + + // A component has an empty config and layers without diff IDs. + mManifests[manifest.mManifest.mPath]->mConfig.mMediaType = oci::cMediaTypeEmptyBlob; + + EXPECT_CALL(mOCISpec, LoadImageConfig(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + const auto calls = mImageManager.GetValidateLayerCalls(); + ASSERT_EQ(calls.size(), 1); + EXPECT_TRUE(calls[0].mDiffDigest.empty()); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerInstallTest, InstallConfigWithoutDiffIDs) +{ + // A component may have an image config without rootfs, so its layers have no diff IDs. + const auto manifest = AddManifest(1, 1, 0); + + CreateValidBlobs(manifest); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + const auto calls = mImageManager.GetValidateLayerCalls(); + ASSERT_EQ(calls.size(), 1); + EXPECT_TRUE(calls[0].mDiffDigest.empty()); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerInstallTest, InstallMissingDiffID) +{ + const auto manifest = AddManifest(1, 2, 1); + + CreateValidBlobs(manifest); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); + + EXPECT_TRUE(mImageManager.GetValidateLayerCalls().empty()); + + auto statuses = GetNotifiedStatuses(); + ASSERT_EQ(statuses.size(), 2); + EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalling); + EXPECT_EQ(statuses[1].mState, ItemStateEnum::eFailed); + EXPECT_TRUE(statuses[1].mError.Is(ErrorEnum::eInvalidArgument)); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eInvalidArgument)); +} + +TEST_F(CommonImageManagerInstallTest, InstallRootDigestMismatch) +{ + const auto manifest = AddManifest(1, 1, 1); + + StoreItem(cTestItemID, cTestVersion, cOtherDigest, ItemStateEnum::eInstalled); + + const auto storedItems = mStorage.GetItems(); + + EXPECT_CALL(mListener, OnItemStatusChanged(_)).Times(0); + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mFSPlatform, Sync(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mStorage.GetItems(), storedItems); + EXPECT_TRUE(mStorage.GetBlobs(cTestItemID, cTestVersion).empty()); +} + +TEST_F(CommonImageManagerInstallTest, InstallMalformedRootDigest) +{ + auto itemInfo = MakeInstallInfo(MakeTestBlob(1)); + + itemInfo.mRootDigest = "sha256:../malformed"; + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(_)).Times(0); + + auto err = mImageManager.Install(itemInfo); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); + + auto statuses = GetNotifiedStatuses(); + ASSERT_EQ(statuses.size(), 2); + EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalling); + EXPECT_EQ(statuses[1].mState, ItemStateEnum::eFailed); + EXPECT_TRUE(statuses[1].mError.Is(ErrorEnum::eInvalidArgument)); + + EXPECT_TRUE(mStorage.GetBlobs(cTestItemID, cTestVersion).empty()); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eInvalidArgument)); +} + +TEST_F(CommonImageManagerInstallTest, InstallMalformedLayerDigest) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + constexpr auto cMalformedDigest = "sha256:../malformed"; + + mManifests[manifest.mManifest.mPath]->mLayers[0].mDigest = cMalformedDigest; + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(_)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); + + EXPECT_TRUE(mImageManager.GetValidateLayerCalls().empty()); + + auto statuses = GetNotifiedStatuses(); + ASSERT_EQ(statuses.size(), 2); + EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalling); + EXPECT_EQ(statuses[1].mState, ItemStateEnum::eFailed); + EXPECT_TRUE(statuses[1].mError.Is(ErrorEnum::eInvalidArgument)); + + // No blob reference is stored for the layer with the malformed digest. + EXPECT_EQ(mStorage.GetBlobs(cTestItemID, cTestVersion), + std::vector({ + MakeBlobInfo(manifest.mManifest, oci::cMediaTypeImageManifest, false, "", 0), + MakeBlobInfo(manifest.mImageConfig, cImageConfigType), + })); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eInvalidArgument)); +} + +TEST_F(CommonImageManagerInstallTest, InstallUnsupportedDigestAlgorithm) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + mManifests[manifest.mManifest.mPath]->mLayers[0].mDigest = "md5:0123456789abcdef0123456789abcdef"; + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eNotSupported)) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eNotSupported)); +} + +TEST_F(CommonImageManagerInstallTest, InstallOversizedDigestAlgorithm) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + const auto digest = std::string(crypto::cAlgLen + 1, 'a') + ":" + std::string(64, '0'); + + mManifests[manifest.mManifest.mPath]->mLayers[0].mDigest = digest.c_str(); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eNotSupported)) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eNotSupported)); +} + +TEST_F(CommonImageManagerInstallTest, InstallBlobIntegrityError) +{ + const auto manifest = AddManifest(1, 1, 1); + const auto& layer = manifest.mLayers[0]; + + CreateValidBlobs(manifest); + + EXPECT_CALL(mFileInfoProvider, GetFileInfo(_, _, _)).WillRepeatedly(Invoke(GetFileInfoFromContent)); + EXPECT_CALL(mFileInfoProvider, GetFileInfo(String(layer.mPath.c_str()), _, _)) + .WillOnce(Return(Error(ErrorEnum::eIntegrity))) + .WillRepeatedly(Invoke(GetFileInfoFromContent)); + + // The corrupted blob is downloaded again. + EXPECT_CALL(mImageManager, DownloadBlob(String(layer.mPath.c_str()), _)).WillOnce(Invoke(DownloadTestBlob)); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerInstallTest, InstallBlobReadFails) +{ + const auto manifest = AddManifest(1, 1, 1); + const auto& layer = manifest.mLayers[0]; + + CreateValidBlobs(manifest); + + ON_CALL(mFileInfoProvider, GetFileInfo(String(layer.mPath.c_str()), _, _)) + .WillByDefault(Return(Error(ErrorEnum::eFailed))); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + // The read error doesn't mean the blob is corrupted, so it is kept. + EXPECT_TRUE(FileExists(layer.mPath.c_str())); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eFailed)); +} + +TEST_F(CommonImageManagerInstallTest, InstallMalformedDigestHash) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + // The digest format is valid, but the hash is not a hex string. + mManifests[manifest.mManifest.mPath]->mLayers[0].mDigest = "sha256:zz"; + + EXPECT_CALL(mBlobInfoProvider, GetBlobInfo(_, _)).Times(0); + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerInstallTest, InstallDownloadedBlobSizeMismatch) +{ + constexpr auto cContent = "short"; + + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(manifest.mLayers[0].mPath.c_str()).IsNone()); + + EXPECT_CALL(mImageManager, DownloadBlob(String(manifest.mLayers[0].mPath.c_str()), _)) + .WillOnce( + Invoke([](const String& path, const BlobInfo&) { return fs::WriteStringToFile(path, cContent, 0600); })); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidChecksum)) << tests::utils::ErrorToStr(err); + + // The invalid blob is kept, so the accepted space matches its size. + EXPECT_TRUE(FileExists(manifest.mLayers[0].mPath.c_str())); + EXPECT_EQ(mLastResizeSize.load(), strlen(cContent)); + EXPECT_EQ(mAcceptCount.load(), 1); + EXPECT_EQ(mReleaseCount.load(), 0); +} + +TEST_F(CommonImageManagerInstallTest, InstallResizeSpaceFails) +{ + const auto manifest = AddManifest(1, 1, 1); + const auto& layer = manifest.mLayers[0]; + + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(layer.mPath.c_str()).IsNone()); + + mResizeError = ErrorEnum::eNoMemory; + + EXPECT_CALL(mImageManager, DownloadBlob(String(layer.mPath.c_str()), _)).WillOnce(Invoke(DownloadTestBlob)); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eNoMemory)) << tests::utils::ErrorToStr(err); + + // The sizing failure is handled as a failed download. + EXPECT_FALSE(FileExists(layer.mPath.c_str())); + EXPECT_EQ(mAcceptCount.load(), 0); + EXPECT_EQ(mReleaseCount.load(), 1); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eNoMemory)); +} + +TEST_F(CommonImageManagerInstallTest, InstallRemovePartialBlobFails) +{ + if (geteuid() == 0) { + GTEST_SKIP() << "Root ignores the folder permissions"; + } + + constexpr auto cContent = "partial"; + + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + const auto& path = manifest.mLayers[0].mPath; + const auto dir = fs::Dir(path.c_str()); + + ASSERT_TRUE(fs::RemoveAll(path.c_str()).IsNone()); + + // The partial file can't be removed from the read-only folder. + EXPECT_CALL(mImageManager, DownloadBlob(String(path.c_str()), _)) + .WillOnce(Invoke([&](const String& blobPath, const BlobInfo&) -> Error { + EXPECT_TRUE(fs::WriteStringToFile(blobPath, cContent, 0600).IsNone()); + EXPECT_EQ(chmod(dir.CStr(), 0500), 0); + + return ErrorEnum::eFailed; + })); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + + EXPECT_EQ(chmod(dir.CStr(), 0700), 0); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + // The partial file stays, so its space stays used and is freed once when the blob is processed again. + EXPECT_TRUE(FileExists(path.c_str())); + EXPECT_EQ(mLastResizeSize.load(), strlen(cContent)); + EXPECT_EQ(mAcceptCount.load(), 1); + EXPECT_EQ(mReleaseCount.load(), 0); +} + +TEST_F(CommonImageManagerInstallTest, InstallDownloadedBlobInvalid) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(manifest.mLayers[0].mPath.c_str()).IsNone()); + + EXPECT_CALL(mImageManager, DownloadBlob(String(manifest.mLayers[0].mPath.c_str()), _)) + .WillOnce( + Invoke([](const String& path, const BlobInfo&) { return fs::WriteStringToFile(path, cOtherHash, 0600); })); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidChecksum)) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eInvalidChecksum)); +} + +TEST_F(CommonImageManagerInstallTest, InstallCorruptManifest) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mOCISpec, LoadImageManifest(String(manifest.mManifest.mPath.c_str()), _)) + .WillOnce(Return(Error(ErrorEnum::eFailed, "corrupt manifest"))); + EXPECT_CALL(mFSPlatform, Sync(String(cTestImagePath), true)).Times(1); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + auto statuses = GetNotifiedStatuses(); + ASSERT_EQ(statuses.size(), 2); + EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalling); + EXPECT_EQ(statuses[1].mState, ItemStateEnum::eFailed); + EXPECT_TRUE(statuses[1].mError.Is(ErrorEnum::eFailed)); + EXPECT_STREQ(statuses[1].mError.Message(), "corrupt manifest"); + + EXPECT_EQ(mStorage.GetBlobs(cTestItemID, cTestVersion), + std::vector({MakeBlobInfo(manifest.mManifest, oci::cMediaTypeImageManifest, false, "", 0)})); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eFailed)); + EXPECT_STREQ(item->mError.Message(), "corrupt manifest"); +} + +TEST_F(CommonImageManagerInstallTest, InstallAllocateSpaceFails) +{ + const auto manifest = AddManifest(1, 2, 2); + + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(manifest.mLayers[1].mPath.c_str()).IsNone()); + + EXPECT_CALL(mSpaceAllocator, AllocateSpace(cInstallBlobSize)) + .WillOnce(Invoke([](size_t) -> RetWithError> { + return {nullptr, ErrorEnum::eNoMemory}; + })); + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + EXPECT_CALL(mFSPlatform, Sync(String(cTestImagePath), true)).Times(1); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eNoMemory)) << tests::utils::ErrorToStr(err); + + auto statuses = GetNotifiedStatuses(); + ASSERT_EQ(statuses.size(), 2); + EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalling); + EXPECT_EQ(statuses[1].mState, ItemStateEnum::eFailed); + EXPECT_TRUE(statuses[1].mError.Is(ErrorEnum::eNoMemory)); + + EXPECT_FALSE(FileExists(manifest.mLayers[1].mPath.c_str())); + + auto blobs = mStorage.GetBlobs(cTestItemID, cTestVersion); + ASSERT_EQ(blobs.size(), 4); + EXPECT_EQ(blobs.back(), MakeBlobInfo(manifest.mLayers[1], cTestLayerType, true, manifest.mDiffIDs[1].mDigest)); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eNoMemory)); +} + +TEST_F(CommonImageManagerInstallTest, InstallRemovedItemRestoresOutdatedItem) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + StoreItem(cTestItemID, cTestVersion, manifest.mManifest.mDigest.c_str(), ItemStateEnum::eRemoved); + + EXPECT_CALL(mSpaceAllocator, RestoreOutdatedItem(String(cTestItemID), String(cTestVersion))) + .WillOnce(Return(ErrorEnum::eNotFound)); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerInstallTest, InstallRemovedItemSetInstallingStateFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + StoreItem(cTestItemID, cTestVersion, manifest.mManifest.mDigest.c_str(), ItemStateEnum::eRemoved); + + mStorage.SetFailUpdateStates({ItemStateEnum::eInstalling}); + + // The item stays in the eRemoved state, so it stays outdated. + EXPECT_CALL(mSpaceAllocator, RestoreOutdatedItem(_, _)).Times(0); + EXPECT_CALL(mListener, OnItemStatusChanged(_)).Times(0); + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eRemoved); +} + +TEST_F(CommonImageManagerInstallTest, InstallRemovedItemRestoreFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + StoreItem(cTestItemID, cTestVersion, manifest.mManifest.mDigest.c_str(), ItemStateEnum::eRemoved); + + EXPECT_CALL(mSpaceAllocator, RestoreOutdatedItem(String(cTestItemID), String(cTestVersion))) + .WillOnce(Return(ErrorEnum::eFailed)); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eInstalled})); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerInstallTest, InstallStoredItemDoesNotRestoreOutdatedItem) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + StoreItem(cTestItemID, cTestVersion, manifest.mManifest.mDigest.c_str(), ItemStateEnum::eFailed); + + EXPECT_CALL(mSpaceAllocator, RestoreOutdatedItem(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); + EXPECT_TRUE(item->mError.IsNone()); +} + +TEST_F(CommonImageManagerInstallTest, InstallStorageFullRemovesOutdatedItem) +{ + const auto manifest = AddManifest(1, 1, 1); + + for (size_t i = 0; i < cMaxNumStoredItems - 1; ++i) { + AddStoredItem(("item" + std::to_string(i + 100)).c_str(), cTestVersion, ItemStateEnum::eInstalled); + } + + AddStoredItem("outdated", cTestVersion, ItemStateEnum::eRemoved); + + EXPECT_CALL(mSpaceAllocator, RemoveOutdatedItem()).WillOnce(Invoke([this]() { + return mImageManager.GetItemRemover().RemoveItem("outdated", cTestVersion); + })); + EXPECT_CALL(mListener, OnItemRemovedFromCache(String("outdated"), String(cTestVersion))); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mStorage.GetItems().size(), cMaxNumStoredItems); + EXPECT_FALSE(GetStoredItem("outdated", cTestVersion).has_value()); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerInstallTest, InstallStorageFullOutdatedItemSkipped) +{ + const auto manifest = AddManifest(1, 1, 1); + + for (size_t i = 0; i < cMaxNumStoredItems - 1; ++i) { + AddStoredItem(("item" + std::to_string(i + 100)).c_str(), cTestVersion, ItemStateEnum::eInstalled); + } + + AddStoredItem("outdated", cTestVersion, ItemStateEnum::eRemoved); + + // The first outdated item is skipped by the item remover, so the next one is removed. + EXPECT_CALL(mSpaceAllocator, RemoveOutdatedItem()) + .WillOnce(Return(RetWithError(0, ErrorEnum::eNone))) + .WillOnce(Invoke([this]() { return mImageManager.GetItemRemover().RemoveItem("outdated", cTestVersion); })); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mStorage.GetItems().size(), cMaxNumStoredItems); + EXPECT_FALSE(GetStoredItem("outdated", cTestVersion).has_value()); +} + +TEST_F(CommonImageManagerInstallTest, InstallStorageFullNoOutdatedItem) +{ + const auto manifest = AddManifest(1, 1, 1); + + for (size_t i = 0; i < cMaxNumStoredItems; ++i) { + AddStoredItem(("item" + std::to_string(i + 100)).c_str(), cTestVersion, ItemStateEnum::eInstalled); + } + + const auto storedItems = mStorage.GetItems(); + + EXPECT_CALL(mSpaceAllocator, RemoveOutdatedItem()).WillOnce(Return(RetWithError(0, ErrorEnum::eNotFound))); + EXPECT_CALL(mListener, OnItemStatusChanged(_)).Times(0); + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eNoMemory)) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mStorage.GetItems(), storedItems); +} + +TEST_F(CommonImageManagerInstallTest, InstallStorageFullRemoveOutdatedItemFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + for (size_t i = 0; i < cMaxNumStoredItems; ++i) { + AddStoredItem(("item" + std::to_string(i + 100)).c_str(), cTestVersion, ItemStateEnum::eRemoved); + } + + const auto storedItems = mStorage.GetItems(); + + EXPECT_CALL(mSpaceAllocator, RemoveOutdatedItem()).WillOnce(Return(RetWithError(0, ErrorEnum::eFailed))); + EXPECT_CALL(mListener, OnItemStatusChanged(_)).Times(0); + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mStorage.GetItems(), storedItems); +} + +TEST_F(CommonImageManagerInstallTest, InstallStoredItemStorageFull) +{ + const auto manifest = AddManifest(1, 1, 1); + + StoreItem(cTestItemID, cTestVersion, manifest.mManifest.mDigest.c_str(), ItemStateEnum::eFailed); + + for (size_t i = 0; i < cMaxNumStoredItems - 1; ++i) { + AddStoredItem(("item" + std::to_string(i + 100)).c_str(), cTestVersion, ItemStateEnum::eInstalled); + } + + // The stored item already has its storage record. + EXPECT_CALL(mSpaceAllocator, RemoveOutdatedItem()).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerInstallTest, InstallBlobInfoNotFound) +{ + const auto manifest = AddManifest(1, 1, 1); + + // The provider doesn't have the blob: it fails without retrying. + EXPECT_CALL(mBlobInfoProvider, GetBlobInfo(_, _)).WillOnce(Return(Error(ErrorEnum::eNotFound))); + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).Times(0); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eNotFound)); +} + +TEST_F(CommonImageManagerInstallTest, InstallBlobInfoRetried) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(manifest.mLayers[0].mPath.c_str()).IsNone()); + + // The other errors mean that the provider is not available for a while. + EXPECT_CALL(mBlobInfoProvider, GetBlobInfo(_, _)) + .WillOnce(Return(Error(ErrorEnum::eFailed))) + .WillOnce(Invoke(GetTestBlobInfo)); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerInstallTest, InstallDownloadFails) +{ + const auto manifest = AddManifest(1, 2, 2); + + CreateValidBlobs(manifest); + ASSERT_TRUE(fs::RemoveAll(manifest.mLayers[0].mPath.c_str()).IsNone()); + ASSERT_TRUE(fs::RemoveAll(manifest.mLayers[1].mPath.c_str()).IsNone()); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).WillRepeatedly(Invoke(DownloadTestBlob)); + EXPECT_CALL(mImageManager, DownloadBlob(String(manifest.mLayers[1].mPath.c_str()), _)) + .WillOnce(Invoke([](const String& path, const BlobInfo&) { + EXPECT_TRUE(fs::WriteStringToFile(path, "partial", 0600).IsNone()); + + return Error(ErrorEnum::eFailed, "download failed"); + })); + EXPECT_CALL(mFSPlatform, Sync(String(cTestImagePath), true)).Times(1); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + auto statuses = GetNotifiedStatuses(); + ASSERT_EQ(statuses.size(), 3); + EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalling); + EXPECT_EQ(statuses[1].mState, ItemStateEnum::eDownloading); + EXPECT_EQ(statuses[2].mState, ItemStateEnum::eFailed); + EXPECT_TRUE(statuses[2].mError.Is(ErrorEnum::eFailed)); + EXPECT_STREQ(statuses[2].mError.Message(), "download failed"); + + EXPECT_TRUE(FileExists(manifest.mLayers[0].mPath.c_str())); + EXPECT_FALSE(FileExists(manifest.mLayers[1].mPath.c_str())); + EXPECT_EQ(mAcceptCount.load(), 1); + EXPECT_EQ(mReleaseCount.load(), 1); + + auto blobs = mStorage.GetBlobs(cTestItemID, cTestVersion); + ASSERT_EQ(blobs.size(), 4); + EXPECT_EQ(blobs.back(), MakeBlobInfo(manifest.mLayers[1], cTestLayerType, true, manifest.mDiffIDs[1].mDigest)); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eFailed)); + EXPECT_STREQ(item->mError.Message(), "download failed"); + + EXPECT_EQ(GetReportedState(cTestItemID, cTestVersion), ItemStateEnum::eFailed); +} + +TEST_F(CommonImageManagerInstallTest, InstallMetadataDownloadFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + EXPECT_CALL(mImageManager, DownloadBlob(String(manifest.mManifest.mPath.c_str()), _)) + .WillOnce(Return(ErrorEnum::eFailed)); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(cInstallBlobSize)).Times(1); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(FileExists(manifest.mManifest.mPath.c_str())); + EXPECT_EQ(mReleaseCount.load(), 1); + EXPECT_EQ(mStorage.GetBlobs(cTestItemID, cTestVersion), + std::vector({MakeBlobInfo(manifest.mManifest, oci::cMediaTypeImageManifest, false, "", 0)})); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); +} + +TEST_F(CommonImageManagerInstallTest, InstallSyncFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + EXPECT_CALL(mFSPlatform, Sync(String(cTestImagePath), true)).WillOnce(Return(ErrorEnum::eFailed)); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eFailed})); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eFailed)); +} + +TEST_F(CommonImageManagerInstallTest, InstallSetInstalledStateFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + mStorage.SetFailUpdateStates({ItemStateEnum::eInstalled}); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eFailed})); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_STREQ(item->mError.Message(), "update failed"); +} + +TEST_F(CommonImageManagerInstallTest, InstallSetFailedStateFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + // Both the eInstalled and the eFailed state updates fail. The blobs are downloaded, so eDownloading is notified. + mStorage.SetFailUpdateStates({ItemStateEnum::eInstalled, ItemStateEnum::eFailed}); + + std::optional reportedOnFailed; + + EXPECT_CALL(mListener, OnItemStatusChanged(_)).WillRepeatedly(Invoke([&](const UpdateItemStatus& status) { + { + std::lock_guard lock {mStatusesMutex}; + + mStatuses.push_back(status); + } + + if (status.mState == ItemStateEnum::eFailed) { + reportedOnFailed = GetReportedState(cTestItemID, cTestVersion); + } + })); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + auto statuses = GetNotifiedStatuses(); + ASSERT_EQ(statuses.size(), 3); + EXPECT_EQ(statuses[0].mState, ItemStateEnum::eInstalling); + EXPECT_EQ(statuses[1].mState, ItemStateEnum::eDownloading); + EXPECT_EQ(statuses[2].mState, ItemStateEnum::eFailed); + EXPECT_STREQ(statuses[2].mError.Message(), "update failed"); + + // While the final eFailed state is notified, the notified eFailed state is reported. + EXPECT_EQ(reportedOnFailed, ItemStateEnum::eFailed); + + // The item is left in eInstalling state, and it is reported as eFailed as it is not being installed. + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalling); + EXPECT_EQ(GetReportedState(cTestItemID, cTestVersion), ItemStateEnum::eFailed); +} + +TEST_F(CommonImageManagerInstallTest, InstallAfterSetFailedStateFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + mStorage.SetFailUpdateStates({ItemStateEnum::eInstalled, ItemStateEnum::eFailed}); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + mStorage.SetFailUpdateStates({}); + + err = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + // The stored state is already eInstalling, but the listeners were notified of eFailed, so the retry is notified. + EXPECT_EQ(GetNotifiedStates(), + std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eDownloading, ItemStateEnum::eFailed, + ItemStateEnum::eInstalling, ItemStateEnum::eInstalled})); +} + +TEST_F(CommonImageManagerInstallTest, GetItemsStatusesDuringInstall) +{ + const auto manifest = AddManifest(1, 1, 1); + + std::promise downloadStarted; + std::promise downloadReleased; + auto downloadReleasedFuture = downloadReleased.get_future(); + std::atomic installingChecked {false}; + + EXPECT_CALL(mListener, OnItemStatusChanged(_)).WillRepeatedly(Invoke([&](const UpdateItemStatus& status) { + if (status.mState == ItemStateEnum::eInstalling) { + EXPECT_EQ(GetReportedState(cTestItemID, cTestVersion), ItemStateEnum::eInstalling); + + installingChecked = true; + } + })); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)) + .WillOnce(Invoke([&](const String& path, const BlobInfo& blobInfo) { + downloadStarted.set_value(); + downloadReleasedFuture.wait(); + + return DownloadTestBlob(path, blobInfo); + })) + .WillRepeatedly(Invoke(DownloadTestBlob)); + + Error installErr; + + std::thread installThread([&]() { installErr = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); }); + + downloadStarted.get_future().wait(); + + EXPECT_TRUE(installingChecked.load()); + EXPECT_EQ(GetReportedState(cTestItemID, cTestVersion), ItemStateEnum::eDownloading); + + downloadReleased.set_value(); + installThread.join(); + + ASSERT_TRUE(installErr.IsNone()) << tests::utils::ErrorToStr(installErr); + EXPECT_EQ(GetReportedState(cTestItemID, cTestVersion), ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerInstallTest, GetItemsStatusesDuringStoredItemInstall) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + + auto storedItem = CreateDeployableItem(cTestItemID, cTestVersion, ItemStateEnum::eFailed); + + storedItem.mType = UpdateItemTypeEnum::eComponent; + storedItem.mRootDigest = manifest.mManifest.mDigest.c_str(); + + ASSERT_TRUE(mStorage.AddDeployableItem(storedItem).IsNone()); + + std::optional reportedOnInstalling; + + EXPECT_CALL(mListener, OnItemStatusChanged(_)).WillRepeatedly(Invoke([&](const UpdateItemStatus& status) { + EXPECT_EQ(status.mType, UpdateItemTypeEnum::eComponent); + + if (status.mState != ItemStateEnum::eInstalling) { + return; + } + + auto statuses = std::make_unique>(); + + EXPECT_TRUE(mImageManager.GetItemsStatuses(*statuses).IsNone()); + + if (const auto* reported = FindStatus(*statuses, cTestItemID, cTestVersion); reported != nullptr) { + reportedOnInstalling = *reported; + } + })); + + auto err = mImageManager.Install(MakeInstallInfo(manifest.mManifest, false, UpdateItemTypeEnum::eComponent)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_TRUE(reportedOnInstalling.has_value()); + EXPECT_EQ(reportedOnInstalling->mState, ItemStateEnum::eInstalling); + EXPECT_EQ(reportedOnInstalling->mType, UpdateItemTypeEnum::eComponent); +} + +TEST_F(CommonImageManagerInstallTest, InstallSameVersionConcurrently) +{ + const auto manifest = AddManifest(1, 1, 1); + + std::promise downloadStarted; + std::promise downloadReleased; + auto downloadReleasedFuture = downloadReleased.get_future(); + std::atomic downloadCount {0}; + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)) + .WillRepeatedly(Invoke([&](const String& path, const BlobInfo& blobInfo) { + if (downloadCount++ == 0) { + downloadStarted.set_value(); + downloadReleasedFuture.wait(); + } + + return DownloadTestBlob(path, blobInfo); + })); + + Error firstErr, secondErr; + + std::thread firstThread([&]() { firstErr = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); }); + + downloadStarted.get_future().wait(); + + std::thread secondThread([&]() { secondErr = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); }); + + // Gives the second install time to reach LockItem: without waiting there, it would notify eInstalling and fail + // the checks below. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + EXPECT_EQ( + GetNotifiedStates(), std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eDownloading})); + EXPECT_EQ(downloadCount.load(), 1); + + downloadReleased.set_value(); + + firstThread.join(); + secondThread.join(); + + ASSERT_TRUE(firstErr.IsNone()) << tests::utils::ErrorToStr(firstErr); + ASSERT_TRUE(secondErr.IsNone()) << tests::utils::ErrorToStr(secondErr); + + // The second install starts after the first one is finished, including its final notification, and finds all + // blobs valid. + EXPECT_EQ(GetNotifiedStates(), + std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eDownloading, ItemStateEnum::eInstalled, + ItemStateEnum::eInstalling, ItemStateEnum::eInstalled})); + EXPECT_EQ(downloadCount.load(), 3); +} + +TEST_F(CommonImageManagerInstallTest, InstallSharedBlobConcurrently) +{ + const auto sharedLayer = MakeTestBlob(500); + const auto manifestA = AddManifest(1, 0, 1, false, {sharedLayer}); + const auto manifestB = AddManifest(200, 0, 1, false, {sharedLayer}); + + CreateValidBlobs(manifestA); + CreateValidBlobs(manifestB); + ASSERT_TRUE(fs::RemoveAll(sharedLayer.mPath.c_str()).IsNone()); + + std::promise downloadStarted; + std::promise downloadReleased; + auto downloadReleasedFuture = downloadReleased.get_future(); + std::atomic sharedDownloadCount {0}; + + const auto countSharedLayerCalls = [&sharedLayer](const std::vector& calls) { + return std::count_if(calls.begin(), calls.end(), + [&sharedLayer](const TestImageManager::LayerCall& call) { return call.mDigest == sharedLayer.mDigest; }); + }; + + EXPECT_CALL(mImageManager, DownloadBlob(String(sharedLayer.mPath.c_str()), _)) + .WillOnce(Invoke([&](const String& path, const BlobInfo& blobInfo) { + sharedDownloadCount++; + downloadStarted.set_value(); + downloadReleasedFuture.wait(); + + return DownloadTestBlob(path, blobInfo); + })); + + Error errA, errB; + + std::thread threadA([&]() { + errA + = mImageManager.Install(MakeInstallInfo(manifestA.mManifest, false, UpdateItemTypeEnum::eService, "itemA")); + }); + + downloadStarted.get_future().wait(); + + std::thread threadB([&]() { + errB + = mImageManager.Install(MakeInstallInfo(manifestB.mManifest, false, UpdateItemTypeEnum::eService, "itemB")); + }); + + // Gives item B time to reach LockBlob of the shared layer: without waiting there, it would validate and download + // the shared layer a second time and fail the checks below. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + EXPECT_EQ(sharedDownloadCount.load(), 1); + + downloadReleased.set_value(); + + threadA.join(); + threadB.join(); + + ASSERT_TRUE(errA.IsNone()) << tests::utils::ErrorToStr(errA); + ASSERT_TRUE(errB.IsNone()) << tests::utils::ErrorToStr(errB); + + // Item B finds the shared layer downloaded by item A valid. + EXPECT_EQ(countSharedLayerCalls(mImageManager.GetValidateLayerCalls()), 2); + EXPECT_EQ(sharedDownloadCount.load(), 1); + + for (const auto& itemID : {"itemA", "itemB"}) { + auto blobs = mStorage.GetBlobs(itemID, cTestVersion); + ASSERT_EQ(blobs.size(), 3); + EXPECT_EQ(blobs.back().mDigest, sharedLayer.mDigest.c_str()); + + auto item = GetStoredItem(itemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); + } + + EXPECT_EQ(ReadFile(sharedLayer.mPath), sharedLayer.mHash); +} + +/*********************************************************************************************************************** + * Remove item suite + **********************************************************************************************************************/ + +class CommonImageManagerRemoveTest : public CommonImageManagerInstallTest { }; + +TEST_F(CommonImageManagerRemoveTest, RemoveInstalledItem) +{ + const auto manifest = AddManifest(1, 1, 1); + + CreateValidBlobs(manifest); + StoreItem(cTestItemID, cTestVersion, manifest.mManifest.mDigest.c_str(), ItemStateEnum::eInstalled); + + const auto blobInfo = MakeBlobInfo(manifest.mManifest, oci::cMediaTypeImageManifest); + + ASSERT_TRUE(mStorage.AddItemBlob(cTestItemID, cTestVersion, blobInfo).IsNone()); + + Time outdatedTimestamp; + + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String(cTestItemID), String(cTestVersion), _)) + .WillOnce(Invoke([&](const String&, const String&, const Time& timestamp) { + outdatedTimestamp = timestamp; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManager, RemoveLayer(_, _, _)).Times(0); + EXPECT_CALL(mSpaceAllocator, FreeSpace(_)).Times(0); + + auto err = mImageManager.Remove(cTestItemID, cTestVersion); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), std::vector({ItemStateEnum::eRemoving, ItemStateEnum::eRemoved})); + + for (const auto& status : GetNotifiedStatuses()) { + EXPECT_EQ(status.mType, UpdateItemTypeEnum::eService); + EXPECT_TRUE(status.mError.IsNone()); + } + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eRemoved); + EXPECT_EQ(item->mTimestamp, outdatedTimestamp); + EXPECT_EQ(GetReportedState(cTestItemID, cTestVersion), ItemStateEnum::eRemoved); + + // The item blobs and blob references are kept in the cache. + EXPECT_EQ(mStorage.GetBlobs(cTestItemID, cTestVersion), std::vector({blobInfo})); + EXPECT_TRUE(FileExists(manifest.mManifest.mPath.c_str())); +} + +TEST_F(CommonImageManagerRemoveTest, RemoveFailedAndInstallingItems) +{ + for (const auto state : {ItemStateEnum::eFailed, ItemStateEnum::eInstalling}) { + const auto version = std::string("1.0.") + std::to_string(static_cast(state)); + + AddStoredItem(cTestItemID, version.c_str(), state, Error(ErrorEnum::eFailed, "install failed")); + + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String(cTestItemID), String(version.c_str()), _)) + .WillOnce(Return(ErrorEnum::eNone)); + + auto err = mImageManager.Remove(cTestItemID, version.c_str()); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + auto item = GetStoredItem(cTestItemID, version.c_str()); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eRemoved); + EXPECT_TRUE(item->mError.IsNone()); + } + + EXPECT_EQ(GetNotifiedStates(), + std::vector( + {ItemStateEnum::eRemoving, ItemStateEnum::eRemoved, ItemStateEnum::eRemoving, ItemStateEnum::eRemoved})); +} + +TEST_F(CommonImageManagerRemoveTest, RemoveNotStoredItem) +{ + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(_, _, _)).Times(0); + EXPECT_CALL(mListener, OnItemStatusChanged(_)).Times(0); + + auto err = mImageManager.Remove(cTestItemID, cTestVersion); + EXPECT_TRUE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); +} + +TEST_F(CommonImageManagerRemoveTest, RemoveRemovedItem) +{ + AddStoredItem(cTestItemID, cTestVersion, ItemStateEnum::eRemoved); + + const auto storedItem = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(storedItem.has_value()); + + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(_, _, _)).Times(0); + EXPECT_CALL(mListener, OnItemStatusChanged(_)).Times(0); + + auto err = mImageManager.Remove(cTestItemID, cTestVersion); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetStoredItem(cTestItemID, cTestVersion), storedItem); +} + +TEST_F(CommonImageManagerRemoveTest, RemoveSetRemovedStateFails) +{ + AddStoredItem(cTestItemID, cTestVersion, ItemStateEnum::eInstalled); + + mStorage.SetFailUpdateStates({ItemStateEnum::eRemoved}); + + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(_, _, _)).Times(0); + + auto err = mImageManager.Remove(cTestItemID, cTestVersion); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + const auto statuses = GetNotifiedStatuses(); + + ASSERT_EQ(statuses.size(), 2); + EXPECT_EQ(statuses[0].mState, ItemStateEnum::eRemoving); + EXPECT_TRUE(statuses[0].mError.IsNone()); + EXPECT_EQ(statuses[1].mState, ItemStateEnum::eInstalled); + EXPECT_EQ(statuses[1].mType, UpdateItemTypeEnum::eService); + EXPECT_TRUE(statuses[1].mError.Is(ErrorEnum::eFailed)); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eInstalled); + EXPECT_EQ(GetReportedState(cTestItemID, cTestVersion), ItemStateEnum::eInstalled); +} + +TEST_F(CommonImageManagerRemoveTest, RemoveItemUnlockedWhenAddedAsOutdated) +{ + AddStoredItem(cTestItemID, cTestVersion, ItemStateEnum::eInstalled); + + bool storedAfterRemover = true; + + // The space allocator can remove the oldest outdated item synchronously, so the item must not be locked here. + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String(cTestItemID), String(cTestVersion), _)) + .WillOnce(Invoke([&](const String&, const String&, const Time&) { + EXPECT_TRUE(mImageManager.GetItemRemover().RemoveItem(cTestItemID, cTestVersion).mError.IsNone()); + + storedAfterRemover = GetStoredItem(cTestItemID, cTestVersion).has_value(); + + return ErrorEnum::eNone; + })); + + auto err = mImageManager.Remove(cTestItemID, cTestVersion); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(storedAfterRemover); +} + +TEST_F(CommonImageManagerRemoveTest, RemoveAddOutdatedItemFails) +{ + AddStoredItem(cTestItemID, cTestVersion, ItemStateEnum::eInstalled); + + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String(cTestItemID), String(cTestVersion), _)) + .WillOnce(Return(ErrorEnum::eFailed)); + + auto err = mImageManager.Remove(cTestItemID, cTestVersion); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), std::vector({ItemStateEnum::eRemoving, ItemStateEnum::eRemoved})); + + // The item stays in the eRemoved state and is registered as outdated on the next start. + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eRemoved); +} + +TEST_F(CommonImageManagerRemoveTest, GetItemsStatusesDuringRemove) +{ + AddStoredItem(cTestItemID, cTestVersion, ItemStateEnum::eInstalled); + + std::optional reportedOnRemoving; + + EXPECT_CALL(mListener, OnItemStatusChanged(_)).WillRepeatedly(Invoke([&](const UpdateItemStatus& status) { + if (status.mState == ItemStateEnum::eRemoving) { + reportedOnRemoving = GetReportedState(cTestItemID, cTestVersion); + } + })); + + auto err = mImageManager.Remove(cTestItemID, cTestVersion); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(reportedOnRemoving, ItemStateEnum::eRemoving); + EXPECT_EQ(GetReportedState(cTestItemID, cTestVersion), ItemStateEnum::eRemoved); +} + +TEST_F(CommonImageManagerRemoveTest, SetItemFail) +{ + AddStoredItem(cTestItemID, cTestVersion, ItemStateEnum::eInstalled); + + auto err = mImageManager.SetItemFail(cTestItemID, cTestVersion, Error(ErrorEnum::eIntegrity, "corrupted")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetNotifiedStates(), std::vector({ItemStateEnum::eFailed})); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eIntegrity)) << tests::utils::ErrorToStr(item->mError); +} + +TEST_F(CommonImageManagerRemoveTest, SetItemFailNotStoredItem) +{ + auto err = mImageManager.SetItemFail(cTestItemID, cTestVersion, ErrorEnum::eIntegrity); + EXPECT_TRUE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); + + EXPECT_TRUE(GetNotifiedStates().empty()); +} + +TEST_F(CommonImageManagerRemoveTest, SetItemFailRemovedItem) +{ + AddStoredItem(cTestItemID, cTestVersion, ItemStateEnum::eRemoved); + + const auto storedItem = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(storedItem.has_value()); + + auto err = mImageManager.SetItemFail(cTestItemID, cTestVersion, ErrorEnum::eIntegrity); + EXPECT_TRUE(err.Is(ErrorEnum::eWrongState)) << tests::utils::ErrorToStr(err); + + EXPECT_TRUE(GetNotifiedStates().empty()); + EXPECT_EQ(GetStoredItem(cTestItemID, cTestVersion), storedItem); +} + +TEST_F(CommonImageManagerRemoveTest, SetItemFailWaitsForInstall) +{ + const auto manifest = AddManifest(1, 1, 1); + + std::promise downloadStarted; + std::promise downloadReleased; + auto downloadReleasedFuture = downloadReleased.get_future(); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)) + .WillOnce(Invoke([&](const String& path, const BlobInfo& blobInfo) { + downloadStarted.set_value(); + downloadReleasedFuture.wait(); + + return DownloadTestBlob(path, blobInfo); + })) + .WillRepeatedly(Invoke(DownloadTestBlob)); + + Error installErr, failErr; + + std::thread installThread([&]() { installErr = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); }); + + downloadStarted.get_future().wait(); + + std::thread failThread( + [&]() { failErr = mImageManager.SetItemFail(cTestItemID, cTestVersion, ErrorEnum::eIntegrity); }); + + // Gives SetItemFail time to reach LockItem: without waiting there, it would notify eFailed and fail the check + // below. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + EXPECT_EQ( + GetNotifiedStates(), std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eDownloading})); + + downloadReleased.set_value(); + + installThread.join(); + failThread.join(); + + ASSERT_TRUE(installErr.IsNone()) << tests::utils::ErrorToStr(installErr); + ASSERT_TRUE(failErr.IsNone()) << tests::utils::ErrorToStr(failErr); + + EXPECT_EQ(GetNotifiedStates(), + std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eDownloading, ItemStateEnum::eInstalled, + ItemStateEnum::eFailed})); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eFailed); + EXPECT_TRUE(item->mError.Is(ErrorEnum::eIntegrity)) << tests::utils::ErrorToStr(item->mError); +} + +TEST_F(CommonImageManagerRemoveTest, RemoveWaitsForInstall) +{ + const auto manifest = AddManifest(1, 1, 1); + + std::promise downloadStarted; + std::promise downloadReleased; + auto downloadReleasedFuture = downloadReleased.get_future(); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)) + .WillOnce(Invoke([&](const String& path, const BlobInfo& blobInfo) { + downloadStarted.set_value(); + downloadReleasedFuture.wait(); + + return DownloadTestBlob(path, blobInfo); + })) + .WillRepeatedly(Invoke(DownloadTestBlob)); + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String(cTestItemID), String(cTestVersion), _)) + .WillOnce(Return(ErrorEnum::eNone)); + + Error installErr, removeErr; + + std::thread installThread([&]() { installErr = mImageManager.Install(MakeInstallInfo(manifest.mManifest)); }); + + downloadStarted.get_future().wait(); + + std::thread removeThread([&]() { removeErr = mImageManager.Remove(cTestItemID, cTestVersion); }); + + // Gives the remove time to reach LockItem: without waiting there, it would notify eRemoving and fail the check + // below. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + EXPECT_EQ( + GetNotifiedStates(), std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eDownloading})); + + downloadReleased.set_value(); + + installThread.join(); + removeThread.join(); + + ASSERT_TRUE(installErr.IsNone()) << tests::utils::ErrorToStr(installErr); + ASSERT_TRUE(removeErr.IsNone()) << tests::utils::ErrorToStr(removeErr); + + EXPECT_EQ(GetNotifiedStates(), + std::vector({ItemStateEnum::eInstalling, ItemStateEnum::eDownloading, ItemStateEnum::eInstalled, + ItemStateEnum::eRemoving, ItemStateEnum::eRemoved})); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eRemoved); +} + +/*********************************************************************************************************************** + * Remove outdated item suite + **********************************************************************************************************************/ + +class CommonImageManagerRemoveOutdatedTest : public CommonImageManagerInstallTest { +protected: + void SetUp() override + { + CommonImageManagerInstallTest::SetUp(); + + ON_CALL(mSpaceAllocator, AddOutdatedItem(_, _, _)).WillByDefault(Return(ErrorEnum::eNone)); + } + + void InstallAndRemove(const TestManifest& manifest, const char* itemID = cTestItemID) + { + auto err = mImageManager.Install( + MakeInstallInfo(manifest.mManifest, false, UpdateItemTypeEnum::eService, itemID, cTestVersion)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mImageManager.Remove(itemID, cTestVersion); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + } + + RetWithError RemoveOutdated(const char* itemID = cTestItemID) + { + return mImageManager.GetItemRemover().RemoveItem(itemID, cTestVersion); + } +}; + +TEST_F(CommonImageManagerRemoveOutdatedTest, RemoveUnusedBlobs) +{ + const auto sharedLayer = MakeTestBlob(500); + const auto manifest = AddManifest(1, 1, 2, false, {sharedLayer}); + const auto other = AddManifest(200, 0, 1, false, {sharedLayer}); + + InstallAndRemove(manifest); + InstallAndRemove(other, "item2"); + + const auto& ownLayer = manifest.mLayers[1]; + + EXPECT_CALL( + mImageManager, RemoveLayer(String(ownLayer.mPath.c_str()), _, String(manifest.mDiffIDs[1].mDigest.c_str()))) + .WillOnce(Return(RetWithError(10, ErrorEnum::eNone))); + EXPECT_CALL(mImageManager, RemoveLayer(String(sharedLayer.mPath.c_str()), _, _)).Times(0); + EXPECT_CALL(mListener, OnItemRemovedFromCache(String(cTestItemID), String(cTestVersion))); + EXPECT_CALL(mListener, OnItemStatusChanged(_)).Times(0); + EXPECT_CALL(mSpaceAllocator, FreeSpace(_)).Times(0); + + auto [size, err] = RemoveOutdated(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + // Manifest, image config and own layer blobs plus the unpacked layer data. + EXPECT_EQ(size, 3 * cInstallBlobSize + 10); + + EXPECT_FALSE(GetStoredItem(cTestItemID, cTestVersion).has_value()); + + for (const auto* blob : {&manifest.mManifest, &manifest.mImageConfig, &ownLayer}) { + EXPECT_FALSE(FileExists(blob->mPath.c_str())) << blob->mDigest; + } + + // The shared layer is used by item2. + EXPECT_TRUE(FileExists(sharedLayer.mPath.c_str())); +} + +TEST_F(CommonImageManagerRemoveOutdatedTest, SkipNotRemovedItem) +{ + AddStoredItem(cTestItemID, cTestVersion, ItemStateEnum::eInstalled); + + EXPECT_CALL(mListener, OnItemRemovedFromCache(_, _)).Times(0); + + auto [size, err] = RemoveOutdated(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_EQ(size, 0); + EXPECT_TRUE(GetStoredItem(cTestItemID, cTestVersion).has_value()); + + // Not stored item is already removed from the cache. + Tie(size, err) = RemoveOutdated("item2"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_EQ(size, 0); +} + +TEST_F(CommonImageManagerRemoveOutdatedTest, SkipItemInProgress) +{ + AddStoredItem(cTestItemID, cTestVersion, ItemStateEnum::eInstalled); + + std::optional> removeResult; + + // The item is already in the eRemoved state in the storage, but it is still being removed by RemoveItem. + EXPECT_CALL(mListener, OnItemStatusChanged(_)).WillRepeatedly(Invoke([&](const UpdateItemStatus& status) { + if (status.mState == ItemStateEnum::eRemoved) { + removeResult = RemoveOutdated(); + } + })); + EXPECT_CALL(mListener, OnItemRemovedFromCache(_, _)).Times(0); + + auto err = mImageManager.Remove(cTestItemID, cTestVersion); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_TRUE(removeResult.has_value()); + EXPECT_TRUE(removeResult->mError.IsNone()) << tests::utils::ErrorToStr(removeResult->mError); + EXPECT_EQ(removeResult->mValue, 0); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eRemoved); +} + +TEST_F(CommonImageManagerRemoveOutdatedTest, SkipLockedBlob) +{ + const auto sharedLayer = MakeTestBlob(500); + const auto manifest = AddManifest(1, 0, 1, false, {sharedLayer}); + const auto other = AddManifest(200, 0, 1, false, {sharedLayer}); + + InstallAndRemove(manifest); + + ASSERT_TRUE(fs::RemoveAll(sharedLayer.mPath.c_str()).IsNone()); + + std::promise downloadStarted; + std::promise downloadReleased; + auto downloadReleasedFuture = downloadReleased.get_future(); + + EXPECT_CALL(mImageManager, DownloadBlob(_, _)).WillRepeatedly(Invoke(DownloadTestBlob)); + EXPECT_CALL(mImageManager, DownloadBlob(String(sharedLayer.mPath.c_str()), _)) + .WillOnce(Invoke([&](const String& path, const BlobInfo& blobInfo) { + downloadStarted.set_value(); + downloadReleasedFuture.wait(); + + return DownloadTestBlob(path, blobInfo); + })); + + Error installErr; + + std::thread installThread([&]() { + installErr + = mImageManager.Install(MakeInstallInfo(other.mManifest, false, UpdateItemTypeEnum::eService, "item2")); + }); + + downloadStarted.get_future().wait(); + + // The shared layer is locked by item2 being downloaded: the item remover doesn't wait for it. + auto [size, err] = RemoveOutdated(); + + downloadReleased.set_value(); + installThread.join(); + + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_EQ(size, 2 * cInstallBlobSize); + EXPECT_FALSE(GetStoredItem(cTestItemID, cTestVersion).has_value()); + + ASSERT_TRUE(installErr.IsNone()) << tests::utils::ErrorToStr(installErr); + EXPECT_EQ(ReadFile(sharedLayer.mPath), sharedLayer.mHash); +} + +TEST_F(CommonImageManagerRemoveOutdatedTest, RemoveLayerFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + InstallAndRemove(manifest); + + EXPECT_CALL(mImageManager, RemoveLayer(_, _, _)).WillOnce(Return(RetWithError(10, ErrorEnum::eFailed))); + EXPECT_CALL(mListener, OnItemRemovedFromCache(String(cTestItemID), String(cTestVersion))); + + auto [size, err] = RemoveOutdated(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + // The layer blob is still removed, the failed unpacked layer data is not counted. + EXPECT_EQ(size, 3 * cInstallBlobSize); + EXPECT_FALSE(FileExists(manifest.mLayers[0].mPath.c_str())); +} + +TEST_F(CommonImageManagerRemoveOutdatedTest, RemoveDeployableItemFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + InstallAndRemove(manifest); + + mStorage.SetFailRemoveItem(true); + + EXPECT_CALL(mImageManager, RemoveLayer(_, _, _)).Times(0); + EXPECT_CALL(mListener, OnItemRemovedFromCache(_, _)).Times(0); + + auto [size, err] = RemoveOutdated(); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + EXPECT_EQ(size, 0); + + // No blob is removed and the item stays in the eRemoved state. + for (const auto* blob : {&manifest.mManifest, &manifest.mImageConfig, &manifest.mLayers[0]}) { + EXPECT_TRUE(FileExists(blob->mPath.c_str())) << blob->mDigest; + } + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eRemoved); +} + +/*********************************************************************************************************************** + * Expired items suite + **********************************************************************************************************************/ + +class CommonImageManagerExpiredTest : public CommonImageManagerRemoveOutdatedTest { +protected: + void SetUp() override + { + CommonImageManagerRemoveOutdatedTest::SetUp(); + + ON_CALL(mSpaceAllocator, RestoreOutdatedItem(_, _)).WillByDefault(Return(ErrorEnum::eNone)); + } + + // Installs and removes the item, then makes it expired. + void AddExpiredItem(const TestManifest& manifest, const char* itemID = cTestItemID) + { + InstallAndRemove(manifest, itemID); + + auto item = GetStoredItem(itemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + + item->mTimestamp = Time::Now().Add(-2 * cItemCacheTTL); + + ASSERT_TRUE(mStorage.UpdateDeployableItem(*item).IsNone()); + } + + // Returns future which is set when the item is removed from the cache. + std::future ExpectRemovedFromCache(const char* itemID = cTestItemID) + { + auto removed = std::make_shared>(); + + EXPECT_CALL(mListener, OnItemRemovedFromCache(String(itemID), String(cTestVersion))) + .WillOnce(Invoke([removed](const String&, const String&) { removed->set_value(); })); + + return removed->get_future(); + } +}; + +TEST_F(CommonImageManagerExpiredTest, RemoveExpiredItemsOnStart) +{ + const auto expired = AddManifest(1, 1, 1); + const auto notExpired = AddManifest(200, 1, 1); + const auto installed = AddManifest(400, 1, 1); + + AddExpiredItem(expired); + InstallAndRemove(notExpired, "item2"); + + auto err = mImageManager.Install( + MakeInstallInfo(installed.mManifest, false, UpdateItemTypeEnum::eService, "item3", cTestVersion)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mSpaceAllocator, RestoreOutdatedItem(String(cTestItemID), String(cTestVersion))) + .WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mSpaceAllocator, RestoreOutdatedItem(String("item2"), _)).Times(0); + EXPECT_CALL(mSpaceAllocator, RestoreOutdatedItem(String("item3"), _)).Times(0); + EXPECT_CALL(mSpaceAllocator, FreeSpace(3 * cInstallBlobSize)); + + auto removed = ExpectRemovedFromCache(); + + ASSERT_TRUE(mImageManager.Start().IsNone()); + ASSERT_EQ(removed.wait_for(cWaitTimeout), std::future_status::ready); + + // The item is removed from the cache before the listener is notified, the space is freed after it. + ASSERT_TRUE(mImageManager.Stop().IsNone()); + + EXPECT_FALSE(GetStoredItem(cTestItemID, cTestVersion).has_value()); + EXPECT_FALSE(FileExists(expired.mManifest.mPath.c_str())); + EXPECT_TRUE(GetStoredItem("item2", cTestVersion).has_value()); + EXPECT_TRUE(GetStoredItem("item3", cTestVersion).has_value()); +} + +TEST_F(CommonImageManagerExpiredTest, RemoveExpiredItemsPeriodically) +{ + ASSERT_TRUE(InitImageManager(cTestImagePath, 100 * Time::cMilliseconds).IsNone()); + ASSERT_TRUE(mImageManager.Start().IsNone()); + + const auto manifest = AddManifest(1, 1, 1); + + auto removed = ExpectRemovedFromCache(); + + AddExpiredItem(manifest); + + ASSERT_EQ(removed.wait_for(cWaitTimeout), std::future_status::ready); + ASSERT_TRUE(mImageManager.Stop().IsNone()); + + EXPECT_FALSE(GetStoredItem(cTestItemID, cTestVersion).has_value()); +} + +TEST_F(CommonImageManagerExpiredTest, SkipItemInProgress) +{ + const auto manifestA = AddManifest(1, 1, 1); + const auto manifestB = AddManifest(200, 1, 1); + + auto err = mImageManager.Install(MakeInstallInfo(manifestA.mManifest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + InstallAndRemove(manifestB, "item2"); + + ASSERT_TRUE(InitImageManager(cTestImagePath, 100 * Time::cMilliseconds).IsNone()); + ASSERT_TRUE(mImageManager.Start().IsNone()); + + auto removedB = ExpectRemovedFromCache("item2"); + + // Item A may be removed in a later period, after RemoveItem returns. + EXPECT_CALL(mListener, OnItemRemovedFromCache(String(cTestItemID), String(cTestVersion))).Times(AnyNumber()); + + bool storedAOnRemove = false; + + // Item A is in progress while eRemoved is notified: both items are made expired, so the expired items removal skips + // item A and removes item B. + EXPECT_CALL(mListener, OnItemStatusChanged(_)).WillRepeatedly(Invoke([&](const UpdateItemStatus& status) { + if (status.mState != ItemStateEnum::eRemoved) { + return; + } + + for (const auto* itemID : {cTestItemID, "item2"}) { + auto item = GetStoredItem(itemID, cTestVersion); + EXPECT_TRUE(item.has_value()); + + item->mTimestamp = Time::Now().Add(-2 * cItemCacheTTL); + + EXPECT_TRUE(mStorage.UpdateDeployableItem(*item).IsNone()); + } + + // The removal notification of item B waits for this callback, so check the storage. + const auto deadline = std::chrono::steady_clock::now() + cWaitTimeout; + + while (GetStoredItem("item2", cTestVersion).has_value() && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + storedAOnRemove = GetStoredItem(cTestItemID, cTestVersion).has_value(); + })); + + err = mImageManager.Remove(cTestItemID, cTestVersion); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_TRUE(mImageManager.Stop().IsNone()); + + EXPECT_EQ(removedB.wait_for(cWaitTimeout), std::future_status::ready); + EXPECT_TRUE(storedAOnRemove); + EXPECT_FALSE(GetStoredItem("item2", cTestVersion).has_value()); +} + +TEST_F(CommonImageManagerExpiredTest, RemoveExpiredItemFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + AddExpiredItem(manifest); + + const auto storedItem = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(storedItem.has_value()); + + mStorage.SetFailRemoveItem(true); + + auto added = std::make_shared>(); + + // The item stays in the eRemoved state, so it is added as outdated again. + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String(cTestItemID), String(cTestVersion), storedItem->mTimestamp)) + .WillOnce(Return(ErrorEnum::eNone)) + .WillOnce(Invoke([added](const String&, const String&, const Time&) { + added->set_value(); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mListener, OnItemRemovedFromCache(_, _)).Times(0); + EXPECT_CALL(mSpaceAllocator, FreeSpace(_)).Times(0); + + ASSERT_TRUE(mImageManager.Start().IsNone()); + ASSERT_EQ(added->get_future().wait_for(cWaitTimeout), std::future_status::ready); + ASSERT_TRUE(mImageManager.Stop().IsNone()); + + auto item = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(item.has_value()); + EXPECT_EQ(item->mState, ItemStateEnum::eRemoved); + EXPECT_TRUE(FileExists(manifest.mManifest.mPath.c_str())); +} + +TEST_F(CommonImageManagerExpiredTest, ReadExpiredItemFails) +{ + const auto manifest = AddManifest(1, 1, 1); + + AddExpiredItem(manifest); + + const auto storedItem = GetStoredItem(cTestItemID, cTestVersion); + ASSERT_TRUE(storedItem.has_value()); + + mStorage.SetFailGetItem(true); + + auto added = std::make_shared>(); + + // The item is restored before it is read, so it is added as outdated again with its stored timestamp. + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String(cTestItemID), String(cTestVersion), storedItem->mTimestamp)) + .WillOnce(Return(ErrorEnum::eNone)) + .WillOnce(Invoke([added](const String&, const String&, const Time&) { + added->set_value(); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mListener, OnItemRemovedFromCache(_, _)).Times(0); + + ASSERT_TRUE(mImageManager.Start().IsNone()); + ASSERT_EQ(added->get_future().wait_for(cWaitTimeout), std::future_status::ready); + ASSERT_TRUE(mImageManager.Stop().IsNone()); + + EXPECT_TRUE(FileExists(manifest.mManifest.mPath.c_str())); +} + +TEST_F(CommonImageManagerExpiredTest, SkipItemRemovedAgain) +{ + const auto manifest = AddManifest(1, 1, 1); + + AddExpiredItem(manifest); + + Time newTimestamp; + std::promise restored; + + // The item is installed and removed again after the expired items are read, before the item is locked. + EXPECT_CALL(mSpaceAllocator, RestoreOutdatedItem(String(cTestItemID), String(cTestVersion))) + .WillOnce(Invoke([&](const String& itemID, const String& version) -> Error { + restored.set_value(); + + auto item = GetStoredItem(itemID, version); + EXPECT_TRUE(item.has_value()); + + item->mTimestamp = newTimestamp = Time::Now(); + + EXPECT_TRUE(mStorage.UpdateDeployableItem(*item).IsNone()); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mListener, OnItemRemovedFromCache(_, _)).Times(0); + EXPECT_CALL(mSpaceAllocator, FreeSpace(_)).Times(0); + + std::vector