diff --git a/sonar-project.properties b/sonar-project.properties index ca07915ea..090505538 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -38,11 +38,12 @@ sonar.coverage.exclusions=**/tests/** # overridden by the build system before this header is included, which constexpr/enum can't do. # # Ignore cpp:M23_163 (MISRA C++:2023 13.1.1, classes should not be inherited virtually) in the CM -# database interface only: DatabaseItf lists all storage interfaces the CM database implements, +# 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 +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 @@ -63,5 +64,9 @@ 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/common/imagemanager/imagemanager.cpp b/src/core/common/imagemanager/imagemanager.cpp index e20d5d5c7..aa9b104f3 100644 --- a/src/core/common/imagemanager/imagemanager.cpp +++ b/src/core/common/imagemanager/imagemanager.cpp @@ -1465,7 +1465,10 @@ Error ImageManager::InstallManifestBlobs(const InstallItemInfo& itemInfo, const return AOS_ERROR_WRAP(err); } - if (imageConfig->mRootfs.mDiffIDs.Size() < manifest->mLayers.Size()) { + // 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")); } } diff --git a/src/core/common/imagemanager/tests/imagemanager.cpp b/src/core/common/imagemanager/tests/imagemanager.cpp index 446ced9aa..64a6e997f 100644 --- a/src/core/common/imagemanager/tests/imagemanager.cpp +++ b/src/core/common/imagemanager/tests/imagemanager.cpp @@ -1319,6 +1319,25 @@ TEST_F(CommonImageManagerInstallTest, InstallEmptyConfig) 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); diff --git a/src/core/common/spaceallocator/spaceallocator.hpp b/src/core/common/spaceallocator/spaceallocator.hpp index 82d22d465..4ba471b1b 100644 --- a/src/core/common/spaceallocator/spaceallocator.hpp +++ b/src/core/common/spaceallocator/spaceallocator.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "itf/spaceallocator.hpp" @@ -20,11 +21,16 @@ namespace aos::spaceallocator { struct Partition; +/** + * Outdated item ID len: item ID or blob digest. + */ +constexpr auto cOutdatedItemIDLen = Max(cIDLen, oci::cDigestLen); + /** * Outdated item. */ struct OutdatedItem { - StaticString mID; + StaticString mID; StaticString mVersion; SpaceAllocatorItf* mSpaceAllocator {}; StaticFunction mFreeCallback; @@ -565,8 +571,14 @@ class SpaceAllocator : public SpaceAllocatorItf, public SpaceAllocatorStorage { OutdatedItem item; - item.mID = id; - item.mVersion = version; + if (auto err = item.mID.Assign(id); !err.IsNone()) { + return AOS_ERROR_WRAP(Error(err, "outdated item id too long")); + } + + if (auto err = item.mVersion.Assign(version); !err.IsNone()) { + return AOS_ERROR_WRAP(Error(err, "outdated item version too long")); + } + item.mPartition = mPartition; item.mRemover = mRemover; item.mTimestamp = timestamp; diff --git a/src/core/common/spaceallocator/tests/spaceallocator.cpp b/src/core/common/spaceallocator/tests/spaceallocator.cpp index f81a21d65..30eee6d3d 100644 --- a/src/core/common/spaceallocator/tests/spaceallocator.cpp +++ b/src/core/common/spaceallocator/tests/spaceallocator.cpp @@ -789,6 +789,52 @@ TEST_F(SpaceallocatorTest, ReAddOutdatedItemUpdatesExisting) ASSERT_TRUE(mSpaceAllocator.Close().IsNone()); } +TEST_F(SpaceallocatorTest, OutdatedItemDigestID) +{ + SpaceAllocator<5> mSpaceAllocator; + + EXPECT_CALL(mPlatformFS, GetMountPoint(mPath)) + .WillOnce(Return(RetWithError>(mMountPoint, ErrorEnum::eNone))); + + ASSERT_TRUE(mSpaceAllocator.Init(mAllocator, mPath, mPlatformFS, mLimit, &mRemover).IsNone()); + + const auto digest = "sha256:" + std::string(64, 'a'); + + ASSERT_GT(digest.size(), cIDLen); + ASSERT_TRUE(mSpaceAllocator.AddOutdatedItem(digest.c_str(), "", Time::Now()).IsNone()); + + EXPECT_CALL(mRemover, RemoveItem(String(digest.c_str()), String(""))) + .WillOnce(Return(RetWithError(32 * cKilobyte, ErrorEnum::eNone))); + + auto [size, err] = mSpaceAllocator.RemoveOutdatedItem(); + ASSERT_TRUE(err.IsNone()); + EXPECT_EQ(size, 32 * cKilobyte); + + ASSERT_TRUE(mSpaceAllocator.Close().IsNone()); +} + +TEST_F(SpaceallocatorTest, OutdatedItemTooLongID) +{ + SpaceAllocator<5> mSpaceAllocator; + + EXPECT_CALL(mPlatformFS, GetMountPoint(mPath)) + .WillOnce(Return(RetWithError>(mMountPoint, ErrorEnum::eNone))); + + ASSERT_TRUE(mSpaceAllocator.Init(mAllocator, mPath, mPlatformFS, mLimit, &mRemover).IsNone()); + + const auto id = std::string(cOutdatedItemIDLen + 1, 'a'); + + EXPECT_FALSE(mSpaceAllocator.AddOutdatedItem(id.c_str(), "", Time::Now()).IsNone()); + + EXPECT_CALL(mRemover, RemoveItem(_, _)).Times(0); + + auto [size, err] = mSpaceAllocator.RemoveOutdatedItem(); + EXPECT_EQ(err, ErrorEnum::eNotFound); + EXPECT_EQ(size, 0U); + + ASSERT_TRUE(mSpaceAllocator.Close().IsNone()); +} + TEST_F(SpaceallocatorTest, RemoveOutdatedItemNoItems) { SpaceAllocator<5> mSpaceAllocator; diff --git a/src/core/common/tools/fs.cpp b/src/core/common/tools/fs.cpp index 2aa1f680d..3bce492cc 100644 --- a/src/core/common/tools/fs.cpp +++ b/src/core/common/tools/fs.cpp @@ -37,6 +37,16 @@ Error FileErrnoToError(int32_t code) } } +int StatNoFollow(const char* path, struct stat* st) +{ +#if defined(__ZEPHYR__) + // Zephyr doesn't provide lstat, its file systems don't support symlinks. + return stat(path, st); +#else + return lstat(path, st); +#endif +} + } // namespace /*********************************************************************************************************************** @@ -93,7 +103,7 @@ bool DirIterator::Next() auto path = JoinPath(mRoot, entry->d_name); struct stat entryStat; - if (stat(path.CStr(), &entryStat) == -1) { + if (StatNoFollow(path.CStr(), &entryStat) == -1) { return false; } @@ -591,11 +601,11 @@ RetWithError CalculateSize(AllocatorItf& allocator, const String& path) { struct stat st; - if (auto ret = stat(path.CStr(), &st); ret != 0) { + if (auto ret = StatNoFollow(path.CStr(), &st); ret != 0) { return {0, AOS_ERROR_WRAP(Error(errno))}; } - if (S_ISREG(st.st_mode)) { + if (!S_ISDIR(st.st_mode)) { return {static_cast(st.st_size)}; } @@ -617,7 +627,12 @@ RetWithError CalculateSize(AllocatorItf& allocator, const String& path) while (dirIt.Next()) { const auto fullPath = JoinPath(dirIt.GetRootPath(), dirIt->mPath); - if (dirIt->mIsDir) { + // Symlinks are not followed: they may point outside the path or to the content counted elsewhere. + if (auto ret = StatNoFollow(fullPath.CStr(), &st); ret != 0) { + return {0, AOS_ERROR_WRAP(Error(errno))}; + } + + if (S_ISDIR(st.st_mode)) { if (auto err = dirIterators->EmplaceBack(fullPath); !err.IsNone()) { return {0, AOS_ERROR_WRAP(err)}; } @@ -627,10 +642,6 @@ RetWithError CalculateSize(AllocatorItf& allocator, const String& path) break; } - if (auto ret = stat(fullPath.CStr(), &st); ret != 0) { - return {0, AOS_ERROR_WRAP(Error(ret))}; - } - size += st.st_size; } diff --git a/src/core/common/tools/tests/fs.cpp b/src/core/common/tools/tests/fs.cpp index 9384012dd..cea23134d 100644 --- a/src/core/common/tools/tests/fs.cpp +++ b/src/core/common/tools/tests/fs.cpp @@ -711,6 +711,34 @@ TEST_F(FSTest, CalculateSize) EXPECT_EQ(size, 0); } +TEST_F(FSTest, CalculateSizeDoesNotFollowSymlinks) +{ + const auto walkDirRoot = cBaseTestDir / "calculate-symlinks-test"; + const auto outsideDir = cBaseTestDir / "calculate-symlinks-outside"; + + ASSERT_TRUE(fs::MakeDirAll(walkDirRoot.c_str()).IsNone()); + ASSERT_TRUE(fs::MakeDirAll(outsideDir.c_str()).IsNone()); + + CreateFile((walkDirRoot / "f.txt").c_str(), std::string(1024, 'a').c_str(), 0444); + CreateFile((outsideDir / "big.txt").c_str(), std::string(4096, 'b').c_str(), 0444); + + const std::vector> links = { + {outsideDir.string(), walkDirRoot / "dir-link"}, + {(outsideDir / "big.txt").string(), walkDirRoot / "file-link"}, + {"/not-existing-target", walkDirRoot / "dangling-link"}, + }; + + size_t expectedSize = 1024; + + for (const auto& [target, link] : links) { + std::filesystem::create_symlink(target, link); + + expectedSize += target.size(); + } + + EXPECT_EQ(fs::CalculateSize(mAllocator, walkDirRoot.c_str()), RetWithError(expectedSize)); +} + TEST_F(FSTest, GetFileMTime) { const auto testFile = cBaseTestDir / "modify-time-test.txt"; diff --git a/src/core/sm/database/database.md b/src/core/sm/database/database.md index 42a0824a1..2090f9f61 100644 --- a/src/core/sm/database/database.md +++ b/src/core/sm/database/database.md @@ -3,7 +3,11 @@ The database module provides persistent storage for various SM modules. It implements storage interfaces for the following modules: -- [aos::sm::imagemanager::StorageItf](../imagemanager/itf/storage.hpp) - image manager storage interface; +- [aos::common::imagemanager::StorageItf](../../common/imagemanager/itf/storage.hpp) - common image manager storage + interface, inherited virtually so that the implementation may share it with a common image manager storage base + class; +- [aos::sm::imagemanager::StorageItf](../imagemanager/itf/storage.hpp) - SM image manager storage interface, which + extends the common one; - [aos::sm::launcher::StorageItf](../launcher/itf/storage.hpp) - launcher storage interface. - [aos::sm::networkmanager::StorageItf](../networkmanager/itf/storage.hpp) - network manager storage interface; @@ -13,6 +17,10 @@ classDiagram <> } + class CommonImageManagerStorageItf ["aos::common::imagemanager::StorageItf"] { + <> + } + class ImageManagerStorageItf ["aos::sm::imagemanager::StorageItf"] { <> } @@ -25,7 +33,9 @@ classDiagram <> } + DatabaseItf ..|> CommonImageManagerStorageItf DatabaseItf ..|> ImageManagerStorageItf + ImageManagerStorageItf ..|> CommonImageManagerStorageItf DatabaseItf ..|> LauncherStorageItf DatabaseItf ..|> NetworkManagerStorageItf ``` diff --git a/src/core/sm/database/itf/database.hpp b/src/core/sm/database/itf/database.hpp index 02edb3c6c..2e9ceef88 100644 --- a/src/core/sm/database/itf/database.hpp +++ b/src/core/sm/database/itf/database.hpp @@ -7,6 +7,7 @@ #ifndef AOS_CORE_SM_DATABASE_ITF_DATABASE_HPP_ #define AOS_CORE_SM_DATABASE_ITF_DATABASE_HPP_ +#include #include #include #include @@ -20,7 +21,10 @@ namespace aos::sm::database { /** * Database interface. */ -class DatabaseItf : public imagemanager::StorageItf, public launcher::StorageItf, public networkmanager::StorageItf { }; +class DatabaseItf : public virtual common::imagemanager::StorageItf, + public imagemanager::StorageItf, + public launcher::StorageItf, + public networkmanager::StorageItf { }; /** @}*/ diff --git a/src/core/sm/imagemanager/CMakeLists.txt b/src/core/sm/imagemanager/CMakeLists.txt index f9e22b824..3ca541402 100644 --- a/src/core/sm/imagemanager/CMakeLists.txt +++ b/src/core/sm/imagemanager/CMakeLists.txt @@ -20,13 +20,13 @@ set(SOURCES imagemanager.cpp) # Headers # ###################################################################################################################### -set(HEADERS itf/blobinfoprovider.hpp itf/imagemanager.hpp itf/iteminfoprovider.hpp config.hpp imagemanager.hpp) +set(HEADERS itf/imagehandler.hpp itf/imagemanager.hpp itf/storage.hpp imagemanager.hpp) # ###################################################################################################################### # Libraries # ###################################################################################################################### -set(LIBRARIES aos::core::common::tools) +set(LIBRARIES aos::core::common::imagemanager aos::core::common::tools) # ###################################################################################################################### # Target diff --git a/src/core/sm/imagemanager/config.hpp b/src/core/sm/imagemanager/config.hpp deleted file mode 100644 index 053e49674..000000000 --- a/src/core/sm/imagemanager/config.hpp +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_SM_IMAGEMANAGER_CONFIG_HPP_ -#define AOS_CORE_SM_IMAGEMANAGER_CONFIG_HPP_ - -#include -#include -#include - -namespace aos::sm::imagemanager { - -/** - * Image manager config. - */ -struct Config { - StaticString mImagePath; - size_t mPartLimit {}; - Duration mUpdateItemTTL {30 * 24 * Time::cHours}; - Duration mRemoveOutdatedPeriod {24 * Time::cHours}; -}; - -} // namespace aos::sm::imagemanager - -#endif diff --git a/src/core/sm/imagemanager/imagemanager.cpp b/src/core/sm/imagemanager/imagemanager.cpp index bcbec05fc..07f5c4f10 100644 --- a/src/core/sm/imagemanager/imagemanager.cpp +++ b/src/core/sm/imagemanager/imagemanager.cpp @@ -11,1534 +11,652 @@ namespace aos::sm::imagemanager { /*********************************************************************************************************************** - * Static + * Public **********************************************************************************************************************/ -namespace { - -Error SplitDigest(const String& digest, String& alg, String& hash) +Error ImageManager::Init(const common::imagemanager::Config& config, + common::imagemanager::BlobInfoProviderItf& blobInfoProvider, StorageItf& storage, + spaceallocator::SpaceAllocatorItf& spaceAllocator, downloader::DownloaderItf& downloader, + ImageHandlerItf& imageHandler, fs::FileInfoProviderItf& fileInfoProvider, oci::OCISpecItf& ociSpec, + fs::FSPlatformItf& fsPlatform, AllocatorItf& allocator) { - StaticArray, 2> digestList; + LOG_DBG() << "Init SM image manager"; - if (auto err = digest.Split(digestList, ':'); !err.IsNone()) { - return err; + mDownloader = &downloader; + mImageHandler = &imageHandler; + mStorage = &storage; + mAllocator = &allocator; + + // The longest layer path is /layers///digest, / has the digest + // length. + if (config.mImagePath.Size() + 1 + String(cLayersFolder).Size() + 1 + oci::cDigestLen + 1 + + String(cDigestFile).Size() + > cFilePathLen) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "image path is too long")); } - if (digestList.Size() != 2) { - return ErrorEnum::eInvalidArgument; + if (auto err = common::imagemanager::ImageManager::Init( + config, blobInfoProvider, storage, spaceAllocator, fileInfoProvider, ociSpec, fsPlatform, allocator); + !err.IsNone()) { + return err; } - if (auto err = alg.Assign(digestList[0]); !err.IsNone()) { + if (auto err = mLayersPath.Assign(fs::JoinPath(config.mImagePath, cLayersFolder)); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (auto err = hash.Assign(digestList[1]); !err.IsNone()) { + if (auto err = fs::MakeDirAll(mLayersPath); !err.IsNone()) { return AOS_ERROR_WRAP(err); } return ErrorEnum::eNone; } -Error AddPathIfNotExist(Array>& list, const String& path) +Error ImageManager::InstallItem(const UpdateItemInfo& itemInfo) { - if (list.Contains(path)) { - return ErrorEnum::eNone; - } + LOG_INF() << "Install item" << Log::Field("itemID", itemInfo.mID) << Log::Field("version", itemInfo.mVersion) + << Log::Field("type", itemInfo.mType) << Log::Field("manifestDigest", itemInfo.mManifestDigest); - return list.PushBack(path); + return common::imagemanager::ImageManager::InstallItem( + {itemInfo.mID, itemInfo.mType, itemInfo.mVersion, itemInfo.mManifestDigest, false}); } -} // namespace +Error ImageManager::RemoveItem(const String& itemID, const String& version) +{ + LOG_INF() << "Remove item" << Log::Field("itemID", itemID) << Log::Field("version", version); -/*********************************************************************************************************************** - * Public - **********************************************************************************************************************/ + return common::imagemanager::ImageManager::RemoveItem(itemID, version); +} -Error ImageManager::Init(AllocatorItf& allocator, const Config& config, BlobInfoProviderItf& blobInfoProvider, - spaceallocator::SpaceAllocatorItf& spaceAllocator, downloader::DownloaderItf& downloader, - fs::FileInfoProviderItf& fileInfoProvider, oci::OCISpecItf& ociSpec, ImageHandlerItf& imageHandler, - StorageItf& storage) +Error ImageManager::Cancel() { - LOG_DBG() << "Init image manager"; - - mAllocator = &allocator; - mConfig = config; - mBlobInfoProvider = &blobInfoProvider; - mSpaceAllocator = &spaceAllocator; - mDownloader = &downloader; - mFileInfoProvider = &fileInfoProvider; - mOCISpec = &ociSpec; - mImageHandler = &imageHandler; - mStorage = &storage; - - LOG_DBG() << "Config" << Log::Field("imagePath", mConfig.mImagePath) << Log::Field("partLimit", mConfig.mPartLimit) - << Log::Field("updateItemTTL", mConfig.mUpdateItemTTL) - << Log::Field("removeOutdatedPeriod", mConfig.mRemoveOutdatedPeriod); - - if (auto err = fs::MakeDirAll(fs::JoinPath(mConfig.mImagePath, cBlobsFolder)); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + LOG_INF() << "Cancel items"; - if (auto err = fs::MakeDirAll(fs::JoinPath(mConfig.mImagePath, cLayersFolder)); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + // The items waiting for a layer processed by another item are canceled as well, so Cancel doesn't wait for the + // layer unpack. + SetLayersCanceled(true); - return ErrorEnum::eNone; + auto err = common::imagemanager::ImageManager::Cancel(); + + SetLayersCanceled(false); + + return err; } -Error ImageManager::Start() +Error ImageManager::GetLayerPath(const String& diffID, String& path) const { - LockGuard lock {mMutex}; - - LOG_DBG() << "Start image manager"; + StaticString layerPath; - if (auto err = UpdateOutdatedItems(); !err.IsNone()) { - LOG_ERR() << "Can't update outdated items" << Log::Field(err); + if (auto err = CreateLayerPath(diffID, layerPath); !err.IsNone()) { + return err; } - mProcessOutdatedItems = true; - - if (auto err = mCV.NotifyAll(); !err.IsNone()) { - LOG_ERR() << "Can't notify image manager" << Log::Field(AOS_ERROR_WRAP(err)); + if (auto err = path.Assign(fs::JoinPath(layerPath, cUnpackedLayerFolder)); !err.IsNone()) { + return AOS_ERROR_WRAP(err); } - if (auto err = mTimer.Start( - mConfig.mRemoveOutdatedPeriod, - [this](void*) { - LockGuard lock {mMutex}; - - mProcessOutdatedItems = true; + LOG_DBG() << "Get layer path" << Log::Field("diffID", diffID) << Log::Field("path", path); - if (auto err = mCV.NotifyAll(); !err.IsNone()) { - LOG_ERR() << "Can't notify image manager" << Log::Field(AOS_ERROR_WRAP(err)); - } - }, - false); - !err.IsNone()) { + auto [exists, err] = fs::DirExist(path); + if (!err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (auto err = mThread.Run([this](void*) { ProcessOutdatedItems(); }); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (!exists) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "unpacked layer not found")); } return ErrorEnum::eNone; } -Error ImageManager::Stop() -{ - Error stopErr; +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ - { - LockGuard lock {mMutex}; +Error ImageManager::DownloadBlob(CancelContext& ctx, const String& path, const BlobInfo& blobInfo) +{ + LOG_DBG() << "Download blob" << Log::Field("digest", blobInfo.mDigest) << Log::Field("size", blobInfo.mSize); - LOG_DBG() << "Stop image manager"; + // The downloaded blob is validated by the common image manager. + if (auto err = mDownloader->Download(ctx, blobInfo.mDigest, blobInfo.mURLs, path); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } - if (auto err = mTimer.Stop(Timer::StopMode::WaitForCallbacks); !err.IsNone() && stopErr.IsNone()) { - stopErr = AOS_ERROR_WRAP(err); - } + return ErrorEnum::eNone; +} - mClose = true; +Error ImageManager::ValidateLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) +{ + (void)path; - if (auto err = mCV.NotifyAll(); !err.IsNone()) { - LOG_ERR() << "Can't notify image manager" << Log::Field(AOS_ERROR_WRAP(err)); - } + // A layer without diff ID has no image config, so it is kept as packed blob only. + if (diffID.IsEmpty()) { + return ErrorEnum::eNotSupported; } - if (auto err = mThread.Join(); !err.IsNone() && stopErr.IsNone()) { - stopErr = AOS_ERROR_WRAP(err); + if (auto err = CheckLayerMediaType(descriptor.mMediaType); !err.IsNone()) { + return err; } - return stopErr; -} - -Error ImageManager::GetAllInstalledItems(Array& statuses) const -{ - LockGuard lock {mMutex}; - - LOG_DBG() << "Get all installed items"; + StaticString layerPath; - auto itemsData = MakeUnique(mAllocator); - if (!itemsData) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + if (auto err = CreateLayerPath(diffID, layerPath); !err.IsNone()) { + return err; } - if (auto err = mStorage->GetAllUpdateItems(*itemsData); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (auto err = LockLayer(diffID); !err.IsNone()) { + return err; } - for (const auto& itemData : *itemsData) { - if (itemData.mState != ItemStateEnum::eInstalled) { - continue; - } + auto unlockLayer = DeferRelease([this, &diffID]() { UnlockLayer(diffID); }); - if (auto err - = statuses.PushBack(UpdateItemStatus {itemData.mID, itemData.mType, itemData.mVersion, itemData.mState}); - !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + if (auto err = ValidateUnpackedLayer(layerPath); !err.IsNone()) { + return err; } return ErrorEnum::eNone; } -Error ImageManager::InstallUpdateItem(const UpdateItemInfo& itemInfo) +Error ImageManager::UnpackLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) { - LOG_INF() << "Install update item" << Log::Field("itemID", itemInfo.mID) << Log::Field("version", itemInfo.mVersion) - << Log::Field("type", itemInfo.mType) << Log::Field("manifestDigest", itemInfo.mManifestDigest); + // ValidateLayer skips the layers without diff ID, so UnpackLayer is never called for them. + if (diffID.IsEmpty()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "layer diff ID is empty")); + } - auto [installItemIt, err] = CreateInstallingItem(itemInfo); - if (!err.IsNone()) { + if (auto err = CheckLayerMediaType(descriptor.mMediaType); !err.IsNone()) { return err; } - auto& installItem = *installItemIt; - - auto releaseInstallingItem - = DeferRelease(this, [&](ImageManager* self) { self->ReleaseInstallingItem(installItemIt); }); - - oci::ContentDescriptor manifestDescriptor {"", itemInfo.mManifestDigest, 0}; - - LOG_DBG() << "Install manifest blob" << Log::Field("digest", itemInfo.mManifestDigest); + StaticString layerPath; - err = InstallBlob(manifestDescriptor, &installItem); - if (!err.IsNone()) { + if (auto err = CreateLayerPath(diffID, layerPath); !err.IsNone()) { return err; } - auto manifest = MakeUnique(mAllocator); - if (!manifest) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); + if (auto err = LockLayer(diffID); !err.IsNone()) { + return err; } - StaticString path; + auto unlockLayer = DeferRelease([this, &diffID]() { UnlockLayer(diffID); }); - err = CreateBlobPath(itemInfo.mManifestDigest, path); - if (!err.IsNone()) { - return err; - } + // The layer could be unpacked from another blob with the same diff ID while this call was waiting for the lock. + if (auto err = ValidateUnpackedLayer(layerPath); err.IsNone()) { + RemoveLayerBlob(path); - err = mOCISpec->LoadImageManifest(path, *manifest); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); + return ErrorEnum::eNone; } - if (manifest->mItemConfig.HasValue()) { - LOG_DBG() << "Install item config blob" << Log::Field("digest", manifest->mItemConfig->mDigest); + // RemoveLayer keeps the layer of the item being installed, as the item references it, so it is removed here. + auto [size, err] = RemoveLayerFiles(layerPath); - err = InstallBlob(*manifest->mItemConfig, &installItem); - if (!err.IsNone()) { - return err; - } + // The size of the files removed before a failure is freed as well. + if (size > 0) { + GetSpaceAllocator().FreeSpace(size); } - if (itemInfo.mType == UpdateItemTypeEnum::eService) { - err = InstallServiceLayers(*manifest, installItem); - if (!err.IsNone()) { - return err; - } - } else { - err = InstallComponentLayers(*manifest, installItem); - if (!err.IsNone()) { - return err; - } + if (!err.IsNone()) { + return err; } - err = StoreUpdateItem(itemInfo); - if (!err.IsNone()) { + if (err = InstallUnpackedLayer(path, descriptor.mMediaType, layerPath); !err.IsNone()) { return err; } + RemoveLayerBlob(path); + return ErrorEnum::eNone; } -Error ImageManager::RemoveUpdateItem(const String& itemID, const String& version) +RetWithError ImageManager::RemoveLayer( + const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) { - LockGuard lock {mMutex}; - - LOG_INF() << "Remove update item" << Log::Field("itemID", itemID) << Log::Field("version", version); - - StaticArray itemData; - - if (auto err = mStorage->GetUpdateItem(itemID, itemData); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + (void)path; + (void)descriptor; - auto it = itemData.FindIf([&](const UpdateItemData& data) { return data.mVersion == version; }); - if (it == itemData.end()) { - return AOS_ERROR_WRAP(ErrorEnum::eNotFound); + if (diffID.IsEmpty()) { + return {0, ErrorEnum::eNone}; } - it->mState = ItemStateEnum::eRemoved; - it->mTimestamp = Time::Now(); + StaticString layerPath; - if (auto err = mSpaceAllocator->AddOutdatedItem(itemID, version, it->mTimestamp); !err.IsNone()) { - LOG_ERR() << "Failed to add outdated item" << Log::Field("itemID", itemID) << Log::Field("version", version) - << Log::Field(err); + if (auto err = CreateLayerPath(diffID, layerPath); !err.IsNone()) { + return {0, err}; } - if (auto err = mStorage->UpdateUpdateItem(*it); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + // It may be called from the item remover, so it must not wait: a layer being processed is in use. + if (!TryLockLayer(diffID)) { + return {0, ErrorEnum::eNone}; } - return ErrorEnum::eNone; -} + auto unlockLayer = DeferRelease([this, &diffID]() { UnlockLayer(diffID); }); -Error ImageManager::GetBlobPath(const String& digest, String& path) const -{ - if (auto err = CreateBlobPath(digest, path); !err.IsNone()) { - return err; + auto [used, err] = mStorage->IsLayerUsed(diffID); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; } - LOG_DBG() << "Get blob path" << Log::Field("digest", digest) << Log::Field("path", path); - - auto [exists, err] = fs::FileExist(path); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (used) { + return {0, ErrorEnum::eNone}; } - if (!exists) { - return AOS_ERROR_WRAP(ErrorEnum::eNotFound); + size_t size = 0; + + if (Tie(size, err) = RemoveLayerFiles(layerPath); !err.IsNone()) { + // It may be called from the item remover under the space allocator lock, so it must not free the space: the + // size of the partially removed files is dropped as the common image manager does on error. + return {0, err}; } - return ErrorEnum::eNone; + return {size, ErrorEnum::eNone}; } -Error ImageManager::GetLayerPath(const String& digest, String& path) const +RetWithError ImageManager::RemoveOrphanLayers() { - if (auto err = CreateLayerPath(digest, path); !err.IsNone()) { - return err; - } + LOG_DBG() << "Remove orphan layers"; - if (auto err = path.Assign(fs::JoinPath(path, cUnpackedLayerFolder)); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + size_t freedSize = 0; + Error firstErr; - LOG_DBG() << "Get layer path" << Log::Field("digest", digest) << Log::Field("path", path); + // The layer which is failed to remove is logged and removed in the next period, the first error is returned. + const auto removeLayer = [this, &firstErr, &freedSize](const String& path, const String& diffID) { + auto [size, err] = RemoveOrphanLayer(path, diffID); + if (!err.IsNone()) { + LOG_ERR() << "Failed to remove orphan layer" << Log::Field("path", path) << Log::Field(err); - auto [exists, err] = fs::DirExist(path); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + if (firstErr.IsNone()) { + firstErr = err; + } + } - if (!exists) { - return AOS_ERROR_WRAP(ErrorEnum::eNotFound); - } + freedSize += size; + }; - return ErrorEnum::eNone; -} + for (auto algIt = fs::DirIterator(mLayersPath); algIt.Next();) { + const auto algPath = fs::JoinPath(mLayersPath, algIt->mPath); -/*********************************************************************************************************************** - * Private - **********************************************************************************************************************/ + // The layers folder contains only the algorithm folders. + if (!algIt->mIsDir) { + removeLayer(algPath, ""); -RetWithError ImageManager::RemoveItem(const String& id, const String& version) -{ - LockGuard lock {mMutex}; + continue; + } - LOG_DBG() << "Remove item" << Log::Field("id", id) << Log::Field("version", version); + for (auto layerIt = fs::DirIterator(algPath); layerIt.Next();) { + StaticString diffID; - if (auto err = mStorage->RemoveUpdateItem(id, version); !err.IsNone()) { - LOG_ERR() << "Failed to remove update item" << Log::Field("itemID", id) << Log::Field("version", version) - << Log::Field(err); - } + // The diff ID is left empty if it doesn't fit, so the layer is removed as invalid. + if (algIt->mPath.Size() + 1 + layerIt->mPath.Size() <= diffID.MaxSize()) { + (void)diffID.Append(algIt->mPath).Append(":").Append(layerIt->mPath); + } - auto [size, err] = RemoveOrphans(); - if (!err.IsNone()) { - return RetWithError(0, AOS_ERROR_WRAP(err)); + removeLayer(fs::JoinPath(algPath, layerIt->mPath), diffID); + } } - return size; + return {freedSize, firstErr}; } -Error ImageManager::CreateBlobPath(const String& digest, String& path) const +Error ImageManager::CreateLayerPath(const String& diffID, String& path) const { - StaticString alg; + StaticString algorithm; StaticString hash; - if (auto err = SplitDigest(digest, alg, hash); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (auto err = ParseDigest(diffID, algorithm, hash); !err.IsNone()) { + return err; } - if (auto err = path.Assign(fs::JoinPath(mConfig.mImagePath, cBlobsFolder, alg, hash)); !err.IsNone()) { + if (auto err = path.Assign(fs::JoinPath(mLayersPath, algorithm, hash)); !err.IsNone()) { return AOS_ERROR_WRAP(err); } return ErrorEnum::eNone; } -Error ImageManager::CreateLayerPath(const String& digest, String& path) const +Error ImageManager::ValidateUnpackedLayer(const String& layerPath) const { - StaticString alg; - StaticString hash; + LOG_DBG() << "Validate unpacked layer" << Log::Field("path", layerPath); - if (auto err = SplitDigest(digest, alg, hash); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + const auto unpackedPath = fs::JoinPath(layerPath, cUnpackedLayerFolder); + const auto digestPath = fs::JoinPath(layerPath, cDigestFile); - if (auto err = path.Assign(fs::JoinPath(mConfig.mImagePath, cLayersFolder, alg, hash)); !err.IsNone()) { + auto [exists, err] = fs::DirExist(unpackedPath); + if (!err.IsNone()) { return AOS_ERROR_WRAP(err); } - return ErrorEnum::eNone; -} - -Error ImageManager::ValidateBlob(const String& path, const String& digest) const -{ - LOG_DBG() << "Validate blob" << Log::Field("digest", digest); + if (!exists) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "unpacked layer not found")); + } - StaticString alg; - StaticString hash; + // The metadata is written last, so the layer without it is not completely unpacked. + const StaticString metadataPaths[] = {digestPath, fs::JoinPath(layerPath, cSizeFile)}; - if (auto err = SplitDigest(digest, alg, hash); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + for (const auto& metadataPath : metadataPaths) { + if (Tie(exists, err) = fs::FileExist(metadataPath); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } - if (alg.Compare(crypto::Hash(crypto::HashEnum::eSHA256).ToString(), String::CaseSensitivity::CaseInsensitive) - != 0) { - return AOS_ERROR_WRAP(ErrorEnum::eNotSupported); + if (!exists) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "layer metadata not found")); + } } - fs::FileInfo fileInfo; + StaticString storedDigest; - if (auto err = mFileInfoProvider->GetFileInfo(path, fileInfo); !err.IsNone()) { + if (err = fs::ReadFileToString(digestPath, storedDigest); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - StaticArray hashBytes; + StaticString layerDigest; - if (auto err = hash.HexToByteArray(hashBytes); !err.IsNone()) { + if (Tie(layerDigest, err) = mImageHandler->GetUnpackedLayerDigest(unpackedPath); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (fileInfo.mCheckSum != hashBytes) { - return AOS_ERROR_WRAP(ErrorEnum::eInvalidChecksum); + if (layerDigest != storedDigest) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidChecksum, "wrong layer checksum")); } return ErrorEnum::eNone; } -Error ImageManager::ValidateLayer(const String& path, const String& diffDigest) const +Error ImageManager::InstallUnpackedLayer(const String& path, const String& mediaType, const String& layerPath) { - LOG_DBG() << "Validate layer" << Log::Field("path", path) << Log::Field("diffDigest", diffDigest); - - auto [size, err] = fs::CalculateSize(*mAllocator, path); + LOG_DBG() << "Unpack layer" << Log::Field("path", path) << Log::Field("layerPath", layerPath); - if (size > oci::cDigestLen) { - return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "not link to unpacked layer")); + auto [unpackedSize, sizeErr] = mImageHandler->GetUnpackedLayerSize(path, mediaType); + if (!sizeErr.IsNone() && !sizeErr.Is(ErrorEnum::eNotSupported)) { + return AOS_ERROR_WRAP(sizeErr); } - StaticString digest; - - err = fs::ReadFileToString(path, digest); + auto [space, err] = GetSpaceAllocator().AllocateSpace(unpackedSize); if (!err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (digest != diffDigest) { - return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidChecksum, "wrong diff digest")); + err = UnpackToLayerPath(path, mediaType, layerPath, unpackedSize, *space); + if (err.IsNone()) { + if (err = space->Accept(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; } - StaticString dstPath; + if (auto removeErr = fs::RemoveAll(layerPath); !removeErr.IsNone()) { + LOG_ERR() << "Failed to remove layer" << Log::Field("path", layerPath) << Log::Field(AOS_ERROR_WRAP(removeErr)); + + // The removal may fail in the middle, so the space of the files left stays used until the layer is processed + // again. + AcceptRemainingSpace(layerPath, *space); - err = CreateLayerPath(diffDigest, dstPath); - if (!err.IsNone()) { return err; } - err = fs::ReadFileToString(fs::JoinPath(dstPath, cDigestFile), digest); - if (!err.IsNone()) { + if (auto releaseErr = space->Release(); !releaseErr.IsNone()) { + LOG_ERR() << "Failed to release space" << Log::Field("path", layerPath) << Log::Field(releaseErr); + } + + return err; +} + +Error ImageManager::UnpackToLayerPath(const String& path, const String& mediaType, const String& layerPath, + size_t unpackedSize, spaceallocator::SpaceItf& space) +{ + if (auto err = fs::MakeDirAll(layerPath); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - StaticString layerDigest; + if (auto err = mImageHandler->UnpackLayer(path, fs::JoinPath(layerPath, cUnpackedLayerFolder), mediaType); + !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = CreateLayerMetadata(layerPath, unpackedSize); !err.IsNone()) { + return err; + } - Tie(layerDigest, err) = mImageHandler->GetUnpackedLayerDigest(fs::JoinPath(dstPath, cUnpackedLayerFolder)); + // The layer removal frees the size of the layer files, so the accepted space has to match it. + auto [size, err] = fs::CalculateSize(*mAllocator, layerPath); if (!err.IsNone()) { return AOS_ERROR_WRAP(err); } - LOG_DBG() << "Layer digest" << Log::Field("path", path) << Log::Field("layerDigest", layerDigest); - - if (layerDigest != digest) { - return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidChecksum, "wrong layer checksum")); + if (err = space.Resize(size); !err.IsNone()) { + return AOS_ERROR_WRAP(err); } return ErrorEnum::eNone; } -Error ImageManager::DownloadBlob(const String& path, const String& digest, size_t size) +Error ImageManager::CreateLayerMetadata(const String& layerPath, size_t unpackedSize) { - Error err; - UniquePtr space; - StaticString url; + auto [digest, err] = mImageHandler->GetUnpackedLayerDigest(fs::JoinPath(layerPath, cUnpackedLayerFolder)); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } - auto releaseSpace = DeferRelease(&err, [this, &digest, &path, &space](const Error* releaseErr) { - if (!releaseErr->IsNone()) { - LOG_ERR() << "Failed to download blob" << Log::Field("digest", digest) << Log::Field(*releaseErr); - } + LOG_DBG() << "Create layer metadata" << Log::Field("path", layerPath) << Log::Field("digest", digest) + << Log::Field("size", unpackedSize); - ReleaseSpace(path, space.Get(), *releaseErr); - }); + StaticString<64> sizeStr; - err = GetBlobURL(digest, url); - if (!err.IsNone()) { - return err; + if (err = sizeStr.Convert(unpackedSize); !err.IsNone()) { + return AOS_ERROR_WRAP(err); } - if (size) { - Tie(space, err) = mSpaceAllocator->AllocateSpace(size); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + if (err = fs::WriteStringToFile(fs::JoinPath(layerPath, cSizeFile), sizeStr, 0600); !err.IsNone()) { + return AOS_ERROR_WRAP(err); } - LOG_DBG() << "Download blob" << Log::Field("digest", digest) << Log::Field("size", size) << Log::Field("url", url); - - err = mDownloader->Download(digest, url, path); - if (!err.IsNone()) { + if (err = fs::WriteStringToFile(fs::JoinPath(layerPath, cDigestFile), digest, 0600); !err.IsNone()) { return AOS_ERROR_WRAP(err); } return ErrorEnum::eNone; } -Error ImageManager::InstallBlob(const oci::ContentDescriptor& descriptor, InstallItem* installItem, bool waitInstalling) +RetWithError ImageManager::RemoveLayerFiles(const String& layerPath) { - if (waitInstalling) { - if (auto err = WaitForInstallingBlob(descriptor.mDigest); !err.IsNone()) { - return err; - } + LOG_DBG() << "Remove layer" << Log::Field("path", layerPath); + + // A file in the layers folder is not a layer, but it is removed the same way. + auto [exists, err] = fs::FileExist(layerPath); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; } - auto releaseInstalling = DeferRelease(&descriptor.mDigest, [this, &waitInstalling](const String* digest) { - if (waitInstalling) { - if (auto releaseErr = ReleaseInstallingBlob(*digest); !releaseErr.IsNone()) { - LOG_ERR() << "Can't release installing blob" << Log::Field("digest", *digest) - << Log::Field(AOS_ERROR_WRAP(releaseErr)); - } + if (!exists) { + if (Tie(exists, err) = fs::DirExist(layerPath); !err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; } - }); + } - if (installItem) { - LockGuard lock {mMutex}; + if (!exists) { + return {0, ErrorEnum::eNone}; + } - if (auto err = installItem->mBlobs.EmplaceBack(descriptor.mDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); + size_t size = 0; + + if (Tie(size, err) = fs::CalculateSize(*mAllocator, layerPath); !err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (err = fs::RemoveAll(layerPath); !err.IsNone()) { + // The removal may fail in the middle, so the size of the removed files is returned with the error. + auto [remainingSize, sizeErr] = fs::CalculateSize(*mAllocator, layerPath); + if (!sizeErr.IsNone() || remainingSize > size) { + return {0, AOS_ERROR_WRAP(err)}; } + + return {size - remainingSize, AOS_ERROR_WRAP(err)}; } - LOG_DBG() << "Install blob" << Log::Field("digest", descriptor.mDigest) << Log::Field("size", descriptor.mSize); + return {size, ErrorEnum::eNone}; +} - StaticString path; +void ImageManager::AcceptRemainingSpace(const String& layerPath, spaceallocator::SpaceItf& space) +{ + auto [size, err] = fs::CalculateSize(*mAllocator, layerPath); + if (!err.IsNone()) { + LOG_ERR() << "Failed to calculate layer size" << Log::Field("path", layerPath) << Log::Field(err); + } else if (err = space.Resize(size); !err.IsNone()) { + LOG_ERR() << "Failed to resize space" << Log::Field("path", layerPath) << Log::Field(AOS_ERROR_WRAP(err)); + } - if (auto err = CreateBlobPath(descriptor.mDigest, path); !err.IsNone()) { - return err; + if (err = space.Accept(); !err.IsNone()) { + LOG_ERR() << "Failed to accept space" << Log::Field("path", layerPath) << Log::Field(AOS_ERROR_WRAP(err)); } +} - LOG_DBG() << "Blob path" << Log::Field("digest", descriptor.mDigest) << Log::Field("path", path); +void ImageManager::RemoveLayerBlob(const String& path) +{ + LOG_DBG() << "Remove layer blob" << Log::Field("path", path); + // The blob is not needed once the layer is unpacked. The blob that can't be removed stays with its space accepted + // until the layer is processed again or the item is removed. auto [exists, err] = fs::FileExist(path); if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } + LOG_ERR() << "Failed to remove layer blob" << Log::Field("path", path) << Log::Field(AOS_ERROR_WRAP(err)); - if (exists) { - err = ValidateBlob(path, descriptor.mDigest); - if (err.IsNone()) { - return ErrorEnum::eNone; - } + return; + } - err = fs::RemoveAll(path); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove blob" << Log::Field("digest", descriptor.mDigest) << Log::Field(err); - } + if (!exists) { + return; } - StaticString parentPath; + size_t size = 0; - err = fs::ParentPath(path, parentPath); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (Tie(size, err) = fs::CalculateSize(*mAllocator, path); !err.IsNone()) { + LOG_ERR() << "Failed to remove layer blob" << Log::Field("path", path) << Log::Field(AOS_ERROR_WRAP(err)); + + return; } - err = fs::MakeDirAll(parentPath); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (err = fs::RemoveAll(path); !err.IsNone()) { + LOG_ERR() << "Failed to remove layer blob" << Log::Field("path", path) << Log::Field(AOS_ERROR_WRAP(err)); + + return; } - err = DownloadBlob(path, descriptor.mDigest, descriptor.mSize); - if (!err.IsNone()) { - return err; + if (size > 0) { + GetSpaceAllocator().FreeSpace(size); } +} - err = ValidateBlob(path, descriptor.mDigest); - if (!err.IsNone()) { - return err; +Error ImageManager::CheckLayerMediaType(const String& mediaType) +{ + if (mediaType != oci::cMediaTypeLayerTar && mediaType != oci::cMediaTypeLayerTarGZip) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "unsupported layer media type")); } return ErrorEnum::eNone; } -Error ImageManager::CreateLayerMetadata(const String& path, size_t size, spaceallocator::SpaceItf* space) +RetWithError ImageManager::RemoveOrphanLayer(const String& layerPath, const String& diffID) { - auto [digest, err] = mImageHandler->GetUnpackedLayerDigest(fs::JoinPath(path, cUnpackedLayerFolder)); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); + StaticString algorithm; + StaticString hash; + + // The file which doesn't match a valid diff ID is not a layer and is removed. + if (diffID.IsEmpty() || !ParseDigest(diffID, algorithm, hash).IsNone()) { + LOG_WRN() << "Remove invalid layer" << Log::Field("path", layerPath); + + return RemoveLayerFiles(layerPath); + } + + // A locked layer is being processed by an item being installed, so it is in use. + if (!TryLockLayer(diffID)) { + return {0, ErrorEnum::eNone}; } - LOG_DBG() << "Create layer metadata" << Log::Field("path", path) << Log::Field("digest", digest) - << Log::Field("size", size); + auto unlockLayer = DeferRelease([this, &diffID]() { UnlockLayer(diffID); }); - err = space->Resize(space->Size() + digest.Size()); + auto [used, err] = mStorage->IsLayerUsed(diffID); if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); + return {0, AOS_ERROR_WRAP(err)}; } - err = fs::WriteStringToFile(fs::JoinPath(path, cDigestFile), digest, 0600); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (used) { + return {0, ErrorEnum::eNone}; } - StaticString<64> sizeStr; + LOG_DBG() << "Remove orphan layer" << Log::Field("diffID", diffID); - err = sizeStr.Convert(size); - if (!err.IsNone()) { + return RemoveLayerFiles(layerPath); +} + +Error ImageManager::LockLayer(const String& diffID) +{ + UniqueLock lock {mLayersMutex}; + + if (auto err = mLayersCondVar.Wait(lock, + [&]() { + return mNumCancels > 0 + || (!IsLayerInProgress(diffID) && mLayersInProgress.Size() < cMaxNumConcurrentItems); + }); + !err.IsNone()) { return AOS_ERROR_WRAP(err); } - err = space->Resize(space->Size() + sizeStr.Size()); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); + if (mNumCancels > 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eCanceled, "layer lock canceled")); } - err = fs::WriteStringToFile(fs::JoinPath(path, cSizeFile), sizeStr, 0600); - if (!err.IsNone()) { + if (auto err = mLayersInProgress.PushBack(diffID); !err.IsNone()) { return AOS_ERROR_WRAP(err); } return ErrorEnum::eNone; } -Error ImageManager::UnpackLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffDigest) +bool ImageManager::TryLockLayer(const String& diffID) { - LOG_DBG() << "Unpack layer" << Log::Field("diffDigest", diffDigest) << Log::Field("path", path); - - StaticString dstPath; + LockGuard lock {mLayersMutex}; - if (auto err = CreateLayerPath(diffDigest, dstPath); !err.IsNone()) { - return err; + if (IsLayerInProgress(diffID)) { + return false; } - LOG_DBG() << "Layer destination" << Log::Field("diffDigest", diffDigest) << Log::Field("path", dstPath); + if (mLayersInProgress.IsFull()) { + LOG_ERR() << "Layers in progress list is full" << Log::Field("diffID", diffID); - if (auto err = fs::ClearDir(dstPath); !err.IsNone()) { - LOG_ERR() << "Failed to remove layer" << Log::Field("diffDigest", diffDigest) << Log::Field(err); + return false; } - auto [size, err] = mImageHandler->GetUnpackedLayerSize(path, descriptor.mMediaType); - if (!err.IsNone() && !err.Is(ErrorEnum::eNotSupported)) { - LOG_ERR() << "Failed to get unpacked size" << Log::Field("path", path) << Log::Field(err); + if (auto err = mLayersInProgress.PushBack(diffID); !err.IsNone()) { + return false; } - UniquePtr space; + return true; +} - auto releaseSpace = DeferRelease(&err, [this, &diffDigest, &dstPath, &space](const Error* releaseErr) { - if (!releaseErr->IsNone()) { - LOG_ERR() << "Failed to unpack layer" << Log::Field("diffDigest", diffDigest) << Log::Field(*releaseErr); - } +void ImageManager::SetLayersCanceled(bool canceled) +{ + LockGuard lock {mLayersMutex}; - ReleaseSpace(dstPath, space.Get(), *releaseErr); - }); + // Counted, as Cancel may be called concurrently. + if (canceled) { + mNumCancels++; + } else { + mNumCancels--; + } - if (size) { - Tie(space, err) = mSpaceAllocator->AllocateSpace(size); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - err = mImageHandler->UnpackLayer(path, fs::JoinPath(dstPath, cUnpackedLayerFolder), descriptor.mMediaType); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = CreateLayerMetadata(dstPath, size, space.Get()); - if (!err.IsNone()) { - return err; - } - - LOG_DBG() << "Remove packed layer" << Log::Field("path", path); - - err = fs::RemoveAll(path); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove packed layer" << Log::Field("path", path) << Log::Field(err); - } else { - mSpaceAllocator->FreeSpace(descriptor.mSize); - } - - err = space->Resize(space->Size() + diffDigest.Size()); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - LOG_DBG() << "Create diff digest file" << Log::Field("diffDigest", diffDigest) << Log::Field("path", path); - - err = fs::WriteStringToFile(path, diffDigest, 0600); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::InstallLayer( - const oci::ContentDescriptor& descriptor, const String& diffDigest, InstallItem& installItem) -{ - if (auto err = WaitForInstallingBlob(descriptor.mDigest); !err.IsNone()) { - return err; - } - - auto releaseInstalling = DeferRelease(&descriptor.mDigest, [this](const String* digest) { - if (auto releaseErr = ReleaseInstallingBlob(*digest); !releaseErr.IsNone()) { - LOG_ERR() << "Can't release installing blob" << Log::Field("digest", *digest) - << Log::Field(AOS_ERROR_WRAP(releaseErr)); - } - }); - - { - LockGuard lock {mMutex}; - - if (auto err = installItem.mBlobs.PushBack(descriptor.mDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = installItem.mLayers.PushBack(diffDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - LOG_DBG() << "Install layer" << Log::Field("digest", descriptor.mDigest); - - StaticString path; - - if (auto err = CreateBlobPath(descriptor.mDigest, path); !err.IsNone()) { - return err; - } - - auto [exists, err] = fs::FileExist(path); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (exists) { - err = ValidateLayer(path, diffDigest); - if (!err.IsNone()) { - LOG_WRN() << "Layer validation failed" << Log::Field("path", path) << Log::Field("diffDigest", diffDigest) - << Log::Field(err); - } else { - return ErrorEnum::eNone; - } - } - - LOG_DBG() << "Install layer blob" << Log::Field("digest", descriptor.mDigest) - << Log::Field("diffDigest", diffDigest); - - err = InstallBlob(descriptor, nullptr, false); - if (!err.IsNone()) { - return err; - } - - err = UnpackLayer(path, descriptor, diffDigest); - if (!err.IsNone()) { - return err; - } - - return ErrorEnum::eNone; -} - -Error ImageManager::GetBlobURL(const String& digest, String& url) const -{ - StaticArray, 1> digests; - StaticArray, 1> urls; - - if (auto err = digests.EmplaceBack(digest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = mBlobInfoProvider->GetBlobsInfo(digests, urls); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (urls.IsEmpty()) { - return Error(ErrorEnum::eNotFound, "blob URL not found"); - } - - if (auto err = url.Assign(urls[0]); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -void ImageManager::ReleaseSpace(const String& path, spaceallocator::SpaceItf* space, const Error& err) -{ - if (!err.IsNone()) { - if (auto removeErr = fs::RemoveAll(path); !removeErr.IsNone()) { - LOG_ERR() << "Failed to remove path" << Log::Field("path", path) << Log::Field(removeErr); - } - } - - if (space) { - if (!err.IsNone()) { - if (auto spaceErr = space->Release(); !spaceErr.IsNone()) { - LOG_ERR() << "Can't release space" << Log::Field(AOS_ERROR_WRAP(spaceErr)); - } - } else { - if (auto spaceErr = space->Accept(); !spaceErr.IsNone()) { - LOG_ERR() << "Can't accept space" << Log::Field(AOS_ERROR_WRAP(spaceErr)); - } - } - } -} - -Error ImageManager::WaitForInstallingBlob(const String& digest) -{ - UniqueLock lock {mMutex}; - - if (auto err = mCV.Wait(lock, - [&]() { - auto it = mInstallingBlobs.FindIf([&digest](const StaticString& installingDigest) { - return installingDigest == digest; - }); - return it == mInstallingBlobs.end(); - }); - !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = mInstallingBlobs.PushBack(digest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::ReleaseInstallingBlob(const String& digest) -{ - UniqueLock lock {mMutex}; - - auto it = mInstallingBlobs.FindIf( - [&digest](const StaticString& installingDigest) { return installingDigest == digest; }); - if (it != mInstallingBlobs.end()) { - (void)mInstallingBlobs.Erase(it); - } else { - return AOS_ERROR_WRAP(ErrorEnum::eNotFound); - } - - if (auto err = mCV.NotifyAll(); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -RetWithError ImageManager::RemoveOldItemVersions(Array& itemData) -{ - if (itemData.Size() < cMaxNumItemVersions) { - return 0; - } - - LOG_DBG() << "Remove old item versions" << Log::Field("itemID", itemData[0].mID); - - return RemoveOldUpdateItems(itemData); -} - -Error ImageManager::InstallServiceLayers(const oci::ImageManifest& manifest, InstallItem& installItem) -{ - StaticString path; - - LOG_DBG() << "Install image config blob" << Log::Field("digest", manifest.mConfig.mDigest); - - if (auto err = InstallBlob(manifest.mConfig, &installItem); !err.IsNone()) { - return err; - } - - auto config = MakeUnique(mAllocator); - if (!config) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = CreateBlobPath(manifest.mConfig.mDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = mOCISpec->LoadImageConfig(path, *config); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - for (size_t i = 0; i < manifest.mLayers.Size(); ++i) { - const auto& layer = manifest.mLayers[i]; - - if (i >= config->mRootfs.mDiffIDs.Size()) { - return AOS_ERROR_WRAP(Error(ErrorEnum::eOutOfRange, "diff IDs size is less than layers size")); - } - - if (auto err = InstallLayer(layer, config->mRootfs.mDiffIDs[i], installItem); !err.IsNone()) { - return err; - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::InstallComponentLayers(const oci::ImageManifest& manifest, InstallItem& installItem) -{ - for (const auto& layer : manifest.mLayers) { - LOG_DBG() << "Install layer blob" << Log::Field("digest", layer.mDigest); - - if (auto err = InstallBlob(layer, &installItem); !err.IsNone()) { - return err; - } - } - - return ErrorEnum::eNone; -} - -RetWithError::Iterator> ImageManager::CreateInstallingItem( - const UpdateItemInfo& itemInfo) -{ - LockGuard lock {mMutex}; - - if (auto err = mInstallingItems.EmplaceBack(); !err.IsNone()) { - return {mInstallingItems.end(), AOS_ERROR_WRAP(err)}; - } - - mInstallingItems.Back().mID = itemInfo.mID; - mInstallingItems.Back().mVersion = itemInfo.mVersion; - - auto it = mInstallingItems.FindIf( - [&](const InstallItem& item) { return item.mID == itemInfo.mID && item.mVersion == itemInfo.mVersion; }); - - if (it == mInstallingItems.end()) { - return {it, AOS_ERROR_WRAP(ErrorEnum::eNotFound)}; - } - - return it; -} - -void ImageManager::ReleaseInstallingItem(List::Iterator it) -{ - LockGuard lock {mMutex}; - - (void)mInstallingItems.Erase(it); - - if (mInstallingItems.IsEmpty()) { - if (auto err = mCV.NotifyAll(); !err.IsNone()) { - LOG_ERR() << "Can't notify image manager" << Log::Field(AOS_ERROR_WRAP(err)); - } - } -} - -RetWithError ImageManager::CropUpdateItems() -{ - auto [itemsCount, err] = mStorage->GetUpdateItemsCount(); - if (!err.IsNone()) { - return {0, AOS_ERROR_WRAP(err)}; - } - - if (itemsCount < cMaxNumStoredUpdateItems) { - return 0; - } - - LOG_DBG() << "Crop update items"; - - auto itemsData = MakeUnique(mAllocator); - if (!itemsData) { - return {0, AOS_ERROR_WRAP(ErrorEnum::eNoMemory)}; - } - - err = mStorage->GetAllUpdateItems(*itemsData); - if (!err.IsNone()) { - return {0, AOS_ERROR_WRAP(err)}; - } - - return RemoveOldUpdateItems(*itemsData); -} - -Error ImageManager::StoreUpdateItem(const UpdateItemInfo& itemInfo) -{ - LockGuard lock {mMutex}; - - StaticArray itemData; - Error err; - - err = mStorage->GetUpdateItem(itemInfo.mID, itemData); - if (!err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { - return AOS_ERROR_WRAP(err); - } - - if (auto it = itemData.FindIf([&](const UpdateItemData& data) { return data.mVersion == itemInfo.mVersion; }); - it != itemData.end()) { - err = mStorage->UpdateUpdateItem(UpdateItemData {itemInfo.mID, itemInfo.mType, itemInfo.mVersion, - itemInfo.mManifestDigest, ItemStateEnum::eInstalled, Time::Now()}); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; - } - - size_t removedItems = 0; - - Tie(removedItems, err) = RemoveOldItemVersions(itemData); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove old item versions" << Log::Field("itemID", itemInfo.mID) << Log::Field(err); - } - - if (!removedItems) { - Tie(removedItems, err) = CropUpdateItems(); - if (!err.IsNone()) { - return err; - } - } - - err = mStorage->AddUpdateItem(UpdateItemData {itemInfo.mID, itemInfo.mType, itemInfo.mVersion, - itemInfo.mManifestDigest, ItemStateEnum::eInstalled, Time::Now()}); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (removedItems) { - size_t removedSize = 0; - - Tie(removedSize, err) = RemoveOrphans(); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove orphans" << Log::Field(err); - } - - mSpaceAllocator->FreeSpace(removedSize); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::RemoveUpdateItem(const UpdateItemData& itemData) -{ - LOG_INF() << "Remove update item from storage" << Log::Field("itemID", itemData.mID) - << Log::Field("version", itemData.mVersion) << Log::Field("state", itemData.mState); - - if (auto err = mStorage->RemoveUpdateItem(itemData.mID, itemData.mVersion); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (itemData.mState == ItemStateEnum::eRemoved) { - if (auto err = mSpaceAllocator->RestoreOutdatedItem(itemData.mID, itemData.mVersion); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - return ErrorEnum::eNone; -} - -RetWithError ImageManager::RemoveOldUpdateItems(Array& itemsData) -{ - LOG_DBG() << "Remove old update items"; - - itemsData.Sort([](const UpdateItemData& a, const UpdateItemData& b) { return a.mTimestamp < b.mTimestamp; }); - - Error err; - size_t removed = 0; - - for (const auto& data : itemsData) { - if (data.mState == ItemStateEnum::eRemoved) { - if (auto removeErr = RemoveUpdateItem(data); !removeErr.IsNone() && err.IsNone()) { - err = removeErr; - } else { - removed++; - } - - removed++; - - break; - } - } - - if (!removed) { - if (auto removeErr = RemoveUpdateItem(itemsData[0]); !removeErr.IsNone() && err.IsNone()) { - err = removeErr; - } else { - removed++; - } + if (auto err = mLayersCondVar.NotifyAll(); !err.IsNone()) { + LOG_ERR() << "Can't notify condition variable" << Log::Field(err); } - - return {removed, err}; } -Error ImageManager::UpdateOutdatedItems() +void ImageManager::UnlockLayer(const String& diffID) { - LOG_DBG() << "Update outdated items"; - - auto itemsData = MakeUnique(mAllocator); - if (!itemsData) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = mStorage->GetAllUpdateItems(*itemsData); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - for (const auto& itemData : *itemsData) { - if (itemData.mState != ItemStateEnum::eRemoved) { - continue; - } - - if (auto err = mSpaceAllocator->AddOutdatedItem(itemData.mID, itemData.mVersion, itemData.mTimestamp); - !err.IsNone()) { - LOG_ERR() << "Failed to add outdated item" << Log::Field("itemID", itemData.mID) << Log::Field(err); - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::ValidateUpdateItem(const UpdateItemData& itemData) -{ - LOG_DBG() << "Validate update item" << Log::Field("itemID", itemData.mID) - << Log::Field("version", itemData.mVersion); - - StaticString path; - - if (auto err = CreateBlobPath(itemData.mManifestDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = ValidateBlob(path, itemData.mManifestDigest); !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 = CreateBlobPath(manifest->mItemConfig->mDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = ValidateBlob(path, manifest->mItemConfig->mDigest); !err.IsNone()) { - return err; - } - } - - if (itemData.mType == UpdateItemTypeEnum::eService) { - if (auto err = CreateBlobPath(manifest->mConfig.mDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = ValidateBlob(path, manifest->mConfig.mDigest); !err.IsNone()) { - return err; - } - - auto config = MakeUnique(mAllocator); - if (!config) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = mOCISpec->LoadImageConfig(path, *config); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - for (size_t i = 0; i < manifest->mLayers.Size(); ++i) { - const auto& layer = manifest->mLayers[i]; - - if (i >= config->mRootfs.mDiffIDs.Size()) { - return AOS_ERROR_WRAP(Error(ErrorEnum::eOutOfRange, "diff IDs size is less than layers size")); - } - - if (auto err = CreateBlobPath(layer.mDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = ValidateLayer(path, config->mRootfs.mDiffIDs[i]); !err.IsNone()) { - return err; - } - } - } else { - for (const auto& layer : manifest->mLayers) { - if (auto err = CreateBlobPath(layer.mDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = ValidateBlob(path, layer.mDigest); !err.IsNone()) { - return err; - } - } - } + LockGuard lock {mLayersMutex}; - return ErrorEnum::eNone; -} - -Error ImageManager::HandleOutdatedItems() -{ - LOG_DBG() << "Handle outdated items"; - - auto itemsData = MakeUnique(mAllocator); - if (!itemsData) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = mStorage->GetAllUpdateItems(*itemsData); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto now = Time::Now(); - - for (const auto& itemData : *itemsData) { - if (itemData.mState != ItemStateEnum::eRemoved || now.Sub(itemData.mTimestamp) < mConfig.mUpdateItemTTL) { - continue; - } - - if (auto err = RemoveUpdateItem(itemData); !err.IsNone()) { - LOG_ERR() << "Failed to remove outdated item" << Log::Field("itemID", itemData.mID) - << Log::Field("version", itemData.mVersion) << Log::Field(err); - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::HandleItemsIntegrity() -{ - LOG_DBG() << "Handle items integrity"; - - auto itemsData = MakeUnique(mAllocator); - if (!itemsData) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = mStorage->GetAllUpdateItems(*itemsData); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - for (const auto& itemData : *itemsData) { - if (itemData.mState != ItemStateEnum::eInstalled) { - continue; - } - - if (auto err = ValidateUpdateItem(itemData); !err.IsNone()) { - LOG_ERR() << "Update item integrity error" << Log::Field("itemID", itemData.mID) - << Log::Field("version", itemData.mVersion) << Log::Field(err); - - err = RemoveUpdateItem(itemData); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove invalid item" << Log::Field("itemID", itemData.mID) - << Log::Field("version", itemData.mVersion) << Log::Field(err); - } - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::AddInstallingItems( - Array>& usedBlobs, Array>& usedLayers) -{ - for (const auto& installingItem : mInstallingItems) { - StaticString path; - - for (const auto& blob : installingItem.mBlobs) { - if (auto err = CreateBlobPath(blob, path); !err.IsNone()) { - return err; - } - - if (auto err = AddPathIfNotExist(usedBlobs, path); !err.IsNone()) { - return err; - } - } - - for (const auto& layer : installingItem.mLayers) { - if (auto err = CreateLayerPath(layer, path); !err.IsNone()) { - return err; - } - - if (auto err = AddPathIfNotExist(usedLayers, path); !err.IsNone()) { - return err; - } - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::CalcItemBlobsAndLayers(const UpdateItemData& itemData, Array>& itemBlobs, - Array>& itemLayers) -{ - LOG_DBG() << "Calculate item blobs and layers" << Log::Field("itemID", itemData.mID) - << Log::Field("version", itemData.mVersion); - - StaticString path; - - if (auto err = CreateBlobPath(itemData.mManifestDigest, 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 (auto err = AddPathIfNotExist(itemBlobs, path); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (manifest->mItemConfig.HasValue()) { - if (auto err = CreateBlobPath(manifest->mItemConfig->mDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = AddPathIfNotExist(itemBlobs, path); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - if (itemData.mType == UpdateItemTypeEnum::eService) { - if (auto err = CreateBlobPath(manifest->mConfig.mDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = AddPathIfNotExist(itemBlobs, path); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - auto config = MakeUnique(mAllocator); - if (!config) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - if (auto err = mOCISpec->LoadImageConfig(path, *config); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - for (size_t i = 0; i < manifest->mLayers.Size(); ++i) { - const auto& layer = manifest->mLayers[i]; - - if (auto err = CreateBlobPath(layer.mDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = AddPathIfNotExist(itemBlobs, path); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - for (const auto& diffID : config->mRootfs.mDiffIDs) { - if (auto err = CreateLayerPath(diffID, path); !err.IsNone()) { - return err; - } - - if (auto err = AddPathIfNotExist(itemLayers, path); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - } else { - for (const auto& layer : manifest->mLayers) { - if (auto err = CreateBlobPath(layer.mDigest, path); !err.IsNone()) { - return err; - } - - if (auto err = AddPathIfNotExist(itemBlobs, path); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - } - - return ErrorEnum::eNone; -} - -RetWithError ImageManager::RemoveOrphanBlobs(const Array>& usedBlobs) -{ - size_t removedSize = 0; - auto srcPath = fs::JoinPath(mConfig.mImagePath, cBlobsFolder); - auto algIterator = fs::DirIterator(srcPath); - - LOG_DBG() << "Remove orphans blobs" << Log::Field("usedBlobsCount", usedBlobs.Size()) - << Log::Field("path", srcPath); - - while (algIterator.Next()) { - auto blobsPath = fs::JoinPath(srcPath, algIterator->mPath); - auto blobIterator = fs::DirIterator(blobsPath); - - LOG_DBG() << "Process blob algorithm folder" << Log::Field("path", blobsPath); - - while (blobIterator.Next()) { - auto blobPath = fs::JoinPath(blobsPath, blobIterator->mPath); - - if (auto it = usedBlobs.Find(blobPath); it != usedBlobs.end()) { - continue; - } - - auto [blobSize, err] = fs::CalculateSize(*mAllocator, blobPath); - if (!err.IsNone()) { - LOG_ERR() << "Failed to get blob size" << Log::Field("path", blobPath) << Log::Field(err); - } else { - removedSize += blobSize; - } - - LOG_DBG() << "Remove orphaned blob" << Log::Field("path", blobPath) << Log::Field("size", blobSize); - - err = fs::RemoveAll(blobPath); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove orphaned blob" << Log::Field(err); - } - } - } - - return removedSize; -} - -RetWithError ImageManager::RemoveOrphanLayers(const Array>& usedLayers) -{ - size_t removedSize = 0; - auto srcPath = fs::JoinPath(mConfig.mImagePath, cLayersFolder); - auto algIterator = fs::DirIterator(srcPath); - - LOG_DBG() << "Remove orphans layers" << Log::Field("usedLayersCount", usedLayers.Size()) - << Log::Field("path", srcPath); - - while (algIterator.Next()) { - auto layersPath = fs::JoinPath(srcPath, algIterator->mPath); - auto layerIterator = fs::DirIterator(layersPath); - - LOG_DBG() << "Process layer algorithm folder" << Log::Field("path", layersPath); - - while (layerIterator.Next()) { - - auto layerPath = fs::JoinPath(layersPath, layerIterator->mPath); - - if (auto it = usedLayers.Find(layerPath); it != usedLayers.end()) { - continue; - } - - auto [layerSize, err] = fs::CalculateSize(*mAllocator, layerPath); - if (!err.IsNone()) { - LOG_ERR() << "Failed to get layer size" << Log::Field("path", layerPath) << Log::Field(err); - } else { - removedSize += layerSize; - } - - LOG_DBG() << "Remove orphaned layer" << Log::Field("path", layerPath) << Log::Field("size", layerSize); - - err = fs::RemoveAll(layerPath); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove orphaned layer" << Log::Field(err); - } - } - } - - return removedSize; -} - -RetWithError ImageManager::RemoveOrphans() -{ - LOG_DBG() << "Remove orphans"; - - auto itemsData = MakeUnique(mAllocator); - if (!itemsData) { - return {0, AOS_ERROR_WRAP(ErrorEnum::eNoMemory)}; - } - - if (auto err = mStorage->GetAllUpdateItems(*itemsData); !err.IsNone()) { - return {0, AOS_ERROR_WRAP(err)}; - } - - auto usedBlobs = MakeUnique, cMaxNumInstalledBlobs>>(mAllocator); - if (!usedBlobs) { - return {0, AOS_ERROR_WRAP(ErrorEnum::eNoMemory)}; - } - - auto usedLayers = MakeUnique, cMaxNumInstalledLayers>>(mAllocator); - if (!usedLayers) { - return {0, AOS_ERROR_WRAP(ErrorEnum::eNoMemory)}; - } - - if (auto err = AddInstallingItems(*usedBlobs, *usedLayers); !err.IsNone()) { - LOG_ERR() << "Failed to add installing items" << Log::Field(err); - } - - for (const auto& itemData : *itemsData) { - if (auto err = CalcItemBlobsAndLayers(itemData, *usedBlobs, *usedLayers); !err.IsNone()) { - LOG_ERR() << "Failed to calculate item blobs and layers" << Log::Field("itemID", itemData.mID) - << Log::Field("version", itemData.mVersion) << Log::Field(err); - } - } - - size_t removedSize = 0; - size_t size = 0; - Error err; - - Tie(size, err) = RemoveOrphanBlobs(*usedBlobs); - if (!err.IsNone()) { - LOG_ERR() << "Remove orphan blobs failed" << Log::Field(err); - } - - removedSize += size; - - Tie(size, err) = RemoveOrphanLayers(*usedLayers); - if (!err.IsNone()) { - LOG_ERR() << "Remove orphan layers failed" << Log::Field(err); - } - - removedSize += size; - - return removedSize; -} - -void ImageManager::ProcessOutdatedItems() -{ - while (true) { - UniqueLock lock {mMutex}; - - if (auto err - = mCV.Wait(lock, [&]() { return (mClose || mProcessOutdatedItems) && mInstallingItems.IsEmpty(); }); - !err.IsNone()) { - LOG_ERR() << "Wait failed" << Log::Field(err); - continue; - } - - if (mClose) { - break; - } - - if (!mProcessOutdatedItems) { - continue; - } - - mProcessOutdatedItems = false; - - if (auto err = HandleOutdatedItems(); !err.IsNone()) { - LOG_ERR() << "Can't handle outdated items" << Log::Field(err); - } - - if (auto err = HandleItemsIntegrity(); !err.IsNone()) { - LOG_ERR() << "Can't handle items integrity" << Log::Field(err); - } - - auto [size, err] = RemoveOrphans(); - if (!err.IsNone()) { - LOG_ERR() << "Remove orphans failed" << Log::Field(err); - } + (void)mLayersInProgress.RemoveIf([&diffID](const String& layer) { return layer == diffID; }); - mSpaceAllocator->FreeSpace(size); + if (auto err = mLayersCondVar.NotifyAll(); !err.IsNone()) { + LOG_ERR() << "Can't notify condition variable" << Log::Field(err); } } diff --git a/src/core/sm/imagemanager/imagemanager.hpp b/src/core/sm/imagemanager/imagemanager.hpp index fc1e9f330..ab00955c9 100644 --- a/src/core/sm/imagemanager/imagemanager.hpp +++ b/src/core/sm/imagemanager/imagemanager.hpp @@ -8,21 +8,12 @@ #define AOS_CORE_SM_IMAGEMANAGER_IMAGEMANAGER_HPP_ #include -#include -#include -#include -#include -#include -#include - -#include "itf/blobinfoprovider.hpp" +#include + #include "itf/imagehandler.hpp" #include "itf/imagemanager.hpp" -#include "itf/iteminfoprovider.hpp" #include "itf/storage.hpp" -#include "config.hpp" - namespace aos::sm::imagemanager { /** @addtogroup sm Service Manager @@ -30,162 +21,117 @@ namespace aos::sm::imagemanager { */ /** - * Image manager. + * SM image manager. + * + * Downloads deployable items from the CM and unpacks their layers. A layer blob is removed once the layer is unpacked. + * The layers of the items without an image config are kept as packed blobs only. */ -class ImageManager : public ImageManagerItf, public ItemInfoProviderItf, public spaceallocator::ItemRemoverItf { +class ImageManager : public common::imagemanager::ImageManager, public ImageManagerItf { public: /** * Initializes image manager. * - * @param allocator allocator to use for temporary objects. * @param config image manager config. - * @param blobInfoProvider blob info provider. - * @param spaceAllocator space allocator. - * @param downloader downloader. + * @param blobInfoProvider provides download info of the blobs from the CM. + * @param storage image manager storage. + * @param spaceAllocator space allocator of the blobs and the unpacked layers. + * @param downloader downloads blobs. + * @param imageHandler unpacks layers. * @param fileInfoProvider file info provider. * @param ociSpec OCI spec interface. - * @param imageHandler image handler. - * @param storage image manager storage. - * @return Error. - */ - Error Init(AllocatorItf& allocator, const Config& config, BlobInfoProviderItf& blobInfoProvider, - spaceallocator::SpaceAllocatorItf& spaceAllocator, downloader::DownloaderItf& downloader, - fs::FileInfoProviderItf& fileInfoProvider, oci::OCISpecItf& ociSpec, ImageHandlerItf& imageHandler, - StorageItf& storage); - - /** - * Starts image manager. - * - * @return Error. + * @param fsPlatform file system platform interface. + * @param allocator allocator for temporary objects. + * @return Error: eInvalidArgument if the image path is too long to hold the longest blob or layer path. */ - Error Start(); + Error Init(const common::imagemanager::Config& config, common::imagemanager::BlobInfoProviderItf& blobInfoProvider, + StorageItf& storage, spaceallocator::SpaceAllocatorItf& spaceAllocator, downloader::DownloaderItf& downloader, + ImageHandlerItf& imageHandler, fs::FileInfoProviderItf& fileInfoProvider, oci::OCISpecItf& ociSpec, + fs::FSPlatformItf& fsPlatform, AllocatorItf& allocator); /** - * Stops image manager. - * - * @return Error. - */ - Error Stop(); - - /** - * Returns all installed update items statuses. - * - * @param statuses list of installed update item statuses. - * @return Error. - */ - Error GetAllInstalledItems(Array& statuses) const override; - - /** - * Installs update item. + * Installs update item. Calling it for an installed item validates the item and repairs it if needed. * * @param itemInfo update item info. * @return Error. */ - Error InstallUpdateItem(const UpdateItemInfo& itemInfo) override; + Error InstallItem(const UpdateItemInfo& itemInfo) override; /** - * Removes update item. + * Removes update item. The item is kept in the cache as an outdated item, so its blobs and unpacked layers are + * reused if the item is installed again. * * @param itemID update item ID. * @param version update item version. * @return Error. */ - Error RemoveUpdateItem(const String& itemID, const String& version) override; + Error RemoveItem(const String& itemID, const String& version) override; /** - * Returns blob path by its digest. + * Cancels all items being installed, including the items waiting for a layer processed by another item. 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 layer path by its digest. + * Returns unpacked layer path. It doesn't validate the unpacked layer. The unpacked layer of an item in the + * eRemoved state may be removed at any time, so the consumer must request the path only for the layers of the items + * it uses. * - * @param digest layer digest. - * @param[out] path result layer path. - * @return Error. + * @param diffID layer diff ID. + * @param[out] path unpacked layer path. + * @return Error: eInvalidArgument if the diff ID is malformed, eNotFound if the unpacked layer doesn't exist. */ - Error GetLayerPath(const String& digest, String& path) const override; + Error GetLayerPath(const String& diffID, String& path) const override; private: - static constexpr auto cBlobsFolder = "blobs"; static constexpr auto cLayersFolder = "layers"; static constexpr auto cUnpackedLayerFolder = "layer"; static constexpr auto cDigestFile = "digest"; static constexpr auto cSizeFile = "size"; - static constexpr auto cMaxNumItemVersions = 2; - // oci::cMaxNumLayers + 3 (layers + manifest + image config + aos service) - static constexpr auto cMaxNumItemBlobs = oci::cMaxNumLayers + 3; - static constexpr auto cMaxNumInstalledBlobs = cMaxNumUpdateItems * cMaxNumItemBlobs; - static constexpr auto cMaxNumInstalledLayers = cMaxNumUpdateItems * oci::cMaxNumLayers; - struct InstallItem { - StaticString mID; - StaticString mVersion; - StaticArray, cMaxNumItemBlobs> mBlobs; - StaticArray, oci::cMaxNumLayers> mLayers; - }; - - RetWithError RemoveItem(const String& id, const String& version) override; - - Error CreateBlobPath(const String& digest, String& path) const; - Error CreateLayerPath(const String& digest, String& path) const; - Error ValidateBlob(const String& path, const String& digest) const; - Error DownloadBlob(const String& path, const String& digest, size_t size); - Error InstallBlob( - const oci::ContentDescriptor& descriptor, InstallItem* installItem = nullptr, bool waitInstalling = true); - Error ValidateLayer(const String& path, const String& diffDigest) const; - Error CreateLayerMetadata(const String& path, size_t size, spaceallocator::SpaceItf* space); - Error UnpackLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffDigest); - Error InstallLayer(const oci::ContentDescriptor& descriptor, const String& diffDigest, InstallItem& installItem); - Error GetBlobURL(const String& digest, String& url) const; - void ReleaseSpace(const String& path, spaceallocator::SpaceItf* space, const Error& err); - Error WaitForInstallingBlob(const String& digest); - Error ReleaseInstallingBlob(const String& digest); - RetWithError::Iterator> CreateInstallingItem(const UpdateItemInfo& itemInfo); - void ReleaseInstallingItem(List::Iterator it); - Error InstallServiceLayers(const oci::ImageManifest& manifest, InstallItem& installItem); - Error InstallComponentLayers(const oci::ImageManifest& manifest, InstallItem& installItem); - Error AddNewUpdateItem(const UpdateItemInfo& itemInfo); - Error StoreUpdateItem(const UpdateItemInfo& itemInfo); - Error RemoveUpdateItem(const UpdateItemData& itemData); - RetWithError RemoveOldUpdateItems(Array& itemsData); - RetWithError RemoveOldItemVersions(Array& itemData); - RetWithError CropUpdateItems(); - Error UpdateOutdatedItems(); - Error HandleOutdatedItems(); - Error HandleItemsIntegrity(); - Error AddInstallingItems( - Array>& usedBlobs, Array>& usedLayers); - Error CalcItemBlobsAndLayers(const UpdateItemData& itemData, Array>& itemBlobs, - Array>& itemLayers); - RetWithError RemoveOrphanBlobs(const Array>& usedBlobs); - RetWithError RemoveOrphanLayers(const Array>& usedLayers); - RetWithError RemoveOrphans(); - Error ValidateUpdateItem(const UpdateItemData& itemData); - void ProcessOutdatedItems(); - - Config mConfig; - BlobInfoProviderItf* mBlobInfoProvider {}; - spaceallocator::SpaceAllocatorItf* mSpaceAllocator {}; - downloader::DownloaderItf* mDownloader {}; - fs::FileInfoProviderItf* mFileInfoProvider {}; - oci::OCISpecItf* mOCISpec {}; - ImageHandlerItf* mImageHandler {}; - StorageItf* mStorage {}; - - AllocatorItf* mAllocator {}; - - Timer mTimer; - mutable Mutex mMutex; - ConditionalVariable mCV; - StaticList mInstallingItems; - StaticList, cMaxNumConcurrentItems> mInstallingBlobs; - Thread<> mThread; - bool mClose {}; - bool mProcessOutdatedItems {}; + // Extra slots for the item remover and the cleanup, which don't wait for the layers. + static constexpr auto cMaxNumLayersInProgress = cMaxNumConcurrentItems + 2; + + Error DownloadBlob(CancelContext& ctx, const String& path, const BlobInfo& blobInfo) override; + Error ValidateLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) override; + Error UnpackLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) override; + RetWithError RemoveLayer( + const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) override; + RetWithError RemoveOrphanLayers() override; + + Error CreateLayerPath(const String& diffID, String& path) const; + Error ValidateUnpackedLayer(const String& layerPath) const; + Error InstallUnpackedLayer(const String& path, const String& mediaType, const String& layerPath); + Error UnpackToLayerPath(const String& path, const String& mediaType, const String& layerPath, size_t unpackedSize, + spaceallocator::SpaceItf& space); + Error CreateLayerMetadata(const String& layerPath, size_t unpackedSize); + RetWithError RemoveLayerFiles(const String& layerPath); + void AcceptRemainingSpace(const String& layerPath, spaceallocator::SpaceItf& space); + void RemoveLayerBlob(const String& path); + static Error CheckLayerMediaType(const String& mediaType); + RetWithError RemoveOrphanLayer(const String& layerPath, const String& diffID); + + Error LockLayer(const String& diffID); + bool TryLockLayer(const String& diffID); + void UnlockLayer(const String& diffID); + void SetLayersCanceled(bool canceled); + + bool IsLayerInProgress(const String& diffID) const + { + return mLayersInProgress.Find(diffID) != mLayersInProgress.end(); + } + + StaticString mLayersPath; + downloader::DownloaderItf* mDownloader {}; + ImageHandlerItf* mImageHandler {}; + StorageItf* mStorage {}; + AllocatorItf* mAllocator {}; + + Mutex mLayersMutex; + ConditionalVariable mLayersCondVar; + StaticList, cMaxNumLayersInProgress> mLayersInProgress; + size_t mNumCancels {}; }; /** @}*/ diff --git a/src/core/sm/imagemanager/imagemanager.md b/src/core/sm/imagemanager/imagemanager.md index cc7b0145e..d636a8c07 100644 --- a/src/core/sm/imagemanager/imagemanager.md +++ b/src/core/sm/imagemanager/imagemanager.md @@ -1,16 +1,55 @@ # Image manager -Image manager installs, removes and manages update items on SM side. +Image manager installs, removes and manages update items on SM side. It downloads the items from the CM and unpacks +their layers. A layer blob is removed once its layer is unpacked, only the unpacked layer is kept. + +It is derived from the [common image manager](../../common/imagemanager/imagemanager.md), which installs and removes a +single version of an item, stores the items and their blobs, removes the outdated items and the orphan blobs, and +provides the item statuses. The SM image manager adds the following: + +* converts `UpdateItemInfo` to the common `InstallItemInfo`. `mIsRootIndex` is `false`, as the SM manages a dedicated + architecture only and processes the item blobs from the image manifest; +* downloads a blob from the URLs in the `BlobInfo` provided by `smclient`. The download error, for example `eIntegrity` + of a broken blob on the CM, is returned as is. The downloaded blob is validated by the common image manager; +* unpacks, validates and removes the layers identified by the `diff_id`. A layer without a `diff_id`, i.e. of an item + without an image config, is kept as a packed blob only. Only the `tar` and `tar+gzip` layer media types are + supported; +* removes the layer blob and frees its space after the layer is unpacked, or when `UnpackLayer` finds the layer already + valid. The blob reference stays in the storage, so a repair downloads the blob again if the unpacked layer is broken. + A blob that can't be removed is logged and kept with its space accepted until the layer is processed again or the + item is removed. On a failed unpack, the blob is kept; +* removes the orphan unpacked layers; +* provides the paths of the unpacked layers. + +The unpacked layers are stored in `/layers//`: + +* `layer` - unpacked layer content, returned by `GetLayerPath`; +* `digest` - digest of the unpacked content, used to validate the layer; +* `size` - unpacked size reported by the image handler. + +`digest` and `size` are written last, so a layer without them is not completely unpacked. + +Different layer blobs, for example compressed in different ways, can be unpacked to the same layer. So the processing of +the layers with the same `diff_id` is serialized in `ValidateLayer` and `UnpackLayer`, `UnpackLayer` skips a layer that +is already valid, and an unpacked layer is removed only if `IsLayerUsed` of the storage returns `false`. `RemoveLayer` +and the orphan layers cleanup never wait for a `diff_id`: they may be called from the item remover, so a layer being +processed is skipped. It implements the following interfaces: -* [aos::sm::imagemanager::ImageManagerItf](itf/imagemanager.hpp) - installs, removes update items; -* [aos::sm::imagemanager::ItemInfoProviderItf](itf/iteminfoprovider.hpp) - provides info about installed blobs, layers. +* [aos::sm::imagemanager::ImageManagerItf](itf/imagemanager.hpp) - installs, removes update items, cancels the install; +* [aos::common::imagemanager::ItemInfoProviderItf](../../common/imagemanager/itf/iteminfoprovider.hpp) - provides info + about installed blobs, layers; +* [aos::common::imagemanager::ItemStatusProviderItf](../../common/imagemanager/itf/itemstatusprovider.hpp) - provides + item statuses. It requires the following interfaces: * [aos::sm::imagemanager::ImageHandlerItf](itf/imagehandler.hpp) - performs platform dependant layers unpacking; -* [aos::sm::imagemanager::BlobInfoProviderItf](itf/blobinfoprovider.hpp) - gets blob information (URL). +* [aos::sm::imagemanager::StorageItf](itf/storage.hpp) - extends the common storage with `IsLayerUsed`; +* [aos::common::imagemanager::BlobInfoProviderItf](../../common/imagemanager/itf/blobinfoprovider.hpp) - gets blob + download info; +* [aos::downloader::DownloaderItf](../../common/downloader/itf/downloader.hpp) - downloads blobs. ```mermaid classDiagram @@ -19,11 +58,18 @@ classDiagram class ImageManager ["aos::sm::imagemanager::ImageManager"] { } + class CommonImageManager ["aos::common::imagemanager::ImageManager"] { + } + class ImageManagerItf ["aos::sm::imagemanager::ImageManagerItf"] { <> } - class ItemInfoProviderItf ["aos::sm::imagemanager::ItemInfoProviderItf"] { + class ItemInfoProviderItf ["aos::common::imagemanager::ItemInfoProviderItf"] { + <> + } + + class ItemStatusProviderItf ["aos::common::imagemanager::ItemStatusProviderItf"] { <> } @@ -31,13 +77,25 @@ classDiagram <> } - class BlobInfoProviderItf ["aos::sm::imagemanager::BlobInfoProviderItf"] { + class StorageItf ["aos::sm::imagemanager::StorageItf"] { + <> + } + + class BlobInfoProviderItf ["aos::common::imagemanager::BlobInfoProviderItf"] { + <> + } + + class DownloaderItf ["aos::downloader::DownloaderItf"] { <> } + ImageManager --|> CommonImageManager ImageManager ..|> ImageManagerItf - ImageManager ..|> ItemInfoProviderItf + CommonImageManager ..|> ItemInfoProviderItf + CommonImageManager ..|> ItemStatusProviderItf ImageManager ..> ImageHandlerItf - ImageManager ..> BlobInfoProviderItf + ImageManager ..> StorageItf + ImageManager ..> DownloaderItf + CommonImageManager ..> BlobInfoProviderItf ``` diff --git a/src/core/sm/imagemanager/itf/blobinfoprovider.hpp b/src/core/sm/imagemanager/itf/blobinfoprovider.hpp deleted file mode 100644 index 406ece969..000000000 --- a/src/core/sm/imagemanager/itf/blobinfoprovider.hpp +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_SM_IMAGEMANAGER_ITF_BLOBINFOPROVIDER_HPP_ -#define AOS_CORE_SM_IMAGEMANAGER_ITF_BLOBINFOPROVIDER_HPP_ - -#include -#include - -namespace aos::sm::imagemanager { - -/** @addtogroup sm Service Manager - * @{ - */ - -/** - * Interface that provides blob info. - */ -class BlobInfoProviderItf { -public: - /** - * Destructor. - */ - virtual ~BlobInfoProviderItf() = default; - - /** - * Returns blobs info. - * - * @param digests list of blob digests. - * @param[out] urls blobs URLs. - * @return Error. - */ - virtual Error GetBlobsInfo(const Array>& digests, Array>& urls) - = 0; -}; - -/** @}*/ - -} // namespace aos::sm::imagemanager - -#endif diff --git a/src/core/sm/imagemanager/itf/imagemanager.hpp b/src/core/sm/imagemanager/itf/imagemanager.hpp index 3669828ff..5447b38ec 100644 --- a/src/core/sm/imagemanager/itf/imagemanager.hpp +++ b/src/core/sm/imagemanager/itf/imagemanager.hpp @@ -46,35 +46,6 @@ struct UpdateItemInfo { friend bool operator!=(const UpdateItemInfo& lhs, const UpdateItemInfo& rhs) { return !(lhs == rhs); }; }; -/** - * Update item status. - */ -struct UpdateItemStatus { - StaticString mID; - UpdateItemType mType; - StaticString mVersion; - ItemState mState; - - /** - * Compares update item status. - * - * @param rhs status to compare. - * @return bool. - */ - friend bool operator==(const UpdateItemStatus& lhs, const UpdateItemStatus& rhs) - { - return lhs.mID == rhs.mID && lhs.mType == rhs.mType && lhs.mVersion == rhs.mVersion && lhs.mState == rhs.mState; - }; - - /** - * Compares update item status. - * - * @param rhs status to compare. - * @return bool. - */ - friend bool operator!=(const UpdateItemStatus& lhs, const UpdateItemStatus& rhs) { return !(lhs == rhs); }; -}; - /** * Image manager interface. */ @@ -85,21 +56,13 @@ class ImageManagerItf { */ virtual ~ImageManagerItf() = default; - /** - * Returns all installed update items statuses. - * - * @param statuses list of installed update item statuses. - * @return Error. - */ - virtual Error GetAllInstalledItems(Array& statuses) const = 0; - /** * Installs update item. * * @param itemInfo update item info. * @return Error. */ - virtual Error InstallUpdateItem(const UpdateItemInfo& itemInfo) = 0; + virtual Error InstallItem(const UpdateItemInfo& itemInfo) = 0; /** * Removes update item. @@ -108,7 +71,14 @@ class ImageManagerItf { * @param version update item version. * @return Error. */ - virtual Error RemoveUpdateItem(const String& itemID, const String& version) = 0; + virtual Error RemoveItem(const String& itemID, const String& version) = 0; + + /** + * Cancels all items being installed. Waits until all InstallItem and RemoveItem calls in progress return. + * + * @return Error. + */ + virtual Error Cancel() = 0; }; /** @}*/ diff --git a/src/core/sm/imagemanager/itf/iteminfoprovider.hpp b/src/core/sm/imagemanager/itf/iteminfoprovider.hpp deleted file mode 100644 index 3f7ab58aa..000000000 --- a/src/core/sm/imagemanager/itf/iteminfoprovider.hpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_SM_IMAGEMANAGER_ITF_ITEMINFOPROVIDER_HPP_ -#define AOS_CORE_SM_IMAGEMANAGER_ITF_ITEMINFOPROVIDER_HPP_ - -#include - -namespace aos::sm::imagemanager { - -/** @addtogroup sm Service Manager - * @{ - */ - -/** - * Interface to provide update item info. - */ -class ItemInfoProviderItf { -public: - /** - * Destructor. - */ - virtual ~ItemInfoProviderItf() = default; - - /** - * 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 layer path by its digest. - * - * @param digest layer digest. - * @param[out] path result layer path. - * @return Error. - */ - virtual Error GetLayerPath(const String& digest, String& path) const = 0; -}; - -/** @}*/ - -} // namespace aos::sm::imagemanager - -#endif diff --git a/src/core/sm/imagemanager/itf/storage.hpp b/src/core/sm/imagemanager/itf/storage.hpp index daf7fa786..10c9329d9 100644 --- a/src/core/sm/imagemanager/itf/storage.hpp +++ b/src/core/sm/imagemanager/itf/storage.hpp @@ -7,108 +7,27 @@ #ifndef AOS_CORE_SM_IMAGEMANAGER_ITF_STORAGE_HPP_ #define AOS_CORE_SM_IMAGEMANAGER_ITF_STORAGE_HPP_ -#include -#include +#include namespace aos::sm::imagemanager { -/** - * Maximum number of stored update items. - * - * @note The maximum number of update items is doubled to account for multiple versions of the same item. +/** @addtogroup sm Service Manager + * @{ */ -constexpr auto cMaxNumStoredUpdateItems = cMaxNumUpdateItems * 2; /** - * Update item data structure. + * SM image manager storage interface. */ -struct UpdateItemData { - StaticString mID; - UpdateItemType mType; - StaticString mVersion; - StaticString mManifestDigest; - ItemState mState; - Time mTimestamp; - - /** - * Compares update item data. - */ - friend bool operator==(const UpdateItemData& lhs, const UpdateItemData& rhs) - { - return lhs.mID == rhs.mID && lhs.mType == rhs.mType && lhs.mVersion == rhs.mVersion - && lhs.mManifestDigest == rhs.mManifestDigest && lhs.mState == rhs.mState - && lhs.mTimestamp == rhs.mTimestamp; - }; - - /** - * Compares update item data. - */ - friend bool operator!=(const UpdateItemData& lhs, const UpdateItemData& rhs) { return !(lhs == rhs); }; -}; - -/** - * Update item data static array type. - */ -using UpdateItemDataStaticArray = StaticArray; - -/** - * Image manager storage interface. - */ -class StorageItf { +class StorageItf : public virtual common::imagemanager::StorageItf { public: /** - * Destructor. - */ - virtual ~StorageItf() = default; - - /** - * Adds update item to storage. - * - * @param updateItem update item to add. - * @return Error. - */ - virtual Error AddUpdateItem(const UpdateItemData& updateItem) = 0; - - /** - * Updates update item in storage. - * - * @param updateItem update item to update. - * @return Error. - */ - virtual Error UpdateUpdateItem(const UpdateItemData& updateItem) = 0; - - /** - * Removes previously stored update item. - * - * @param itemID update item ID to remove. - * @param version update item version. - * @return Error. - */ - virtual Error RemoveUpdateItem(const String& itemID, const String& version) = 0; - - /** - * Returns update item versions by item ID. - * - * @param itemID update item ID. - * @param[out] itemData array to store update item data. - * @return Error. - */ - virtual Error GetUpdateItem(const String& itemID, Array& itemData) = 0; - - /** - * Returns all update items. - * - * @param itemsData[out] array to store update items data. - * @return Error. - */ - virtual Error GetAllUpdateItems(Array& itemsData) = 0; - - /** - * Returns count of stored update items. + * Checks if layer is referenced by any existing deployable item, regardless of the item state. Blob references + * left without an item are ignored. * - * @return RetWithError. + * @param diffID layer diff ID. + * @return RetWithError. */ - virtual RetWithError GetUpdateItemsCount() = 0; + virtual RetWithError IsLayerUsed(const String& diffID) = 0; }; /** @}*/ diff --git a/src/core/sm/imagemanager/tests/imagemanager.cpp b/src/core/sm/imagemanager/tests/imagemanager.cpp index 82f5a2b68..beccd8a7c 100644 --- a/src/core/sm/imagemanager/tests/imagemanager.cpp +++ b/src/core/sm/imagemanager/tests/imagemanager.cpp @@ -4,13 +4,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include +#include +#include #include +#include +#include +#include +#include +#include +#include #include +#include #include #include +#include #include #include #include @@ -19,7 +28,6 @@ #include -#include "mocks/blobinfoprovidermock.hpp" #include "mocks/imagehandlermock.hpp" #include "stubs/storagestub.hpp" @@ -33,128 +41,88 @@ namespace { * Consts **********************************************************************************************************************/ -constexpr auto cTestImagePath = "/tmp/imagemanager_test/images"; -constexpr auto cUpdateItemTTL = 10 * Time::cSeconds; +constexpr auto cTestDir = "/tmp/sm_imagemanager_test"; +constexpr auto cTestImagePath = "/tmp/sm_imagemanager_test/images"; +constexpr auto cItemCacheTTL = 10 * Time::cSeconds; constexpr auto cRemoveOutdatedPeriod = 5 * Time::cSeconds; +constexpr auto cManifestDigest = "sha256:1000000000000000000000000000000000000000000000000000000000000000"; +constexpr auto cManifest2Digest = "sha256:1000000000000000000000000000000000000000000000000000000000000001"; +constexpr auto cConfigDigest = "sha256:2000000000000000000000000000000000000000000000000000000000000000"; +constexpr auto cLayerDigest = "sha256:3000000000000000000000000000000000000000000000000000000000000000"; +constexpr auto cLayer2Digest = "sha256:3000000000000000000000000000000000000000000000000000000000000001"; +constexpr auto cDiffID = "sha256:4000000000000000000000000000000000000000000000000000000000000000"; +constexpr auto cOrphanDiffID = "sha256:4000000000000000000000000000000000000000000000000000000000000001"; +constexpr auto cEmptyDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; +constexpr auto cLayerType = "application/vnd.oci.image.layer.v1.tar+gzip"; +constexpr auto cConfigType = "application/vnd.oci.image.config.v1+json"; +constexpr auto cBlobContent = "blob"; +constexpr auto cBlobSize = 4; +constexpr auto cUnpackedData = "layer data"; +constexpr auto cUnpackedSize = 1024; + /*********************************************************************************************************************** * Static **********************************************************************************************************************/ -Error SplitDigest(const String& digest, String& alg, String& hash) +std::string DigestPath(const std::string& folder, const std::string& digest) { - StaticArray, 2> digestList; - - if (auto err = digest.Split(digestList, ':'); !err.IsNone()) { - return err; - } + auto pos = digest.find(':'); - if (digestList.Size() != 2) { - return ErrorEnum::eInvalidArgument; - } - - alg = digestList[0]; - hash = digestList[1]; - - return ErrorEnum::eNone; + return std::string(cTestImagePath) + "/" + folder + "/" + digest.substr(0, pos) + "/" + digest.substr(pos + 1); } -fs::FileInfo GetFileInfoByDigest(const String& digest, size_t size = 1024) +std::string BlobPath(const std::string& digest) { - fs::FileInfo fileInfo; - StaticString alg; - StaticString hash; - - auto err = SplitDigest(digest, alg, hash); - EXPECT_TRUE(err.IsNone()) << "Failed to split digest: " << tests::utils::ErrorToStr(err); - - err = hash.HexToByteArray(fileInfo.mCheckSum); - EXPECT_TRUE(err.IsNone()) << "Failed to convert hash to byte array: " << tests::utils::ErrorToStr(err); - - fileInfo.mSize = size; - - return fileInfo; -}; + return DigestPath("blobs", digest); +} -fs::FileInfo GetFileInfoByPath(const String& path, size_t size = 1024) +std::string LayerPath(const std::string& diffID) { - fs::FileInfo fileInfo; - StaticString hash; - - auto err = fs::BaseName(path, hash); - EXPECT_TRUE(err.IsNone()) << "Failed to get base name: " << tests::utils::ErrorToStr(err); - - err = hash.HexToByteArray(fileInfo.mCheckSum); - EXPECT_TRUE(err.IsNone()) << "Failed to convert hash to byte array: " << tests::utils::ErrorToStr(err); - - fileInfo.mSize = size; - - return fileInfo; -}; + return DigestPath("layers", diffID); +} -StaticString GetURL(const String& digest) +std::string UnpackedPath(const std::string& diffID) { - StaticString url; - StaticString alg; - StaticString hash; - - auto err = SplitDigest(digest, alg, hash); - EXPECT_TRUE(err.IsNone()) << "Failed to split digest: " << tests::utils::ErrorToStr(err); - - err = url.Format("https://main/%s/%s", alg.CStr(), hash.CStr()); - EXPECT_TRUE(err.IsNone()) << "Failed to format URL: " << tests::utils::ErrorToStr(err); - - return url; + return LayerPath(diffID) + "/layer"; } -StaticString GetBlobPath(const String& digest) +bool Exists(const std::string& path) { - StaticString path; - StaticString alg; - StaticString hash; + auto [exists, err] = fs::FileExist(path.c_str()); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto err = SplitDigest(digest, alg, hash); - EXPECT_TRUE(err.IsNone()) << "Failed to split digest: " << tests::utils::ErrorToStr(err); + if (exists) { + return true; + } - path = fs::JoinPath(cTestImagePath, "blobs", alg, hash); + Tie(exists, err) = fs::DirExist(path.c_str()); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - return path; + return exists; } -StaticString GetLayerPath(const String& digest) +void WriteFile(const std::string& path, const std::string& content) { - StaticString path; - StaticString alg; - StaticString hash; - - auto err = SplitDigest(digest, alg, hash); - EXPECT_TRUE(err.IsNone()) << "Failed to split digest: " << tests::utils::ErrorToStr(err); + auto err = fs::MakeDirAll(fs::Dir(path.c_str())); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - path = fs::JoinPath(cTestImagePath, "layers", alg, hash); + std::ofstream file(path); + ASSERT_TRUE(file.is_open()) << path; - return path; + file << content; } -std::string ReadFileToString(const std::string& filename) +std::string ReadFile(const std::string& path) { - std::ifstream file(filename, std::ios::binary); - EXPECT_TRUE(file.is_open()) << "Failed to open file: " << filename; - - std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - - file.close(); + std::ifstream file(path, std::ios::binary); - return content; + return std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); } -void CreateFile(const std::string& path, const std::string& content = "") +oci::ContentDescriptor Descriptor(const String& mediaType, const String& digest) { - std::ofstream file(path); - EXPECT_TRUE(file.is_open()) << "Failed to create file: " << path; - - file << content; - - file.close(); + return oci::ContentDescriptor {mediaType, digest, cBlobSize}; } } // namespace @@ -163,7 +131,7 @@ void CreateFile(const std::string& path, const std::string& content = "") * Suite **********************************************************************************************************************/ -class ImageManagerTest : public Test { +class SMImageManagerTest : public Test { protected: static void SetUpTestSuite() { @@ -174,886 +142,654 @@ class ImageManagerTest : public Test { void SetUp() override { - Config config {cTestImagePath, 0, cUpdateItemTTL, cRemoveOutdatedPeriod}; + auto err = fs::RemoveAll(cTestDir); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto err = mImageManager.Init(mAllocator, config, mBlobInfoProviderMock, mSpaceAllocatorMock, mDownloaderMock, - mFileInfoProviderMock, mOCISpecMock, mImageHandlerMock, mStorageStub); - EXPECT_TRUE(err.IsNone()) << "Failed to initialize image manager: " << tests::utils::ErrorToStr(err); + err = mImageManager.Init(CreateConfig(cTestImagePath), mBlobInfoProvider, mStorage, mSpaceAllocator, + mDownloader, mImageHandler, mFileInfoProvider, mOCISpec, mFSPlatform, mAllocator); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfo(_, _)) - .WillRepeatedly(Invoke( - [](const Array>& digests, Array>& urls) -> Error { - (void)digests; - (void)urls; + ON_CALL(mBlobInfoProvider, GetBlobInfo).WillByDefault(Invoke([](const String& digest, BlobInfo& info) -> Error { + info.mDigest = digest; + info.mSize = cBlobSize; - if (!digests.Size()) { - return ErrorEnum::eInvalidArgument; - } + auto err = info.mURLs.EmplaceBack("https://cm/blob"); + if (!err.IsNone()) { + return err; + } - if (auto err = urls.EmplaceBack(); !err.IsNone()) { - return err; - } + return info.mURLs.EmplaceBack("https://cm/fallback"); + })); - urls[0] = GetURL(digests[0]); + ON_CALL(mDownloader, Download) + .WillByDefault(Invoke([this](CancelContext&, const String& digest, const Array>&, + const String& path) -> Error { + { + std::lock_guard lock {mMutex}; - return ErrorEnum::eNone; - })); - EXPECT_CALL(mSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](uint64_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); + mDownloaded.insert(digest.CStr()); + } - EXPECT_CALL(*space, Accept()).Times(AtLeast(0)); - EXPECT_CALL(*space, Release()).Times(AtLeast(0)); - EXPECT_CALL(*space, Resize(_)).Times(AtLeast(0)); - EXPECT_CALL(*space, Size()).Times(AtLeast(0)); + WriteFile(path.CStr(), cBlobContent); - return {Move(space), ErrorEnum::eNone}; + return ErrorEnum::eNone; })); - } - - void TearDown() override - { - auto err = fs::RemoveAll(cTestImagePath); - EXPECT_TRUE(err.IsNone()) << "Failed to remove test image path: " << tests::utils::ErrorToStr(err); - } - // mAllocator must be declared (and therefore destroyed) after any member that allocates from it, since - // members are destroyed in reverse declaration order. - HeapAllocator mAllocator; - - ImageManager mImageManager; - NiceMock mBlobInfoProviderMock; - NiceMock mSpaceAllocatorMock; - NiceMock mDownloaderMock; - NiceMock mFileInfoProviderMock; - NiceMock mOCISpecMock; - NiceMock mImageHandlerMock; - StorageStub mStorageStub; -}; - -/*********************************************************************************************************************** - * Tests - **********************************************************************************************************************/ - -TEST_F(ImageManagerTest, InstallComponent) -{ - // Input data + ON_CALL(mFileInfoProvider, GetFileInfo) + .WillByDefault(Invoke([this](const String& path, fs::FileInfo& info, crypto::Hash) -> Error { + const std::string filePath = path.CStr(); - constexpr auto cManifestDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - constexpr auto cLayerDigest = "sha256:4a6f6b8f5f5e3e7b9c4d3e2f1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b"; + StaticString hash; - UpdateItemInfo itemInfo {"component1", UpdateItemTypeEnum::eComponent, "1.0.0", cManifestDigest}; + hash = filePath.substr(filePath.rfind('/') + 1).c_str(); - auto manifestPath = GetBlobPath(cManifestDigest); - auto layerPath = GetBlobPath(cLayerDigest); + if (auto err = hash.HexToByteArray(info.mCheckSum); !err.IsNone()) { + return err; + } - auto imageManifest = std::make_unique(); + std::lock_guard lock {mMutex}; - imageManifest->mConfig.mMediaType = "application/vnd.oci.empty.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 2; - imageManifest->mLayers.EmplaceBack( - oci::ContentDescriptor {"vnd.aos.image.component.full.v1+gzip", cLayerDigest, 1024}); + if (mBrokenBlobs.count(filePath) != 0) { + info.mCheckSum.Clear(); + } - // Expected calls + info.mSize = ReadFile(filePath).size(); - EXPECT_CALL(mDownloaderMock, Download(String(cManifestDigest), _, manifestPath)).Times(1); - EXPECT_CALL(mDownloaderMock, Download(String(cLayerDigest), _, layerPath)).Times(1); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillOnce(DoAll(SetArgReferee<1>(GetFileInfoByDigest(cManifestDigest)), Return(ErrorEnum::eNone))) - .WillOnce(DoAll(SetArgReferee<1>(GetFileInfoByDigest(cLayerDigest)), Return(ErrorEnum::eNone))); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(manifestPath, _)) - .WillOnce(DoAll(SetArgReferee<1>(*imageManifest), Return(ErrorEnum::eNone))); + return ErrorEnum::eNone; + })); - // Install update item + ON_CALL(mOCISpec, LoadImageManifest) + .WillByDefault(Invoke([this](const String& path, oci::ImageManifest& manifest) -> Error { + manifest = *mManifests.at(path.CStr()); - auto err = mImageManager.InstallUpdateItem(itemInfo); - EXPECT_TRUE(err.IsNone()) << "Failed to install update item: " << tests::utils::ErrorToStr(err); -} - -TEST_F(ImageManagerTest, GetBlobPath) -{ - constexpr auto cDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - auto blobPath = GetBlobPath(cDigest); + return ErrorEnum::eNone; + })); - bool created = std::filesystem::create_directories(std::filesystem::path(blobPath.CStr()).parent_path()); - EXPECT_TRUE(created) << "Failed to create blob directory at path: " << blobPath.CStr(); + ON_CALL(mOCISpec, LoadImageConfig) + .WillByDefault(Invoke([this](const String&, oci::ImageConfig& config) -> Error { + config = *mImageConfig; - CreateFile(blobPath.CStr()); + return ErrorEnum::eNone; + })); - StaticString path; + ON_CALL(mImageHandler, GetUnpackedLayerSize).WillByDefault(Return(RetWithError(cUnpackedSize))); - auto err = mImageManager.GetBlobPath(cDigest, path); - EXPECT_TRUE(err.IsNone()) << "Failed to get blob path: " << tests::utils::ErrorToStr(err); - EXPECT_EQ(path, blobPath); -} + ON_CALL(mImageHandler, UnpackLayer) + .WillByDefault(Invoke([](const String&, const String& dst, const String&) -> Error { + WriteFile(std::string(dst.CStr()) + "/data", cUnpackedData); -TEST_F(ImageManagerTest, InstallService) -{ - // Input data - - constexpr auto cManifestDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - constexpr auto cImageConfigDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - constexpr auto cItemConfigDigest = "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; - constexpr auto cLayerDigest = "sha256:4a6f6b8f5f5e3e7b9c4d3e2f1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b"; - constexpr auto cDiffDigest = "sha256:0f9e8d7c6b5a4b3c2b1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b1a0f9e"; - constexpr auto cUnpackedLayerDigest = "sha256:9e8d7c6b5a4b3c2b1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b1a0f9e8d"; - constexpr auto cUnpackedLayerSize = 2048; - - UpdateItemInfo itemInfo {"service1", UpdateItemTypeEnum::eService, "1.0.0", cManifestDigest}; - - auto manifestPath = GetBlobPath(cManifestDigest); - auto imageConfigPath = GetBlobPath(cImageConfigDigest); - auto itemConfigPath = GetBlobPath(cItemConfigDigest); - auto layerBlobPath = GetBlobPath(cLayerDigest); - auto layerUnpackedPath = fs::JoinPath(GetLayerPath(cDiffDigest), "layer"); - - auto imageManifest = std::make_unique(); - - imageManifest->mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - imageManifest->mConfig.mDigest = cImageConfigDigest; - imageManifest->mConfig.mSize = 512; - imageManifest->mItemConfig.EmplaceValue( - oci::ContentDescriptor {"application/vnd.aos.item.config.v1+json", cItemConfigDigest, 256}); - imageManifest->mLayers.EmplaceBack( - oci::ContentDescriptor {"application/vnd.oci.image.layer.v1.tar+gzip", cLayerDigest, 1024}); - - auto imageConfig = std::make_unique(); - - imageConfig->mRootfs.mDiffIDs.EmplaceBack(cDiffDigest); - - // Expected calls - - EXPECT_CALL(mDownloaderMock, Download(String(cManifestDigest), _, manifestPath)).Times(1); - EXPECT_CALL(mDownloaderMock, Download(String(cItemConfigDigest), _, itemConfigPath)).Times(1); - EXPECT_CALL(mDownloaderMock, Download(String(cImageConfigDigest), _, imageConfigPath)).Times(1); - EXPECT_CALL(mDownloaderMock, Download(String(cLayerDigest), _, layerBlobPath)).Times(1); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillOnce(DoAll(SetArgReferee<1>(GetFileInfoByDigest(cManifestDigest)), Return(ErrorEnum::eNone))) - .WillOnce(DoAll(SetArgReferee<1>(GetFileInfoByDigest(cItemConfigDigest)), Return(ErrorEnum::eNone))) - .WillOnce(DoAll(SetArgReferee<1>(GetFileInfoByDigest(cImageConfigDigest)), Return(ErrorEnum::eNone))) - .WillOnce(DoAll(SetArgReferee<1>(GetFileInfoByDigest(cLayerDigest)), Return(ErrorEnum::eNone))); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(manifestPath, _)) - .WillOnce(DoAll(SetArgReferee<1>(*imageManifest), Return(ErrorEnum::eNone))); - EXPECT_CALL(mOCISpecMock, LoadImageConfig(imageConfigPath, _)) - .WillOnce(DoAll(SetArgReferee<1>(*imageConfig), Return(ErrorEnum::eNone))); - EXPECT_CALL( - mImageHandlerMock, GetUnpackedLayerSize(layerBlobPath, String("application/vnd.oci.image.layer.v1.tar+gzip"))) - .WillOnce(Return(cUnpackedLayerSize)); - EXPECT_CALL(mImageHandlerMock, UnpackLayer(layerBlobPath, layerUnpackedPath, imageManifest->mLayers[0].mMediaType)) - .Times(1); - EXPECT_CALL(mImageHandlerMock, GetUnpackedLayerDigest(layerUnpackedPath)) - .WillOnce(Return(StaticString(cUnpackedLayerDigest))); - - // Install update item - - auto err = mImageManager.InstallUpdateItem(itemInfo); - EXPECT_TRUE(err.IsNone()) << "Failed to install update item: " << tests::utils::ErrorToStr(err); - - // Check metadata - - auto diffLayerDigest = ReadFileToString(GetBlobPath(cLayerDigest).CStr()); - EXPECT_EQ(diffLayerDigest, cDiffDigest); - - auto unpackedLayerDigest = ReadFileToString(fs::JoinPath(GetLayerPath(cDiffDigest), "digest").CStr()); - EXPECT_EQ(unpackedLayerDigest, cUnpackedLayerDigest); - - auto unpackedLayerSizeStr = ReadFileToString(fs::JoinPath(GetLayerPath(cDiffDigest), "size").CStr()); - EXPECT_EQ(std::stoull(unpackedLayerSizeStr), cUnpackedLayerSize); -} + return ErrorEnum::eNone; + })); -TEST_F(ImageManagerTest, GetLayerPath) -{ - constexpr auto cDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - auto layerPath = fs::JoinPath(GetLayerPath(cDigest), "layer"); + ON_CALL(mImageHandler, GetUnpackedLayerDigest) + .WillByDefault(Invoke([](const String& path) -> RetWithError> { + StaticString digest; - bool created = std::filesystem::create_directories(layerPath.CStr()); - EXPECT_TRUE(created) << "Failed to create layer directory at path: " << layerPath.CStr(); + digest = ReadFile(std::string(path.CStr()) + "/data").c_str(); - StaticString path; + return digest; + })); - auto err = mImageManager.GetLayerPath(cDigest, path); - EXPECT_TRUE(err.IsNone()) << "Failed to get layer path: " << tests::utils::ErrorToStr(err); - EXPECT_EQ(path, layerPath); -} + ON_CALL(mSpaceAllocator, AllocateSpace) + .WillByDefault(Invoke([this](size_t size) -> RetWithError> { + auto space = MakeUnique>(&mAllocator); -TEST_F(ImageManagerTest, GetAllInstalledItems) -{ - std::vector installItems - = {{"component1", UpdateItemTypeEnum::eComponent, "1.0.0", - "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"}, - {"service1", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"}, - {"component2", UpdateItemTypeEnum::eComponent, "2.0.0", - "sha256:0f9e8d7c6b5a4b3c2b1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b1a0f9e"}, - {"service2", UpdateItemTypeEnum::eService, "2.0.0", - "sha256:9e8d7c6b5a4b3c2b1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b1a0f9e8d"}, - {"component3", UpdateItemTypeEnum::eComponent, "3.0.0", - "sha256:8d7c6b5a4b3c2b1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b1a0f9e8d7c"}, - {"service3", UpdateItemTypeEnum::eService, "3.0.0", - "sha256:7c6b5a4b3c2b1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b1a0f9e8d7c6b"}}; - - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([](const String& path, fs::FileInfo& fileInfo, crypto::Hash) -> Error { - fileInfo = GetFileInfoByPath(path); + ON_CALL(*space, Accept()).WillByDefault(Invoke([this, size]() { + mAcceptedAllocations.push_back(size); - return ErrorEnum::eNone; - })); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([](const String& path, oci::ImageManifest& manifest) -> Error { - (void)path; - - manifest.mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - manifest.mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - manifest.mConfig.mSize = 512; - manifest.mItemConfig.EmplaceValue(oci::ContentDescriptor {"application/vnd.aos.item.config.v1+json", - "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", 256}); - manifest.mLayers.EmplaceBack(oci::ContentDescriptor {"application/vnd.oci.image.layer.v1.tar+gzip", - "sha256:4a6f6b8f5f5e3e7b9c4d3e2f1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b", 1024}); + return ErrorEnum::eNone; + })); + ON_CALL(*space, Release()).WillByDefault(Invoke([this]() { + mReleasedSpaces++; - return ErrorEnum::eNone; - })); - EXPECT_CALL(mOCISpecMock, LoadImageConfig(_, _)) - .WillRepeatedly(Invoke([](const String& path, oci::ImageConfig& imageConfig) -> Error { - (void)path; + return ErrorEnum::eNone; + })); + ON_CALL(*space, Resize(_)).WillByDefault(Invoke([this](size_t resized) { + mLastResize = resized; - imageConfig.mRootfs.mDiffIDs.EmplaceBack( - "sha256:0f9e8d7c6b5a4b3c2b1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b1a0f9e"); + return ErrorEnum::eNone; + })); - return ErrorEnum::eNone; - })); - EXPECT_CALL(mImageHandlerMock, GetUnpackedLayerSize(_, String("application/vnd.oci.image.layer.v1.tar+gzip"))) - .WillRepeatedly(Return(1024)); - EXPECT_CALL(mImageHandlerMock, GetUnpackedLayerDigest(_)) - .WillRepeatedly(Return( - StaticString("sha256:9e8d7c6b5a4b3c2b1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b1a0f9e8d"))); + return {Move(space), ErrorEnum::eNone}; + })); - // Install update item + AddService(cManifestDigest, cLayerDigest); - for (const auto& item : installItems) { - auto err = mImageManager.InstallUpdateItem(item); - EXPECT_TRUE(err.IsNone()) << "Failed to install item: " << tests::utils::ErrorToStr(err); + mImageConfig = std::make_unique(); + mImageConfig->mRootfs.mDiffIDs.EmplaceBack(cDiffID); } - std::vector removeIndexes = {1, 4}; - - for (auto it = removeIndexes.rbegin(); it != removeIndexes.rend(); ++it) { - auto err = mImageManager.RemoveUpdateItem(installItems[*it].mID, installItems[*it].mVersion); - EXPECT_TRUE(err.IsNone()) << "Failed to remove item: " << tests::utils::ErrorToStr(err); - - installItems.erase(installItems.begin() + *it); + void TearDown() override + { + auto err = fs::RemoveAll(cTestDir); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); } - auto installedItems = std::make_unique>(); - - auto err = mImageManager.GetAllInstalledItems(*installedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << tests::utils::ErrorToStr(err); + static common::imagemanager::Config CreateConfig(const String& imagePath) + { + common::imagemanager::Config config; - for (size_t i = 0; i < installedItems->Size(); ++i) { - UpdateItemStatus status - = {installItems[i].mID, installItems[i].mType, installItems[i].mVersion, ItemStateEnum::eInstalled}; + config.mImagePath = imagePath; + config.mItemCacheTTL = cItemCacheTTL; + config.mRemoveOutdatedPeriod = cRemoveOutdatedPeriod; - EXPECT_NE(installedItems->Find(status), installedItems->end()) - << "Installed item not found: " << (*installedItems)[i].mID.CStr() << " " - << (*installedItems)[i].mVersion.CStr(); + return config; } -} -TEST_F(ImageManagerTest, RemoveOutdatedItems) -{ - std::vector initialItems = { - {"item1", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:1111111111111111111111111111111111111111111111111111111111111111", ItemStateEnum::eRemoved, - Time::Now().Add(-(cUpdateItemTTL + 1 * Time::cSeconds))}, - {"item2", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:2222222222222222222222222222222222222222222222222222222222222222", ItemStateEnum::eRemoved, - Time::Now().Add(-(cUpdateItemTTL + 1 * Time::cSeconds))}, - {"item3", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:3333333333333333333333333333333333333333333333333333333333333333", ItemStateEnum::eRemoved, - Time::Now()}, - {"item4", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:4444444444444444444444444444444444444444444444444444444444444444", ItemStateEnum::eRemoved, - Time::Now()}, - }; - - mStorageStub.Init(initialItems); - - auto imageManifest = std::make_unique(); - - imageManifest->mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 512; - - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(DoAll(SetArgReferee<1>(*imageManifest), Return(ErrorEnum::eNone))); - - // Expect adding outdated items to space allocator for all deleted items - - EXPECT_CALL(mSpaceAllocatorMock, AddOutdatedItem(String("item1"), String("1.0.0"), _)).Times(1); - EXPECT_CALL(mSpaceAllocatorMock, AddOutdatedItem(String("item2"), String("1.0.0"), _)).Times(1); - EXPECT_CALL(mSpaceAllocatorMock, AddOutdatedItem(String("item3"), String("1.0.0"), _)).Times(1); - EXPECT_CALL(mSpaceAllocatorMock, AddOutdatedItem(String("item4"), String("1.0.0"), _)).Times(1); - - // Expect restoring outdated items for first two removed items just after start - - EXPECT_CALL(mSpaceAllocatorMock, RestoreOutdatedItem(String("item1"), String("1.0.0"))).Times(1); - EXPECT_CALL(mSpaceAllocatorMock, RestoreOutdatedItem(String("item2"), String("1.0.0"))).Times(1); + void AddService(const std::string& manifestDigest, const std::string& layerDigest) + { + auto manifest = std::make_unique(); - std::vector> futures; + manifest->mConfig = Descriptor(cConfigType, cConfigDigest); + manifest->mLayers.EmplaceBack(Descriptor(cLayerType, layerDigest.c_str())); - for (size_t i = 0; i < 2; ++i) { - futures.emplace_back(mStorageStub.GetRemoveFuture()); + mManifests[BlobPath(manifestDigest)] = std::move(manifest); } - auto err = mImageManager.Start(); - EXPECT_TRUE(err.IsNone()) << "Failed to start image manager: " << tests::utils::ErrorToStr(err); - - // Verify that first two removed items are removed just after start + void AddComponent(const std::string& manifestDigest, const std::string& layerDigest) + { + auto manifest = std::make_unique(); - for (size_t i = 0; i < 2; ++i) { - ASSERT_EQ(futures[i].wait_for(std::chrono::milliseconds(cUpdateItemTTL.Milliseconds() * 2)), - std::future_status::ready); + manifest->mConfig = oci::ContentDescriptor {oci::cMediaTypeEmptyBlob, cEmptyDigest, 0}; + manifest->mLayers.EmplaceBack(Descriptor(cLayerType, layerDigest.c_str())); - auto itemData = futures[i].get(); + mManifests[BlobPath(manifestDigest)] = std::move(manifest); + } - EXPECT_TRUE(itemData.mID == "item1" || itemData.mID == "item2") - << "Unexpected item ID: " << itemData.mID.CStr(); + static UpdateItemInfo ServiceInfo(const String& itemID, const String& digest = cManifestDigest) + { + return UpdateItemInfo {itemID, UpdateItemTypeEnum::eService, "1.0.0", digest}; } - // Verify that last two removed items are still present in storage + std::optional GetItemState(const String& itemID) + { + auto statuses = std::make_unique>(); - auto storedItems = std::make_unique(); + auto err = mImageManager.GetItemsStatuses(*statuses); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all stored items: " << tests::utils::ErrorToStr(err); + for (const auto& status : *statuses) { + if (status.mItemID == itemID) { + return status.mState; + } + } - for (const auto& item : *storedItems) { - EXPECT_TRUE(item.mID == "item3" || item.mID == "item4") << "Unexpected item ID: " << item.mID.CStr(); + return std::nullopt; } - // Expect restoring outdated items for last two removed items after remove outdated period - - EXPECT_CALL(mSpaceAllocatorMock, RestoreOutdatedItem(String("item3"), String("1.0.0"))).Times(1); - EXPECT_CALL(mSpaceAllocatorMock, RestoreOutdatedItem(String("item4"), String("1.0.0"))).Times(1); + // mAllocator must be declared (and therefore destroyed) after any member that allocates from it, since + // members are destroyed in reverse declaration order. + HeapAllocator mAllocator; - // Verify that last two removed items are removed after remove outdated period + ImageManager mImageManager; + NiceMock mBlobInfoProvider; + NiceMock mSpaceAllocator; + NiceMock mDownloader; + NiceMock mFileInfoProvider; + NiceMock mOCISpec; + NiceMock mImageHandler; + NiceMock mFSPlatform; + StorageStub mStorage; + + std::mutex mMutex; + std::set mDownloaded; + std::set mBrokenBlobs; + std::map> mManifests; + std::unique_ptr mImageConfig; + std::vector mAcceptedAllocations; + std::atomic mReleasedSpaces {}; + std::atomic mLastResize {}; +}; - futures.clear(); +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ - for (size_t i = 0; i < 2; ++i) { - futures.emplace_back(mStorageStub.GetRemoveFuture()); - } +TEST_F(SMImageManagerTest, InitFailsIfImagePathIsTooLongForLayerPath) +{ + // The path fits the blob path, but not the longest layer path. + const auto maxBlobImagePathSize = cFilePathLen - (sizeof("/blobs/") - 1) - oci::cDigestLen; - for (size_t i = 0; i < 2; ++i) { - ASSERT_EQ(futures[i].wait_for(std::chrono::milliseconds(cUpdateItemTTL.Milliseconds() * 2)), - std::future_status::ready); + StaticString imagePath; - auto itemData = futures[i].get(); + imagePath.Resize(maxBlobImagePathSize); - EXPECT_TRUE(itemData.mID == "item3" || itemData.mID == "item4") - << "Unexpected item ID: " << itemData.mID.CStr(); + for (auto& ch : imagePath) { + ch = 'a'; } - // Verify that no stored items are present in storage + ImageManager imageManager; - storedItems->Clear(); - - err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all stored items: " << tests::utils::ErrorToStr(err); - EXPECT_TRUE(storedItems->IsEmpty()) << "Expected no stored items after removal"; - - err = mImageManager.Stop(); - EXPECT_TRUE(err.IsNone()) << "Failed to stop image manager: " << tests::utils::ErrorToStr(err); + auto err = imageManager.Init(CreateConfig(imagePath), mBlobInfoProvider, mStorage, mSpaceAllocator, mDownloader, + mImageHandler, mFileInfoProvider, mOCISpec, mFSPlatform, mAllocator); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); } -TEST_F(ImageManagerTest, MaxItemVersions) +TEST_F(SMImageManagerTest, InstallServiceUnpacksLayerAndRemovesBlob) { - UpdateItemInfo itemInfo {"service1", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"}; - - auto imageManifest = std::make_unique(); + EXPECT_CALL(mDownloader, + Download(_, _, Truly([](const Array>& urls) { return urls.Size() == 2; }), _)) + .Times(3); + std::string unpackSrc; + std::string unpackDst; + std::string unpackType; - imageManifest->mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 1024; + EXPECT_CALL(mImageHandler, UnpackLayer) + .WillOnce(Invoke([&](const String& src, const String& dst, const String& mediaType) -> Error { + unpackSrc = src.CStr(); + unpackDst = dst.CStr(); + unpackType = mediaType.CStr(); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([](const String& path, fs::FileInfo& fileInfo, crypto::Hash) -> Error { - fileInfo = GetFileInfoByPath(path); + WriteFile(unpackDst + "/data", cUnpackedData); return ErrorEnum::eNone; })); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(DoAll(SetArgReferee<1>(*imageManifest), Return(ErrorEnum::eNone))); - // Install update items - - for (size_t i = 0; i < 3; ++i) { - auto err = itemInfo.mVersion.Format("%d.0.0", i + 1); - EXPECT_TRUE(err.IsNone()) << "Failed to format item version: " << tests::utils::ErrorToStr(err); - - err = mImageManager.InstallUpdateItem(itemInfo); - EXPECT_TRUE(err.IsNone()) << "Failed to install update item: " << tests::utils::ErrorToStr(err); - } + // The layer blob is removed after the layer is unpacked and its space is freed. + EXPECT_CALL(mSpaceAllocator, FreeSpace(strlen(cBlobContent))); - auto storedItems = std::make_unique(); + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - // Expect 2 last installed items + EXPECT_EQ(unpackSrc, BlobPath(cLayerDigest)); + EXPECT_EQ(unpackDst, UnpackedPath(cDiffID)); + EXPECT_EQ(unpackType, cLayerType); - auto err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << tests::utils::ErrorToStr(err); + EXPECT_EQ(GetItemState("service"), ItemState(ItemStateEnum::eInstalled)); + EXPECT_FALSE(Exists(BlobPath(cLayerDigest))); + EXPECT_TRUE(Exists(BlobPath(cConfigDigest))); + EXPECT_EQ(ReadFile(LayerPath(cDiffID) + "/digest"), cUnpackedData); + EXPECT_EQ(ReadFile(LayerPath(cDiffID) + "/size"), std::to_string(cUnpackedSize)); - EXPECT_EQ(storedItems->Size(), 2) << "Unexpected number of installed items"; - EXPECT_EQ(storedItems->FindIf( - [&](const UpdateItemData& item) { return item.mID == "service1" && item.mVersion == "1.0.0"; }), - storedItems->end()) - << "Unexpected installed item version 1.0.0 found"; + // Metadata blobs and the layer blob are allocated with the blob size, the layer with its unpacked size. + EXPECT_EQ(mAcceptedAllocations.back(), cUnpackedSize); - // Remove specific version + // The space is resized to the real size of the layer files, as this is what the removal frees. + EXPECT_EQ(mLastResize, strlen(cUnpackedData) * 2 + std::to_string(cUnpackedSize).size()); - err = mImageManager.RemoveUpdateItem("service1", "3.0.0"); - EXPECT_TRUE(err.IsNone()) << "Failed to remove update item: " << tests::utils::ErrorToStr(err); + StaticString path; - err = mImageManager.InstallUpdateItem({"service1", UpdateItemTypeEnum::eService, "4.0.0", - "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"}); - EXPECT_TRUE(err.IsNone()) << "Failed to install update item: " << tests::utils::ErrorToStr(err); + err = mImageManager.GetLayerPath(cDiffID, path); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_EQ(UnpackedPath(cDiffID), path.CStr()); +} - // Expect this version to be removed +TEST_F(SMImageManagerTest, InstallComponentKeepsLayerPacked) +{ + AddComponent(cManifest2Digest, cLayerDigest); - storedItems->Clear(); + EXPECT_CALL(mImageHandler, UnpackLayer).Times(0); - err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << tests::utils::ErrorToStr(err); + auto err = mImageManager.InstallItem( + UpdateItemInfo {"component", UpdateItemTypeEnum::eComponent, "1.0.0", cManifest2Digest}); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_EQ(storedItems->Size(), 2) << "Unexpected number of installed items"; - EXPECT_EQ(storedItems->FindIf( - [&](const UpdateItemData& item) { return item.mID == "service1" && item.mVersion == "3.0.0"; }), - storedItems->end()) - << "Unexpected installed item version 3.0.0 found"; + EXPECT_EQ(GetItemState("component"), ItemState(ItemStateEnum::eInstalled)); + EXPECT_TRUE(Exists(BlobPath(cLayerDigest))); + EXPECT_FALSE(Exists(LayerPath(cDiffID))); } -TEST_F(ImageManagerTest, MaxStoredItems) +TEST_F(SMImageManagerTest, InstallAgainKeepsValidLayer) { - UpdateItemInfo itemInfo {"service1", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"}; + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto imageManifest = std::make_unique(); + // The packed blob is not needed if the unpacked layer is valid. + ASSERT_FALSE(Exists(BlobPath(cLayerDigest))); - imageManifest->mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 1024; + mDownloaded.clear(); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([](const String& path, fs::FileInfo& fileInfo, crypto::Hash) -> Error { - fileInfo = GetFileInfoByPath(path); - - return ErrorEnum::eNone; - })); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(DoAll(SetArgReferee<1>(*imageManifest), Return(ErrorEnum::eNone))); + EXPECT_CALL(mImageHandler, UnpackLayer).Times(0); - // Install update items up to the max limit + err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - for (size_t i = 0; i < cMaxNumStoredUpdateItems; ++i) { - auto err = itemInfo.mID.Format("service%d", i + 1); - EXPECT_TRUE(err.IsNone()) << "Failed to format item version: " << tests::utils::ErrorToStr(err); - - err = mImageManager.InstallUpdateItem(itemInfo); - EXPECT_TRUE(err.IsNone()) << "Failed to install update item: " << tests::utils::ErrorToStr(err); - } - - // Install one more item to exceed the limit + EXPECT_TRUE(mDownloaded.empty()); +} - auto err = itemInfo.mID.Format("service%d", cMaxNumStoredUpdateItems + 1); - EXPECT_TRUE(err.IsNone()) << "Failed to format item version: " << tests::utils::ErrorToStr(err); +TEST_F(SMImageManagerTest, InstallAgainRepairsBrokenLayer) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - err = mImageManager.InstallUpdateItem(itemInfo); - EXPECT_TRUE(err.IsNone()) << "Failed to install update item: " << tests::utils::ErrorToStr(err); + WriteFile(UnpackedPath(cDiffID) + "/data", "broken"); + WriteFile(UnpackedPath(cDiffID) + "/garbage", "garbage"); - // Expect the first installed item to be removed + const auto oldSize + = strlen("broken") + strlen("garbage") + strlen(cUnpackedData) + std::to_string(cUnpackedSize).size(); - auto storedItems = std::make_unique(); + mDownloaded.clear(); - err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << tests::utils::ErrorToStr(err); + // The layer is referenced by the item itself, so RemoveLayer keeps it and it is replaced by UnpackLayer. The + // layer blob, removed by the first install, is downloaded again and removed after the layer is unpacked. + { + InSequence seq; - EXPECT_EQ(storedItems->Size(), cMaxNumStoredUpdateItems) << "Unexpected number of installed items"; - EXPECT_EQ( - storedItems->FindIf([&](const UpdateItemData& item) { return item.mID == "service1"; }), storedItems->end()) - << "Unexpected installed item service1 found"; + EXPECT_CALL(mSpaceAllocator, FreeSpace(oldSize)); + EXPECT_CALL(mSpaceAllocator, FreeSpace(strlen(cBlobContent))); + } - // Remove specific version and install again to verify space is freed + EXPECT_CALL(mImageHandler, UnpackLayer); - err = mImageManager.RemoveUpdateItem("service42", "1.0.0"); - EXPECT_TRUE(err.IsNone()) << "Failed to remove update item: " << tests::utils::ErrorToStr(err); + err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - err = itemInfo.mID.Format("service%d", cMaxNumStoredUpdateItems + 2); - EXPECT_TRUE(err.IsNone()) << "Failed to format item version: " << tests::utils::ErrorToStr(err); + EXPECT_EQ(mDownloaded, std::set {cLayerDigest}); + EXPECT_EQ(ReadFile(UnpackedPath(cDiffID) + "/data"), cUnpackedData); + EXPECT_FALSE(Exists(UnpackedPath(cDiffID) + "/garbage")); + EXPECT_FALSE(Exists(BlobPath(cLayerDigest))); +} - err = mImageManager.InstallUpdateItem(itemInfo); - EXPECT_TRUE(err.IsNone()) << "Failed to install update item: " << tests::utils::ErrorToStr(err); +TEST_F(SMImageManagerTest, InstallAgainFinishesInterruptedUnpack) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - // Expect the removed item to be gone + ASSERT_TRUE(fs::RemoveAll((LayerPath(cDiffID) + "/digest").c_str()).IsNone()); - storedItems->Clear(); + EXPECT_CALL(mImageHandler, UnpackLayer); - err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << tests::utils::ErrorToStr(err); + err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - EXPECT_EQ(storedItems->Size(), cMaxNumStoredUpdateItems) << "Unexpected number of installed items"; - EXPECT_EQ( - storedItems->FindIf([&](const UpdateItemData& item) { return item.mID == "service42"; }), storedItems->end()) - << "Unexpected installed item service42 found"; + EXPECT_TRUE(Exists(LayerPath(cDiffID) + "/digest")); } -TEST_F(ImageManagerTest, ValidateIntegrity) +TEST_F(SMImageManagerTest, UnpackFailureLeavesNoLayer) { - auto itemsInfo = std::vector { - {"service1", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:0000000000000000000000000000000000000000000000000000000000000000", ItemStateEnum::eInstalled, - Time::Now()}, - {"service2", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:1111111111111111111111111111111111111111111111111111111111111111", ItemStateEnum::eInstalled, - Time::Now()}, - {"component1", UpdateItemTypeEnum::eComponent, "1.0.0", - "sha256:2222222222222222222222222222222222222222222222222222222222222222", ItemStateEnum::eInstalled, - Time::Now()}, - {"component2", UpdateItemTypeEnum::eComponent, "1.0.0", - "sha256:3333333333333333333333333333333333333333333333333333333333333333", ItemStateEnum::eInstalled, - Time::Now()}, - }; - - mStorageStub.Init(itemsInfo); - - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(Invoke([&](const String& path, oci::ImageManifest& manifest) -> Error { - auto fileName = std::filesystem::path(path.CStr()).filename().string(); - - auto it = std::find_if(itemsInfo.begin(), itemsInfo.end(), [&](const UpdateItemData& item) { - return std::string(item.mManifestDigest.CStr()).compare(7, fileName.size(), fileName) == 0; - }); - if (it == itemsInfo.end()) { - return ErrorEnum::eNotFound; - } + EXPECT_CALL(mImageHandler, UnpackLayer) + .WillOnce(Invoke([](const String&, const String& dst, const String&) -> Error { + WriteFile(std::string(dst.CStr()) + "/partial", "partial"); - if (it->mType == UpdateItemTypeEnum::eService) { - manifest.mConfig = oci::ContentDescriptor {"application/vnd.oci.image.config.v1+json", - "sha256:4444444444444444444444444444444444444444444444444444444444444444", 512}; - } else { - manifest.mConfig = oci::ContentDescriptor {"application/vnd.oci.empty.v1+json", - "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", 2}; - } - - return ErrorEnum::eNone; + return ErrorEnum::eFailed; })); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([&](const String& path, fs::FileInfo& fileInfo, crypto::Hash) -> Error { - auto fileName = std::filesystem::path(path.CStr()).filename().string(); - - auto it = std::find_if(itemsInfo.begin(), itemsInfo.end(), [&](const UpdateItemData& item) { - return std::string(item.mManifestDigest.CStr()).compare(7, fileName.size(), fileName) == 0; - }); - if (it != itemsInfo.end()) { - if (it->mID == "service2" || it->mID == "component2") { - fileInfo.mCheckSum - = Array(reinterpret_cast("invalidinvalidinvalidinvalidinva"), 32); - fileInfo.mSize = 1024; - return ErrorEnum::eNone; - } - } + // The layer blob is kept on a failed unpack. + EXPECT_CALL(mSpaceAllocator, FreeSpace).Times(0); - fileInfo = GetFileInfoByPath(path); + auto err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); - return ErrorEnum::eNone; - })); + EXPECT_FALSE(Exists(LayerPath(cDiffID))); + EXPECT_TRUE(Exists(BlobPath(cLayerDigest))); + EXPECT_EQ(mReleasedSpaces, 1); + EXPECT_EQ(GetItemState("service"), ItemState(ItemStateEnum::eFailed)); +} - std::vector> futures; +TEST_F(SMImageManagerTest, SpaceAllocationFailureLeavesNoLayer) +{ + EXPECT_CALL(mSpaceAllocator, AllocateSpace(_)).WillRepeatedly(DoDefault()); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(static_cast(cUnpackedSize))) + .WillOnce(Return(RetWithError>(nullptr, ErrorEnum::eNoMemory))); - for (size_t i = 0; i < 2; ++i) { - futures.emplace_back(mStorageStub.GetRemoveFuture()); - } + auto err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.Is(ErrorEnum::eNoMemory)) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(Exists(LayerPath(cDiffID))); +} - auto err = mImageManager.Start(); - EXPECT_TRUE(err.IsNone()) << "Failed to start image manager: " << tests::utils::ErrorToStr(err); +TEST_F(SMImageManagerTest, UnsupportedLayerMediaTypeFails) +{ + auto manifest = std::make_unique(); - for (size_t i = 0; i < 2; ++i) { - ASSERT_EQ( - futures[i].wait_for(std::chrono::milliseconds(cUpdateItemTTL.Milliseconds())), std::future_status::ready); + manifest->mConfig = Descriptor(cConfigType, cConfigDigest); + manifest->mLayers.EmplaceBack(Descriptor("application/vnd.oci.image.layer.v1.tar+zstd", cLayerDigest)); - auto itemData = futures[i].get(); + mManifests[BlobPath(cManifestDigest)] = std::move(manifest); - EXPECT_TRUE(itemData.mID == "service2" || itemData.mID == "component2") - << "Unexpected item ID: " << itemData.mID.CStr(); - } + EXPECT_CALL(mImageHandler, UnpackLayer).Times(0); - err = mImageManager.Stop(); - EXPECT_TRUE(err.IsNone()) << "Failed to stop image manager: " << tests::utils::ErrorToStr(err); + auto err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(Exists(LayerPath(cDiffID))); + EXPECT_EQ(GetItemState("service"), ItemState(ItemStateEnum::eFailed)); } -TEST_F(ImageManagerTest, RemoveOrphanBlobs) +TEST_F(SMImageManagerTest, BrokenDownloadFails) { - auto itemsInfo = std::vector { - {"service1", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:0000000000000000000000000000000000000000000000000000000000000000", ItemStateEnum::eInstalled, - Time::Now()}, - {"service2", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:1111111111111111111111111111111111111111111111111111111111111111", ItemStateEnum::eInstalled, - Time::Now()}, - {"component1", UpdateItemTypeEnum::eComponent, "1.0.0", - "sha256:2222222222222222222222222222222222222222222222222222222222222222", ItemStateEnum::eInstalled, - Time::Now()}, - {"component2", UpdateItemTypeEnum::eComponent, "1.0.0", - "sha256:3333333333333333333333333333333333333333333333333333333333333333", ItemStateEnum::eInstalled, - Time::Now()}, - }; + { + std::lock_guard lock {mMutex}; - auto imageManifest = std::make_unique(); + mBrokenBlobs.insert(BlobPath(cManifestDigest)); + } - imageManifest->mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 512; + auto err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidChecksum)) << tests::utils::ErrorToStr(err); - mStorageStub.Init(itemsInfo); + EXPECT_EQ(GetItemState("service"), ItemState(ItemStateEnum::eFailed)); +} - auto blobsPath = fs::JoinPath(cTestImagePath, "blobs", "sha256"); +TEST_F(SMImageManagerTest, DownloadErrorIsReturned) +{ + EXPECT_CALL(mDownloader, Download).WillOnce(Return(ErrorEnum::eIntegrity)); - std::filesystem::create_directories(blobsPath.CStr()); + auto err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.Is(ErrorEnum::eIntegrity)) << tests::utils::ErrorToStr(err); +} - // Create image config blob +TEST_F(SMImageManagerTest, CancelStopsDownload) +{ + std::promise started; - CreateFile(GetBlobPath(imageManifest->mConfig.mDigest).CStr()); + EXPECT_CALL(mDownloader, Download) + .WillOnce(Invoke([&](CancelContext& ctx, const String&, const Array>&, const String&) { + started.set_value(); - // Create used blobs + return ctx.Wait(); + })) + .WillRepeatedly(DoDefault()); - for (const auto& item : itemsInfo) { - CreateFile(GetBlobPath(item.mManifestDigest).CStr()); - } + auto install + = std::async(std::launch::async, [this]() { return mImageManager.InstallItem(ServiceInfo("service")); }); - // Create orphan blobs + started.get_future().wait(); - for (size_t i = 0; i < 10; i++) { - StaticString digest; + auto err = mImageManager.Cancel(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - digest.Format("sha256:%064d", i + 1); + err = install.get(); + EXPECT_TRUE(err.Is(ErrorEnum::eCanceled)) << tests::utils::ErrorToStr(err); - CreateFile(GetBlobPath(digest).CStr()); - } + // The items installed after the cancel are not affected. + err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} - std::promise promise; +TEST_F(SMImageManagerTest, SameDiffIDIsUnpackedOnce) +{ + AddService(cManifest2Digest, cLayer2Digest); + + EXPECT_CALL(mImageHandler, UnpackLayer) + .WillOnce(Invoke([](const String&, const String& dst, const String&) -> Error { + std::this_thread::sleep_for(std::chrono::milliseconds(200)); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([&](const String& path, fs::FileInfo& fileInfo, crypto::Hash) -> Error { - fileInfo = GetFileInfoByPath(path); + WriteFile(std::string(dst.CStr()) + "/data", cUnpackedData); return ErrorEnum::eNone; })); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(DoAll(SetArgReferee<1>(*imageManifest), Return(ErrorEnum::eNone))); - EXPECT_CALL(mSpaceAllocatorMock, FreeSpace(_)).WillOnce(Invoke([&](size_t size) -> Error { - promise.set_value(size); - - return ErrorEnum::eNone; - })); - - auto err = mImageManager.Start(); - EXPECT_TRUE(err.IsNone()) << "Failed to start image manager: " << tests::utils::ErrorToStr(err); - ASSERT_TRUE(promise.get_future().wait_for(std::chrono::milliseconds(cUpdateItemTTL.Milliseconds())) - == std::future_status::ready); - - err = mImageManager.Stop(); - EXPECT_TRUE(err.IsNone()) << "Failed to stop image manager: " << tests::utils::ErrorToStr(err); + auto install1 + = std::async(std::launch::async, [this]() { return mImageManager.InstallItem(ServiceInfo("service1")); }); + auto install2 = std::async( + std::launch::async, [this]() { return mImageManager.InstallItem(ServiceInfo("service2", cManifest2Digest)); }); - for (const auto& entry : std::filesystem::directory_iterator(blobsPath.CStr())) { - bool found = false; + auto err = install1.get(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - for (const auto& item : itemsInfo) { - if (entry.path().c_str() == GetBlobPath(item.mManifestDigest)) { - found = true; - - break; - } - } + err = install2.get(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - if (!found) { - if (entry.path().c_str() == GetBlobPath(imageManifest->mConfig.mDigest)) { - found = true; - } - } - - EXPECT_TRUE(found) << "Orphan blob not removed: " << entry.path().c_str(); - } + // The blob of the layer unpacked or found valid is not kept. + EXPECT_FALSE(Exists(BlobPath(cLayerDigest))); + EXPECT_FALSE(Exists(BlobPath(cLayer2Digest))); } -TEST_F(ImageManagerTest, RemoveOrphanLayers) +TEST_F(SMImageManagerTest, CancelStopsWaitForLayer) { - auto itemsInfo = std::vector { - {"service1", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:0000000000000000000000000000000000000000000000000000000000000000", ItemStateEnum::eInstalled, - Time::Now()}, - {"service2", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:1111111111111111111111111111111111111111111111111111111111111111", ItemStateEnum::eInstalled, - Time::Now()}, - {"service3", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:2222222222222222222222222222222222222222222222222222222222222222", ItemStateEnum::eInstalled, - Time::Now()}, - {"service4", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:3333333333333333333333333333333333333333333333333333333333333333", ItemStateEnum::eInstalled, - Time::Now()}, - }; + AddService(cManifest2Digest, cLayer2Digest); - auto imageManifest = std::make_unique(); + std::promise unpackStarted; + std::promise unpackReleased; + auto unpackReleasedFuture = unpackReleased.get_future(); - imageManifest->mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 512; - imageManifest->mLayers.EmplaceBack(oci::ContentDescriptor {"vnd.aos.image.component.full.v1+gzip", - "sha256:4444444444444444444444444444444444444444444444444444444444444444", 1024}); + EXPECT_CALL(mImageHandler, UnpackLayer) + .WillOnce(Invoke([&](const String&, const String& dst, const String&) -> Error { + unpackStarted.set_value(); + unpackReleasedFuture.wait(); - auto imageConfig = std::make_unique(); + WriteFile(std::string(dst.CStr()) + "/data", cUnpackedData); - imageConfig->mRootfs.mDiffIDs.EmplaceBack( - "sha256:5555555555555555555555555555555555555555555555555555555555555555"); + return ErrorEnum::eNone; + })); - mStorageStub.Init(itemsInfo); + auto install1 + = std::async(std::launch::async, [this]() { return mImageManager.InstallItem(ServiceInfo("service1")); }); - auto blobsPath = fs::JoinPath(cTestImagePath, "blobs", "sha256"); - auto layersPath = fs::JoinPath(cTestImagePath, "layers", "sha256"); + unpackStarted.get_future().wait(); - std::filesystem::create_directories(blobsPath.CStr()); - std::filesystem::create_directories(layersPath.CStr()); + // The second item waits for the layer with the same diff ID being unpacked by the first one. + auto install2 = std::async( + std::launch::async, [this]() { return mImageManager.InstallItem(ServiceInfo("service2", cManifest2Digest)); }); - // Create image config blob + std::this_thread::sleep_for(std::chrono::milliseconds(100)); - CreateFile(GetBlobPath(imageManifest->mConfig.mDigest).CStr()); + auto cancel = std::async(std::launch::async, [this]() { return mImageManager.Cancel(); }); - // Create layer blob + // The wait for the layer is canceled while the first item still unpacks it. + auto status = install2.wait_for(std::chrono::seconds(5)); - CreateFile(GetBlobPath(imageManifest->mLayers[0].mDigest).CStr(), imageConfig->mRootfs.mDiffIDs[0].CStr()); + unpackReleased.set_value(); - // Create used layers + EXPECT_EQ(status, std::future_status::ready); - auto layerPath = GetLayerPath(imageConfig->mRootfs.mDiffIDs[0]); + auto err = install2.get(); + EXPECT_TRUE(err.Is(ErrorEnum::eCanceled)) << tests::utils::ErrorToStr(err); - std::filesystem::create_directories(layerPath.CStr()); + // The first item may finish the unpack or fail with eCanceled on the next blob. + install1.wait(); - // Create layer digest + err = cancel.get(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} - CreateFile(fs::JoinPath(layerPath, "digest").CStr(), imageConfig->mRootfs.mDiffIDs[0].CStr()); +TEST_F(SMImageManagerTest, RemoveItemKeepsLayerInCache) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - // Create orphan layers + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String("service"), String("1.0.0"), _)); - for (size_t i = 0; i < 10; i++) { - StaticString digest; + err = mImageManager.RemoveItem("service", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - digest.Format("sha256:%064d", i + 1); + EXPECT_EQ(GetItemState("service"), ItemState(ItemStateEnum::eRemoved)); + EXPECT_TRUE(Exists(UnpackedPath(cDiffID))); +} - auto layerPath = GetLayerPath(digest); +TEST_F(SMImageManagerTest, ItemRemoverRemovesUnusedLayer) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - std::filesystem::create_directories(layerPath.CStr()); - } + err = mImageManager.RemoveItem("service", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - std::promise promise; + const auto layerSize = strlen(cUnpackedData) * 2 + std::to_string(cUnpackedSize).size(); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillRepeatedly(Invoke([&](const String& path, fs::FileInfo& fileInfo, crypto::Hash) -> Error { - fileInfo = GetFileInfoByPath(path); + size_t size = 0; - return ErrorEnum::eNone; - })); - EXPECT_CALL(mOCISpecMock, LoadImageManifest(_, _)) - .WillRepeatedly(DoAll(SetArgReferee<1>(*imageManifest), Return(ErrorEnum::eNone))); - EXPECT_CALL(mSpaceAllocatorMock, FreeSpace(_)).WillOnce(Invoke([&](size_t size) -> Error { - promise.set_value(size); + // The space allocator frees the returned size, so nothing is freed by the image manager. + EXPECT_CALL(mSpaceAllocator, FreeSpace).Times(0); - return ErrorEnum::eNone; - })); - EXPECT_CALL(mOCISpecMock, LoadImageConfig(_, _)) - .WillRepeatedly(DoAll(SetArgReferee<1>(*imageConfig), Return(ErrorEnum::eNone))); - EXPECT_CALL(mImageHandlerMock, GetUnpackedLayerDigest(_)) - .WillRepeatedly(Return(StaticString(imageConfig->mRootfs.mDiffIDs[0]))); + Tie(size, err) = mImageManager.GetItemRemover().RemoveItem("service", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto err = mImageManager.Start(); - EXPECT_TRUE(err.IsNone()) << "Failed to start image manager: " << tests::utils::ErrorToStr(err); + // The layer blob is already removed after the unpack, so only the manifest and config blobs are freed. + EXPECT_EQ(size, layerSize + 2 * cBlobSize); + EXPECT_FALSE(Exists(LayerPath(cDiffID))); + EXPECT_FALSE(Exists(BlobPath(cLayerDigest))); +} - ASSERT_TRUE(promise.get_future().wait_for(std::chrono::milliseconds(cUpdateItemTTL.Milliseconds())) - == std::future_status::ready); +TEST_F(SMImageManagerTest, ItemRemoverKeepsLayerUsedByAnotherItem) +{ + AddService(cManifest2Digest, cLayer2Digest); - err = mImageManager.Stop(); - EXPECT_TRUE(err.IsNone()) << "Failed to stop image manager: " << tests::utils::ErrorToStr(err); + auto err = mImageManager.InstallItem(ServiceInfo("service1")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - for (const auto& entry : std::filesystem::directory_iterator(layersPath.CStr())) { - EXPECT_EQ(GetLayerPath(imageConfig->mRootfs.mDiffIDs[0]), entry.path().c_str()) - << "Orphan layer not removed: " << entry.path().c_str(); - } -} + err = mImageManager.InstallItem(ServiceInfo("service2", cManifest2Digest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); -TEST_F(ImageManagerTest, RemoveOrphansPreservesInstallingBlobs) -{ - // Verify that blobs being downloaded during an active install are not deleted as orphans - // when RemoveOrphans runs concurrently via RemoveItem (called by the space allocator). + err = mImageManager.RemoveItem("service1", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - constexpr auto cManifestDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + size_t size = 0; - UpdateItemInfo itemInfo {"component1", UpdateItemTypeEnum::eComponent, "1.0.0", cManifestDigest}; + Tie(size, err) = mImageManager.GetItemRemover().RemoveItem("service1", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto manifestPath = GetBlobPath(cManifestDigest); + EXPECT_FALSE(Exists(BlobPath(cLayerDigest))); + EXPECT_TRUE(Exists(UnpackedPath(cDiffID))); - auto imageManifest = std::make_unique(); + err = mImageManager.RemoveItem("service2", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - imageManifest->mConfig.mMediaType = "application/vnd.oci.empty.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 2; + Tie(size, err) = mImageManager.GetItemRemover().RemoveItem("service2", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - // Block the manifest download after the file is created so RemoveOrphans can run - // while the install is still in progress. + EXPECT_FALSE(Exists(LayerPath(cDiffID))); +} - std::promise downloadStartedPromise; - std::promise downloadResumePromise; +TEST_F(SMImageManagerTest, CleanupRemovesOrphanLayers) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto downloadStartedFuture = downloadStartedPromise.get_future(); - auto downloadResumeFuture = downloadResumePromise.get_future(); + const auto orphanFile = UnpackedPath(cOrphanDiffID) + "/data"; + const auto strayFile = std::string(cTestImagePath) + "/layers/stray"; - EXPECT_CALL(mDownloaderMock, Download(String(cManifestDigest), _, manifestPath)) - .WillOnce(Invoke([&](const String&, const String&, const String& path) -> Error { - CreateFile(path.CStr()); - downloadStartedPromise.set_value(); - downloadResumeFuture.wait(); - return ErrorEnum::eNone; - })); + WriteFile(orphanFile, "orphan"); + WriteFile(strayFile, "stray"); - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillOnce(DoAll(SetArgReferee<1>(GetFileInfoByDigest(cManifestDigest)), Return(ErrorEnum::eNone))); + std::promise freed; - EXPECT_CALL(mOCISpecMock, LoadImageManifest(manifestPath, _)) - .WillOnce(DoAll(SetArgReferee<1>(*imageManifest), Return(ErrorEnum::eNone))); + EXPECT_CALL(mSpaceAllocator, FreeSpace(strlen("orphan") + strlen("stray"))).WillOnce(InvokeWithoutArgs([&]() { + freed.set_value(); + })); - // Install in background: will block inside the manifest downloader. + err = mImageManager.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto installFuture = std::async(std::launch::async, [&]() { return mImageManager.InstallUpdateItem(itemInfo); }); + EXPECT_EQ(freed.get_future().wait_for(std::chrono::seconds(5)), std::future_status::ready); - // Wait until the manifest file exists on disk and the digest is already recorded - // in the installing item's blob list (added by InstallBlob before calling Download). + err = mImageManager.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - ASSERT_EQ(downloadStartedFuture.wait_for(std::chrono::seconds(5)), std::future_status::ready); + EXPECT_FALSE(Exists(LayerPath(cOrphanDiffID))); + EXPECT_FALSE(Exists(strayFile)); + EXPECT_TRUE(Exists(UnpackedPath(cDiffID))); +} - // Trigger RemoveOrphans via ItemRemoverItf (as the space allocator would do). - // The manifest blob exists on disk but is not yet in storage — without AddInstallingItems - // it would be treated as an orphan and deleted. +TEST_F(SMImageManagerTest, GetLayerPathErrors) +{ + StaticString path; - static_cast(&mImageManager)->RemoveItem("nonexistent", "1.0.0"); + auto err = mImageManager.GetLayerPath("malformed", path); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); - EXPECT_TRUE(std::filesystem::exists(manifestPath.CStr())) - << "Manifest blob was incorrectly deleted as orphan during active install"; + err = mImageManager.GetLayerPath(cDiffID, path); + EXPECT_TRUE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); +} - // Let the download finish and verify the install completes successfully. +TEST_F(SMImageManagerTest, GetBlobPath) +{ + StaticString path; - downloadResumePromise.set_value(); + auto err = mImageManager.GetBlobPath(cLayerDigest, path); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); - auto err = installFuture.get(); - EXPECT_TRUE(err.IsNone()) << "Install failed: " << tests::utils::ErrorToStr(err); + EXPECT_EQ(BlobPath(cLayerDigest), path.CStr()); } } // namespace aos::sm::imagemanager diff --git a/src/core/sm/imagemanager/tests/mocks/blobinfoprovidermock.hpp b/src/core/sm/imagemanager/tests/mocks/blobinfoprovidermock.hpp deleted file mode 100644 index cca1151a3..000000000 --- a/src/core/sm/imagemanager/tests/mocks/blobinfoprovidermock.hpp +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_SM_IMAGEMANAGER_TESTS_MOCKS_BLOBINFOPROVIDERMOCK_HPP_ -#define AOS_CORE_SM_IMAGEMANAGER_TESTS_MOCKS_BLOBINFOPROVIDERMOCK_HPP_ - -#include - -#include - -namespace aos::sm::imagemanager { - -/** - * Blob info provider mock. - */ -class BlobInfoProviderMock : public BlobInfoProviderItf { -public: - MOCK_METHOD( - Error, GetBlobsInfo, (const Array>&, Array>&), (override)); -}; - -} // namespace aos::sm::imagemanager - -#endif diff --git a/src/core/sm/imagemanager/tests/mocks/iteminfoprovidermock.hpp b/src/core/sm/imagemanager/tests/mocks/iteminfoprovidermock.hpp deleted file mode 100644 index 211b64f46..000000000 --- a/src/core/sm/imagemanager/tests/mocks/iteminfoprovidermock.hpp +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2026 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_SM_IMAGEMANAGER_TESTS_MOCKS_ITEMINFOPROVIDERMOCK_HPP_ -#define AOS_CORE_SM_IMAGEMANAGER_TESTS_MOCKS_ITEMINFOPROVIDERMOCK_HPP_ - -#include - -#include - -namespace aos::sm::imagemanager { - -/** - * Item info provider mock. - */ -class ItemInfoProviderMock : public ItemInfoProviderItf { -public: - MOCK_METHOD(Error, GetBlobPath, (const String&, String&), (const override)); - MOCK_METHOD(Error, GetLayerPath, (const String&, String&), (const override)); -}; - -} // namespace aos::sm::imagemanager - -#endif diff --git a/src/core/sm/imagemanager/tests/stubs/storagestub.hpp b/src/core/sm/imagemanager/tests/stubs/storagestub.hpp index fb4948faf..b970d14dc 100644 --- a/src/core/sm/imagemanager/tests/stubs/storagestub.hpp +++ b/src/core/sm/imagemanager/tests/stubs/storagestub.hpp @@ -6,149 +6,87 @@ #ifndef AOS_SM_IMAGE_MANAGER_TESTS_STUBS_STORAGESTUB_HPP_ #define AOS_SM_IMAGE_MANAGER_TESTS_STUBS_STORAGESTUB_HPP_ -#include -#include -#include +#include +#include #include namespace aos::sm::imagemanager { +/** + * Storage stub. + */ class StorageStub : public StorageItf { public: - void Init(std::vector initialItems) + Error AddDeployableItem(const common::imagemanager::DeployableItemInfo& item) override { - for (const auto& item : initialItems) { - mItemsList.push_back(item); - } + return mStorage.AddDeployableItem(item); } - /** - * Returns future to wait when item removed. - * - * @return future. - */ - auto GetRemoveFuture() + Error UpdateDeployableItem(const common::imagemanager::DeployableItemInfo& item) override { - mRemovePromises.emplace(); - - return mRemovePromises.back().get_future(); + return mStorage.UpdateDeployableItem(item); } - /** - * Adds update item to storage. - * - * @param updateItem update item to add. - * @return Error. - */ - Error AddUpdateItem(const UpdateItemData& updateItem) override + Error RemoveDeployableItem(const String& itemID, const String& version) override { - auto it = std::find_if(mItemsList.begin(), mItemsList.end(), [&](const UpdateItemData& item) { - return item.mID == updateItem.mID && item.mVersion == updateItem.mVersion; - }); - if (it != mItemsList.end()) { - return ErrorEnum::eAlreadyExist; - } - - mItemsList.push_back(updateItem); + return mStorage.RemoveDeployableItem(itemID, version); + } - return ErrorEnum::eNone; + Error GetDeployableItem( + const String& itemID, const String& version, common::imagemanager::DeployableItemInfo& item) override + { + return mStorage.GetDeployableItem(itemID, version, item); } - /** - * Updates update item in storage. - * - * @param updateItem update item to update. - * @return Error. - */ - Error UpdateUpdateItem(const UpdateItemData& updateItem) override + Error GetAllDeployableItems(Array& items) override { - auto it = std::find_if(mItemsList.begin(), mItemsList.end(), [&](const UpdateItemData& item) { - return item.mID == updateItem.mID && item.mVersion == updateItem.mVersion; - }); - if (it != mItemsList.end()) { - *it = updateItem; + return mStorage.GetAllDeployableItems(items); + } - return ErrorEnum::eNone; - } + RetWithError GetDeployableItemsCount() override { return mStorage.GetDeployableItemsCount(); } - mItemsList.push_back(updateItem); + Error AddItemBlob( + const String& itemID, const String& version, const common::imagemanager::ItemBlobInfo& blob) override + { + return mStorage.AddItemBlob(itemID, version, blob); + } - return ErrorEnum::eNone; + Error GetItemBlobs( + const String& itemID, const String& version, Array& blobs) override + { + return mStorage.GetItemBlobs(itemID, version, blobs); } - /** - * Removes previously stored update item. - * - * @param itemID update item ID to remove. - * @param version update item version. - * @return Error. - */ - Error RemoveUpdateItem(const String& itemID, const String& version) override + RetWithError IsBlobUsed(const String& digest) override { return mStorage.IsBlobUsed(digest); } + + RetWithError IsLayerUsed(const String& diffID) override { - auto it = std::find_if(mItemsList.begin(), mItemsList.end(), - [&](const UpdateItemData& item) { return item.mID == itemID && item.mVersion == version; }); - if (it == mItemsList.end()) { - return ErrorEnum::eNotFound; - } + auto items = std::make_unique>(); - if (!mRemovePromises.empty()) { - mRemovePromises.front().set_value(*it); - mRemovePromises.pop(); + if (auto err = mStorage.GetAllDeployableItems(*items); !err.IsNone()) { + return {false, err}; } - mItemsList.erase(it); - - return ErrorEnum::eNone; - } + for (const auto& item : *items) { + auto blobs = std::make_unique>(); - /** - * Returns update item versions by item ID. - * - * @param itemID update item ID. - * @param[out] itemData array to store update item data. - * @return Error. - */ - Error GetUpdateItem(const String& itemID, Array& itemData) override - { - for (const auto& item : mItemsList) { - if (item.mID == itemID) { - if (auto err = itemData.PushBack(item); !err.IsNone()) { - return err; - } + if (auto err = mStorage.GetItemBlobs(item.mItemID, item.mVersion, *blobs); !err.IsNone()) { + return {false, err}; } - } - - return ErrorEnum::eNone; - } - /** - * Returns all update items. - * - * @param itemsData[out] array to store update items data. - * @return Error. - */ - Error GetAllUpdateItems(Array& itemsData) override - { - for (const auto& item : mItemsList) { - if (auto err = itemsData.PushBack(item); !err.IsNone()) { - return err; + for (const auto& blob : *blobs) { + if (blob.mIsLayer && blob.mDiffID == diffID) { + return true; + } } } - return ErrorEnum::eNone; + return false; } - /** - * Returns count of stored update items. - * - * @return RetWithError. - */ - RetWithError GetUpdateItemsCount() override { return RetWithError(mItemsList.size()); } - private: - std::list mItemsList; - std::queue> mRemovePromises; + common::imagemanager::StorageStub mStorage; }; } // namespace aos::sm::imagemanager diff --git a/src/core/sm/launcher/launcher.cpp b/src/core/sm/launcher/launcher.cpp index ec891c03a..d9249373b 100644 --- a/src/core/sm/launcher/launcher.cpp +++ b/src/core/sm/launcher/launcher.cpp @@ -15,8 +15,9 @@ namespace aos::sm::launcher { **********************************************************************************************************************/ Error Launcher::Init(AllocatorItf& allocator, const Array& runtimes, - imagemanager::ImageManagerItf& imageManager, SenderItf& sender, StorageItf& storage, oci::OCISpecItf& ociSpec, - imagemanager::ItemInfoProviderItf& itemInfoProvider, cloudconnection::CloudConnectionItf& cloudConnection, + imagemanager::ImageManagerItf& imageManager, common::imagemanager::ItemStatusProviderItf& itemStatusProvider, + SenderItf& sender, StorageItf& storage, oci::OCISpecItf& ociSpec, + common::imagemanager::ItemInfoProviderItf& itemInfoProvider, cloudconnection::CloudConnectionItf& cloudConnection, networkmanager::NetworkManagerItf& networkManager, InstanceIDProviderItf& instanceIDProvider, resourcemanager::ResourceInfoProviderItf& resourceInfoProvider) { @@ -31,6 +32,7 @@ Error Launcher::Init(AllocatorItf& allocator, const Array& runtimes } mImageManager = &imageManager; + mItemStatusProvider = &itemStatusProvider; mStorage = &storage; mSender = &sender; mOCISpec = &ociSpec; @@ -70,7 +72,8 @@ Error Launcher::Start() << Log::Field("type", runtimeInfo.mRuntimeType); } - mIsRunning = true; + mIsRunning = true; + mInstallsCanceled = false; if (auto err = mRebootThread.Run([this](void*) { RunRebootThread(); }); !err.IsNone()) { return AOS_ERROR_WRAP(err); @@ -116,6 +119,9 @@ Error Launcher::Stop() stopErr = AOS_ERROR_WRAP(err); } + // Don't wait for the items being installed by the launch in progress. + CancelInstalls(); + { UniqueLock lock {mMutex}; @@ -670,11 +676,14 @@ Error Launcher::LoadInstanceData(InstanceData& instanceData) return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); } - if (auto err = GetInstanceConfigs(instanceData.mInfo, *itemConfig, *imageConfig); !err.IsNone()) { + // The broken content is not repaired on start: the repair needs the CM, which may be not connected yet. The first + // UpdateInstances prepares again the instance whose configs aren't loaded and repairs the broken content. + if (auto err = LoadInstanceConfigs(instanceData.mInfo, *itemConfig, *imageConfig); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - instanceData.mOfflineTTL = itemConfig->mOfflineTTL; + instanceData.mOfflineTTL = itemConfig->mOfflineTTL; + instanceData.mConfigsLoaded = true; return ErrorEnum::eNone; } @@ -736,23 +745,46 @@ void Launcher::UpdateInstancesImpl(Array& stopInstances, const Ar mFirstStart = false; }); - if (auto err = mLaunchPool.Run(); !err.IsNone()) { - LOG_ERR() << "Can't start thread pool" << Log::Field(AOS_ERROR_WRAP(err)); + // The buffers are allocated before the thread pool is started, so an allocation failure doesn't leave it running. + auto removeItems = MakeUnique>(mAllocator); + if (!removeItems) { + LOG_ERR() << "Failed to allocate remove update items" << Log::Field(ErrorEnum::eNoMemory); return; } - if (auto err = AppendInstancesWithModifiedParams(startInstances, stopInstances); !err.IsNone()) { - LOG_ERR() << "Failed to append instances with modified params to stop list" << Log::Field(AOS_ERROR_WRAP(err)); + auto installItems = MakeUnique>(mAllocator); + if (!installItems) { + LOG_ERR() << "Failed to allocate install items" << Log::Field(ErrorEnum::eNoMemory); + + return; } - auto removeItems = MakeUnique>(mAllocator); - if (!removeItems) { - LOG_ERR() << "Failed to allocate remove update items" << Log::Field(ErrorEnum::eNoMemory); + // An item is repaired at most once per request, so the repaired items are shared by the prepare and start stages. + auto repairedItems = MakeUnique>(mAllocator); + if (!repairedItems) { + LOG_ERR() << "Failed to allocate repaired items" << Log::Field(ErrorEnum::eNoMemory); + + return; + } + + auto processedInstances = MakeUnique>(mAllocator); + if (!processedInstances) { + LOG_ERR() << "Failed to allocate processed instances" << Log::Field(ErrorEnum::eNoMemory); + + return; + } + + if (auto err = mLaunchPool.Run(); !err.IsNone()) { + LOG_ERR() << "Can't start thread pool" << Log::Field(AOS_ERROR_WRAP(err)); return; } + if (auto err = AppendInstancesWithModifiedParams(startInstances, stopInstances); !err.IsNone()) { + LOG_ERR() << "Failed to append instances with modified params to stop list" << Log::Field(AOS_ERROR_WRAP(err)); + } + if (!mFirstStart) { GetRemoveUpdateItems(stopInstances, startInstances, *removeItems); } @@ -766,15 +798,24 @@ void Launcher::UpdateInstancesImpl(Array& stopInstances, const Ar << Log::Field("installCount", startInstances.Size()); RemoveUpdateItems(*removeItems); - InstallUpdateItems(startInstances); + auto installError = InstallUpdateItems(startInstances, *installItems); LOG_INF() << "[profiling] Install items end"; - PrepareInstances(startInstances); + PrepareInstances(startInstances, *installItems, installError, *processedInstances); + RepairInstances(*processedInstances, *repairedItems, &Launcher::AddPrepareInstanceTask); } StartNetworks(startInstances); - StartInstances(startInstances); + + processedInstances->Clear(); + + StartInstances(startInstances, *processedInstances); + + // The repair needs the CM, which may be not connected on module start: the first request repairs the failed items. + if (!mFirstStart) { + RepairInstances(*processedInstances, *repairedItems, &Launcher::AddStartInstanceTask); + } if (auto err = mLaunchPool.Shutdown(); !err.IsNone()) { LOG_ERR() << "Thread pool shutdown failed" << Log::Field(AOS_ERROR_WRAP(err)); @@ -939,11 +980,13 @@ Error Launcher::PrepareInstance(InstanceData& instanceData) return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); } - if (auto err = GetInstanceConfigs(instanceData.mInfo, *itemConfig, *imageConfig); !err.IsNone()) { + // The broken content is repaired by RepairInstances once per item after all instances are prepared. + if (auto err = LoadInstanceConfigs(instanceData.mInfo, *itemConfig, *imageConfig); !err.IsNone()) { return err; } - instanceData.mOfflineTTL = itemConfig->mOfflineTTL; + instanceData.mOfflineTTL = itemConfig->mOfflineTTL; + instanceData.mConfigsLoaded = true; if (auto err = CreateNetwork(instanceData, *itemConfig, *imageConfig); !err.IsNone()) { return err; @@ -952,43 +995,66 @@ Error Launcher::PrepareInstance(InstanceData& instanceData) return ErrorEnum::eNone; } -void Launcher::PrepareInstances(const Array& startInstances) +void Launcher::PrepareInstances(const Array& startInstances, const Array& installItems, + const Error& installError, Array& preparedInstances) { LOG_INF() << "[profiling] Prepare instances begin" << Log::Field("count", startInstances.Size()); for (const auto& instance : startInstances) { auto instanceData = FindInstanceData(instance); - if (instanceData) { - LOG_DBG() << "Instance data already exists" << Log::Field("instance", instance); + // The running instance whose configs failed to load on module start is prepared again to load them. + auto existingInstance = instanceData && instanceData->mStatus.mState != InstanceStateEnum::eFailed + && (instanceData->mInfo.mType != UpdateItemTypeEnum::eService || instanceData->mConfigsLoaded); + Error err; + + if (!instanceData) { + Tie(instanceData, err) = AddInstanceData(instance); + if (!err.IsNone()) { + LOG_ERR() << "Failed to add instance data" << Log::Field("instance", instance) + << Log::Field(AOS_ERROR_WRAP(err)); + continue; + } + } else { SetInstanceState(*instanceData, InstanceStateEnum::eInactive); + } - continue; + auto installItem = installItems.FindIf([&instance](const auto& item) { + return item.mID == instance.mItemID && item.mVersion == instance.mVersion; + }); + err = installError; + if (err.IsNone() && installItem != installItems.end()) { + err = installItem->mError; } - Error err; + // The instance which is already running doesn't use the item being installed, so it keeps running. + if (existingInstance) { + if (!err.IsNone()) { + LOG_WRN() << "Item install failed, keep running instance" << Log::Field("instance", instance) + << Log::Field(err); + } + + continue; + } - Tie(instanceData, err) = AddInstanceData(instance); if (!err.IsNone()) { - LOG_ERR() << "Failed to add instance data" << Log::Field("instance", instance) - << Log::Field(AOS_ERROR_WRAP(err)); + SetInstanceState(*instanceData, InstanceStateEnum::eFailed, err); continue; } - err = mLaunchPool.AddTask([this, instanceData](void*) { - if (auto err = PrepareInstance(*instanceData); !err.IsNone()) { - LOG_ERR() << "Failed to start instance" << Log::Field("instance", instanceData->mInfo) - << Log::Field(AOS_ERROR_WRAP(err)); - - SetInstanceState(*instanceData, InstanceStateEnum::eFailed, AOS_ERROR_WRAP(err)); - } - }); - if (!err.IsNone()) { + if (err = AddPrepareInstanceTask(*instanceData); !err.IsNone()) { LOG_ERR() << "Failed to prepare instance" << Log::Field("instance", instance) << Log::Field(AOS_ERROR_WRAP(err)); SetInstanceState(*instanceData, InstanceStateEnum::eFailed, AOS_ERROR_WRAP(err)); + + continue; + } + + if (err = preparedInstances.PushBack(static_cast(instance)); !err.IsNone()) { + LOG_ERR() << "Failed to add prepared instance" << Log::Field("instance", instance) + << Log::Field(AOS_ERROR_WRAP(err)); } } @@ -999,6 +1065,26 @@ void Launcher::PrepareInstances(const Array& startInstances) LOG_INF() << "[profiling] Prepare instances end"; } +Error Launcher::AddPrepareInstanceTask(InstanceData& instanceData) +{ + if (auto err = mLaunchPool.AddTask([this, &instanceData](void*) { + // The instance which failed the previous prepare is prepared again after its item is repaired. + SetInstanceState(instanceData, InstanceStateEnum::eInactive); + + if (auto err = PrepareInstance(instanceData); !err.IsNone()) { + LOG_ERR() << "Failed to prepare instance" << Log::Field("instance", instanceData.mInfo) + << Log::Field(AOS_ERROR_WRAP(err)); + + SetInstanceState(instanceData, InstanceStateEnum::eFailed, AOS_ERROR_WRAP(err)); + } + }); + !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + Error Launcher::AddStartNetworkTask(InstanceData& instanceData) { if (auto err = mLaunchPool.AddTask([this, &instanceData](void*) { @@ -1162,7 +1248,7 @@ void Launcher::StopNetworks(const Array& stopInstances) LOG_INF() << "[profiling] Stop networks end"; } -void Launcher::StartInstances(const Array& startInstances) +void Launcher::StartInstances(const Array& startInstances, Array& startedInstances) { LOG_INF() << "[profiling] Start instances begin" << Log::Field("count", startInstances.Size()); @@ -1187,6 +1273,13 @@ void Launcher::StartInstances(const Array& startInstances) << Log::Field(AOS_ERROR_WRAP(err)); SetInstanceState(*instanceData, InstanceStateEnum::eFailed, AOS_ERROR_WRAP(err)); + + continue; + } + + if (auto err = startedInstances.PushBack(static_cast(instance)); !err.IsNone()) { + LOG_ERR() << "Failed to add started instance" << Log::Field("instance", instance) + << Log::Field(AOS_ERROR_WRAP(err)); } } @@ -1197,6 +1290,109 @@ void Launcher::StartInstances(const Array& startInstances) LOG_INF() << "[profiling] Start instances end"; } +void Launcher::RepairInstances( + const Array& instances, Array& repairedItems, AddInstanceTaskFunc addRetryTask) +{ + const auto isBroken = [](const InstanceData& instanceData) { + return instanceData.mStatus.mState == InstanceStateEnum::eFailed + && instanceData.mStatus.mError.Is(ErrorEnum::eIntegrity); + }; + + const auto findItem = [&repairedItems](const InstanceInfo& instance) { + return repairedItems.FindIf([&instance](const InstallItem& item) { + return item.mID == instance.mItemID && item.mVersion == instance.mVersion; + }); + }; + + // The items repaired by the previous stage of the request are not repaired again. + const auto firstNewItem = repairedItems.Size(); + + { + LockGuard lock {mMutex}; + + for (const auto& instance : instances) { + auto instanceData = FindInstanceData(instance); + if (!instanceData || !isBroken(*instanceData) || findItem(instanceData->mInfo) != repairedItems.end()) { + continue; + } + + const auto& info = instanceData->mInfo; + + if (auto err = repairedItems.EmplaceBack( + InstallItem {{info.mItemID, info.mType, info.mVersion, info.mManifestDigest}, {}}); + !err.IsNone()) { + LOG_ERR() << "Failed to add repair item" << Log::Field("itemID", info.mItemID) + << Log::Field("version", info.mVersion) << Log::Field(AOS_ERROR_WRAP(err)); + } + } + } + + if (repairedItems.Size() == firstNewItem) { + return; + } + + LOG_INF() << "[profiling] Repair instances begin" << Log::Field("itemCount", repairedItems.Size() - firstNewItem); + + // Each item is repaired once per request, the instances which still fail stay failed. + for (auto item = repairedItems.begin() + firstNewItem; item != repairedItems.end(); ++item) { + if (auto err = mLaunchPool.AddTask([this, item](void*) { + LOG_WRN() << "Repair update item" << Log::Field("itemID", item->mID) + << Log::Field("version", item->mVersion); + + item->mError = InstallUpdateItem(*item); + if (!item->mError.IsNone()) { + LOG_ERR() << "Repair update item failed" << Log::Field("itemID", item->mID) + << Log::Field("version", item->mVersion) << Log::Field(AOS_ERROR_WRAP(item->mError)); + } + }); + !err.IsNone()) { + item->mError = AOS_ERROR_WRAP(err); + } + } + + if (auto err = mLaunchPool.Wait(); !err.IsNone()) { + LOG_ERR() << "Thread pool wait failed" << Log::Field(AOS_ERROR_WRAP(err)); + } + + for (const auto& instance : instances) { + InstanceData* instanceData = nullptr; + + { + LockGuard lock {mMutex}; + + instanceData = FindInstanceData(instance); + if (!instanceData || !isBroken(*instanceData)) { + continue; + } + + auto item = findItem(instanceData->mInfo); + if (item == repairedItems.end() || item < repairedItems.begin() + firstNewItem) { + continue; + } + + // The instance of an item which can't be repaired fails with the repair error. + if (!item->mError.IsNone()) { + instanceData->mStatus.mError = item->mError; + + continue; + } + } + + if (auto err = (this->*addRetryTask)(*instanceData); !err.IsNone()) { + LOG_ERR() << "Failed to retry instance" << Log::Field("instance", instance) + << Log::Field(AOS_ERROR_WRAP(err)); + + SetInstanceState(*instanceData, InstanceStateEnum::eFailed, AOS_ERROR_WRAP(err)); + } + } + + if (auto err = mLaunchPool.Wait(); !err.IsNone()) { + LOG_ERR() << "Thread pool wait failed" << Log::Field(AOS_ERROR_WRAP(err)); + } + + LOG_INF() << "[profiling] Repair instances end"; +} + Error Launcher::AddStartInstanceTask(InstanceData& instanceData) { auto runtime = FindInstanceRuntime(instanceData.mInfo.mRuntimeID); @@ -1267,6 +1463,55 @@ Error Launcher::AppendInstancesWithModifiedParams( return ErrorEnum::eNone; } +Error Launcher::InstallUpdateItem(const imagemanager::UpdateItemInfo& item) +{ + { + LockGuard lock {mMutex}; + + if (mInstallsCanceled) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eCanceled, "launcher is stopping")); + } + + mNumInstalls++; + } + + auto err = mImageManager->InstallItem(item); + + LockGuard lock {mMutex}; + + mNumInstalls--; + + if (auto notifyErr = mCondVar.NotifyAll(); !notifyErr.IsNone()) { + LOG_ERR() << "Can't notify installs" << Log::Field(AOS_ERROR_WRAP(notifyErr)); + } + + return err; +} + +void Launcher::CancelInstalls() +{ + { + LockGuard lock {mMutex}; + + mInstallsCanceled = true; + } + + // An install which passed the check above may reach the image manager after Cancel resets its context, so cancel + // again until all of them return. + while (true) { + if (auto err = mImageManager->Cancel(); !err.IsNone()) { + LOG_ERR() << "Can't cancel installs" << Log::Field(AOS_ERROR_WRAP(err)); + } + + UniqueLock lock {mMutex}; + + if (auto err = mCondVar.Wait(lock, cCancelInstallsPeriod, [this]() { return mNumInstalls == 0; }); + err.IsNone() || !err.Is(ErrorEnum::eTimeout)) { + return; + } + } +} + Error Launcher::StartLaunch() { LockGuard lock {mMutex}; @@ -1396,7 +1641,7 @@ void Launcher::RemoveUpdateItems(const Array& removeItems) { for (const auto& updateItem : removeItems) { if (auto err = mLaunchPool.AddTask([this, &updateItem](void*) { - if (auto err = mImageManager->RemoveUpdateItem(updateItem.mItemID, updateItem.mVersion); + if (auto err = mImageManager->RemoveItem(updateItem.mItemID, updateItem.mVersion); !err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { LOG_ERR() << "Remove update item failed" << Log::Field("itemID", updateItem.mItemID) << Log::Field("version", updateItem.mVersion) << Log::Field(AOS_ERROR_WRAP(err)); @@ -1415,51 +1660,55 @@ void Launcher::RemoveUpdateItems(const Array& removeItems) } } -void Launcher::InstallUpdateItems(const Array& startInstances) +Error Launcher::InstallUpdateItems(const Array& startInstances, Array& installItems) { - auto currentItems = MakeUnique>(mAllocator); + // The image manager reports all stored item versions, including the ones kept in the cache. + auto currentItems = MakeUnique>(mAllocator); if (!currentItems) { LOG_ERR() << "Failed to allocate current items" << Log::Field(ErrorEnum::eNoMemory); - return; - } - - auto installItems = MakeUnique>(mAllocator); - if (!installItems) { - LOG_ERR() << "Failed to allocate install items" << Log::Field(ErrorEnum::eNoMemory); - - return; + return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); } - if (auto err = mImageManager->GetAllInstalledItems(*currentItems); !err.IsNone()) { + if (auto err = mItemStatusProvider->GetItemsStatuses(*currentItems); !err.IsNone()) { LOG_ERR() << "Get update items statuses failed" << Log::Field(AOS_ERROR_WRAP(err)); + + currentItems->Clear(); } for (const auto& startInstance : startInstances) { if (auto it = currentItems->FindIf([&startInstance](const auto& item) { - return item.mID == startInstance.mItemID && item.mVersion == startInstance.mVersion; + return item.mItemID == startInstance.mItemID && item.mVersion == startInstance.mVersion; }); it != currentItems->end() && it->mState == ItemStateEnum::eInstalled) { continue; } - if (auto it = installItems->FindIf([&startInstance](const auto& item) { + if (auto it = installItems.FindIf([&startInstance](const auto& item) { return item.mID == startInstance.mItemID && item.mVersion == startInstance.mVersion; }); - it == installItems->end()) { - (void)installItems->EmplaceBack(imagemanager::UpdateItemInfo { - startInstance.mItemID, startInstance.mType, startInstance.mVersion, startInstance.mManifestDigest}); + it == installItems.end()) { + if (auto err = installItems.EmplaceBack(InstallItem { + {startInstance.mItemID, startInstance.mType, startInstance.mVersion, startInstance.mManifestDigest}, + {}}); + !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } } } - for (const auto& installItem : *installItems) { - if (auto err = mLaunchPool.AddTask([this, installItem](void*) { - if (auto err = mImageManager->InstallUpdateItem(installItem); !err.IsNone()) { + for (auto& installItem : installItems) { + if (auto err = mLaunchPool.AddTask([this, &installItem](void*) { + if (auto err = InstallUpdateItem(installItem); !err.IsNone()) { + installItem.mError = AOS_ERROR_WRAP(err); + LOG_ERR() << "Install update item failed" << Log::Field("itemID", installItem.mID) << Log::Field("version", installItem.mVersion) << Log::Field(AOS_ERROR_WRAP(err)); } }); !err.IsNone()) { + installItem.mError = AOS_ERROR_WRAP(err); + LOG_ERR() << "Install update item failed" << Log::Field("itemID", installItem.mID) << Log::Field("version", installItem.mVersion) << Log::Field(AOS_ERROR_WRAP(err)); @@ -1468,8 +1717,10 @@ void Launcher::InstallUpdateItems(const Array& startInstances) } if (auto err = mLaunchPool.Wait(); !err.IsNone()) { - LOG_ERR() << "Thread pool wait failed" << Log::Field(AOS_ERROR_WRAP(err)); + return AOS_ERROR_WRAP(err); } + + return ErrorEnum::eNone; } RetWithError Launcher::AddInstanceData(const InstanceInfo& instanceInfo) @@ -1504,7 +1755,7 @@ Error Launcher::ReleaseInstance(const InstanceData& instanceData) { LOG_DBG() << "Remove instance" << Log::Field("instance", instanceData.mInfo); - if (auto err = mStorage->RemoveInstanceInfo(instanceData.mInfo); !err.IsNone()) { + if (auto err = mStorage->RemoveInstanceInfo(instanceData.mInfo); !err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { return AOS_ERROR_WRAP(err); } @@ -1569,10 +1820,9 @@ void Launcher::SetInstanceState(InstanceData& instance, const InstanceState& sta instance.mStatus.mError = error; } -Error Launcher::GetInstanceConfigs( +Error Launcher::LoadInstanceConfigs( const InstanceInfo& instance, oci::ItemConfig& itemConfig, oci::ImageConfig& imageConfig) { - LOG_DBG() << "Get instance configs" << Log::Field("instance", instance); auto path = MakeUnique>(mAllocator); if (!path) { diff --git a/src/core/sm/launcher/launcher.hpp b/src/core/sm/launcher/launcher.hpp index 6602be13e..60aa9e3c3 100644 --- a/src/core/sm/launcher/launcher.hpp +++ b/src/core/sm/launcher/launcher.hpp @@ -8,14 +8,17 @@ #include #include +#include +#include #include #include #include #include #include #include +#include #include -#include +#include #include #include @@ -48,6 +51,7 @@ class Launcher : public LauncherItf, * @param allocator allocator to use for temporary objects. * @param runtimes available runtimes. * @param imageManager image manager. + * @param itemStatusProvider item status provider. * @param statusSender sender. * @param storage storage. * @param ociSpec OCI spec. @@ -60,10 +64,10 @@ class Launcher : public LauncherItf, * @return Error. */ Error Init(AllocatorItf& allocator, const Array& runtimes, imagemanager::ImageManagerItf& imageManager, - SenderItf& sender, StorageItf& storage, oci::OCISpecItf& ociSpec, - imagemanager::ItemInfoProviderItf& itemInfoProvider, cloudconnection::CloudConnectionItf& cloudConnection, - networkmanager::NetworkManagerItf& networkManager, InstanceIDProviderItf& instanceIDProvider, - resourcemanager::ResourceInfoProviderItf& resourceInfoProvider); + common::imagemanager::ItemStatusProviderItf& itemStatusProvider, SenderItf& sender, StorageItf& storage, + oci::OCISpecItf& ociSpec, common::imagemanager::ItemInfoProviderItf& itemInfoProvider, + cloudconnection::CloudConnectionItf& cloudConnection, networkmanager::NetworkManagerItf& networkManager, + InstanceIDProviderItf& instanceIDProvider, resourcemanager::ResourceInfoProviderItf& resourceInfoProvider); /** * Starts launcher. @@ -163,6 +167,7 @@ class Launcher : public LauncherItf, InstanceStatus mStatus; StaticString mInstanceID; Duration mOfflineTTL; + bool mConfigsLoaded {}; }; struct UpdateItemInfo { @@ -170,9 +175,16 @@ class Launcher : public LauncherItf, StaticString mVersion; }; - static constexpr auto cOIDNamespace = "6ba7b812-9dad-11d1-80b4-00c04fd430c8"; - static constexpr auto cThreadTaskSize = 512; - static constexpr auto cMaxNumSubscribers = 4; + struct InstallItem : imagemanager::UpdateItemInfo { + Error mError; + }; + + using AddInstanceTaskFunc = Error (Launcher::*)(InstanceData& instanceData); + + static constexpr auto cOIDNamespace = "6ba7b812-9dad-11d1-80b4-00c04fd430c8"; + static constexpr auto cThreadTaskSize = 512; + static constexpr auto cMaxNumSubscribers = 4; + static constexpr auto cCancelInstallsPeriod = Time::cMilliseconds * 100; void OnConnect() override; void OnDisconnect() override; @@ -190,10 +202,14 @@ class Launcher : public LauncherItf, void StopAllInstances(); void StopAllNetworks(); Error PrepareInstance(InstanceData& instanceData); - void PrepareInstances(const Array& startInstances); + void PrepareInstances(const Array& startInstances, const Array& installItems, + const Error& installError, Array& preparedInstances); + Error AddPrepareInstanceTask(InstanceData& instanceData); void StartNetworks(const Array& startInstances); Error AddStartNetworkTask(InstanceData& instanceData); - void StartInstances(const Array& startInstances); + void StartInstances(const Array& startInstances, Array& startedInstances); + void RepairInstances( + const Array& instances, Array& repairedItems, AddInstanceTaskFunc addRetryTask); Error AddStartInstanceTask(InstanceData& instanceData); Error StartInstance(aos::sm::launcher::RuntimeItf* runtime, InstanceData& instanceData) const; Error AppendInstancesWithModifiedParams( @@ -203,12 +219,14 @@ class Launcher : public LauncherItf, void GetRemoveUpdateItems(const Array& stopInstances, const Array& startInstances, Array& removeItems); void RemoveUpdateItems(const Array& removeItems); - void InstallUpdateItems(const Array& startInstances); + Error InstallUpdateItems(const Array& startInstances, Array& installItems); + Error InstallUpdateItem(const imagemanager::UpdateItemInfo& item); + void CancelInstalls(); RetWithError AddInstanceData(const InstanceInfo& instanceInfo); Error ReleaseInstance(const InstanceData& instanceData); void RemoveInstances(const Array& instances); void SetInstanceState(InstanceData& instance, const InstanceState& state, const Error& error = ErrorEnum::eNone); - Error GetInstanceConfigs(const InstanceInfo& instance, oci::ItemConfig& itemConfig, oci::ImageConfig& imageConfig); + Error LoadInstanceConfigs(const InstanceInfo& instance, oci::ItemConfig& itemConfig, oci::ImageConfig& imageConfig); Error GetInstanceNetworkConfig(const InstanceInfo& instance, const oci::ItemConfig& itemConfig, const oci::ImageConfig& imageConfig, networkmanager::InstanceNetworkConfig& networkConfig); Error CreateNetwork( @@ -237,14 +255,17 @@ class Launcher : public LauncherItf, mutable Mutex mMutex; Mutex mSubscribersMutex; mutable ConditionalVariable mCondVar; + size_t mNumInstalls {}; + bool mInstallsCanceled {}; StaticArray mInstances; StaticMap, cMaxNumNodeRuntimes> mRuntimes; StaticArray, cMaxNumNodeRuntimes> mRebootQueue; imagemanager::ImageManagerItf* mImageManager {}; + common::imagemanager::ItemStatusProviderItf* mItemStatusProvider {}; StorageItf* mStorage {}; SenderItf* mSender {}; oci::OCISpecItf* mOCISpec {}; - imagemanager::ItemInfoProviderItf* mItemInfoProvider {}; + common::imagemanager::ItemInfoProviderItf* mItemInfoProvider {}; cloudconnection::CloudConnectionItf* mCloudConnection {}; networkmanager::NetworkManagerItf* mNetworkManager {}; InstanceIDProviderItf* mInstanceIDProvider {}; diff --git a/src/core/sm/launcher/launcher.md b/src/core/sm/launcher/launcher.md index 4cf6025ba..1c5066197 100644 --- a/src/core/sm/launcher/launcher.md +++ b/src/core/sm/launcher/launcher.md @@ -27,7 +27,13 @@ It requires the following interfaces: * [aos::sm::launcher::RuntimeItf](itf/runtime.hpp) - launch different kind of items on different runtimes, and provides instance monitoring data (implements `aos::monitoring::InstanceMonitoringProviderItf` interface); * [aos::sm::launcher::StorageItf](itf/storage.hpp) - persistently store current instances; -* [aos::sm::launcher::SenderItf](itf/sender.hpp) - sends node and updated instances statuses. +* [aos::sm::launcher::SenderItf](itf/sender.hpp) - sends node and updated instances statuses; +* [aos::sm::imagemanager::ImageManagerItf](../imagemanager/itf/imagemanager.hpp) - installs and removes update items, + cancels the items being installed when the launcher is stopped; +* [aos::common::imagemanager::ItemStatusProviderItf](../../common/imagemanager/itf/itemstatusprovider.hpp) - provides + the states of the stored update items; +* [aos::common::imagemanager::ItemInfoProviderItf](../../common/imagemanager/itf/iteminfoprovider.hpp) - provides the + local paths of the blobs. ```mermaid classDiagram @@ -121,49 +127,92 @@ Launcher processes instances in parallel using a thread pool. ## Update instances On update instances request, launcher checks if previous launch is still in progress. If so, it returns -`eWrongState` error. Otherwise, it stops all running instances, updates storage and local cache to reflect -new instances, and starts required instances. After all instances updated, -it sends node instances status using `InstanceStatusSenderItf`. +`eWrongState` error. Otherwise, it stops the requested instances and removes them from storage and local cache. +It then removes unused update items and installs required items, once per item ID and version. +If reading the item statuses fails, launcher logs the error and attempts to install all required items. + +An installation failure marks every dependent instance as `eFailed` with the installation error. These instances +remain in the local cache for status reporting, but are not prepared, persisted, or started. Other items can still +start normally. On a subsequent request, cached `eFailed` instances are prepared again after successful installation. +This retry applies to all failed instances, including failures during preparation or runtime startup. + +After preparing eligible instances, launcher starts their networks and runtimes and sends node instances statuses. ```mermaid sequenceDiagram participant smclient participant launcher + participant imagemanager participant runtimes@{ "type" : "collections" } participant storage smclient ->> launcher: UpdateInstances alt Previous launch is in progress launcher ->> smclient: eWrongState - else Previous launch completed - loop All runtimes - loop Stop instances - launcher ->> runtimes: StopInstance - runtimes -->> launcher: eInactive - end + loop Stop instances + launcher ->> runtimes: StopInstance + launcher ->> launcher: Stop instance network + launcher ->> storage: RemoveInstanceInfo (ignore eNotFound) + launcher ->> launcher: Release network and remove from cache + end - loop Cached instances - launcher ->> storage: RemoveInstanceInfo - launcher ->> launcher: removeFromCache - end + launcher ->> imagemanager: Remove unused update items + launcher ->> imagemanager: GetItemsStatuses + loop Required items not known to be installed + launcher ->> imagemanager: InstallItem + imagemanager -->> launcher: Installation result + end - loop Start instances - launcher ->> launcher: addToCache - launcher ->> runtimes: StartInstance - - runtimes -->> launcher: status - alt eActive or eActivating + loop Start instances + launcher ->> launcher: Find or add to cache + alt Item installation failed + launcher ->> launcher: Set eFailed with installation error + else Item available + opt New or previously failed instance launcher ->> storage: UpdateInstanceInfo + launcher ->> launcher: Prepare configs and network + end + alt Preparation succeeded + launcher ->> launcher: Start instance network + opt Network startup succeeded + launcher ->> runtimes: StartInstance + runtimes -->> launcher: status + end + else Preparation failed + launcher ->> launcher: Set eFailed end end end + launcher ->> smclient: SendNodeInstancesStatuses end - - launcher ->> smclient: SendNodeInstancesStatuses ``` -Launcher processes instances in parallel using a thread pool. +Launcher processes instances in parallel using a thread pool. Installation, preparation, network startup, and +runtime startup are separate stages; each stage completes before the next one starts. + +## Integrity recovery + +Launcher repairs the update items whose content is broken by installing them again with +`ImageManagerItf::InstallItem`, which validates the item and downloads only the broken blobs. An item is repaired at +most once per request: + +* after the instances of a request are prepared, launcher installs again once each item that has an instance whose + item configs failed to read with `eIntegrity`. Then it prepares again the failed instances of the repaired items; +* after the instances of a request are started, launcher installs again once each item that has an instance failed to + start with `eIntegrity`, unless the item was already repaired at the prepare stage. Then it starts again the failed + instances of the repaired items. + +The instances of an item which can't be repaired stay failed with the repair error, and an instance which fails the +second prepare or start stays failed with the error of this attempt. + +The repair needs the CM, which may be not connected on module start, so the stored instances are read and started +without the repair. The first request handles the broken content: it prepares and starts again the instances failed +with `eIntegrity`, and prepares again the running instances whose item configs failed to read on module start. + +Any other error doesn't trigger the repair. The new or failed instances of an item whose installation failed aren't +started, so they aren't repaired either: they stay failed with the installation error until the next request. A +running instance of such an item keeps running. ## aos::sm::launcher::InstanceStatusReceiverItf diff --git a/src/core/sm/launcher/tests/launcher.cpp b/src/core/sm/launcher/tests/launcher.cpp index 4dde8006e..37e955e44 100644 --- a/src/core/sm/launcher/tests/launcher.cpp +++ b/src/core/sm/launcher/tests/launcher.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include #include @@ -13,11 +14,12 @@ #include #include #include +#include +#include #include #include #include #include -#include #include #include #include @@ -140,6 +142,25 @@ monitoring::InstanceMonitoringData CreateMonitoringData(const InstanceInfo& inst return data; } +auto ItemsInstalled(std::vector itemIDs) +{ + return [itemIDs = std::move(itemIDs)](Array& statuses) { + for (const auto& itemID : itemIDs) { + UpdateItemStatus installed; + + installed.mItemID = itemID.c_str(); + installed.mVersion = "1.0.0"; + installed.mState = ItemStateEnum::eInstalled; + + if (auto err = statuses.PushBack(installed); !err.IsNone()) { + return err; + } + } + + return Error(ErrorEnum::eNone); + }; +} + } // namespace /*********************************************************************************************************************** @@ -215,20 +236,21 @@ class LauncherTest : public Test { Launcher mLauncher; - StrictMock mRuntime0; - StrictMock mRuntime1; - NiceMock mImageManager; - StorageStub mStorage; - SenderStub mSender; - StrictMock mOCISpec; - StrictMock mItemInfoProvider; - StrictMock mCloudConnection; - StrictMock mNetworkManager; - instancestatusprovider::ListenerMock mStatusListener; - InstanceStatusArray mReceivedStatuses; - cloudconnection::ConnectionListenerItf* mCloudConnectionListener {}; - StrictMock mInstanceIDProvider; - StrictMock mResourceInfoProvider; + StrictMock mRuntime0; + StrictMock mRuntime1; + NiceMock mImageManager; + NiceMock mItemStatusProvider; + StorageStub mStorage; + SenderStub mSender; + StrictMock mOCISpec; + StrictMock mItemInfoProvider; + StrictMock mCloudConnection; + StrictMock mNetworkManager; + instancestatusprovider::ListenerMock mStatusListener; + InstanceStatusArray mReceivedStatuses; + cloudconnection::ConnectionListenerItf* mCloudConnectionListener {}; + StrictMock mInstanceIDProvider; + StrictMock mResourceInfoProvider; oci::ImageManifest mImageManifest; oci::ImageConfig mImageConfig; @@ -241,8 +263,8 @@ class LauncherTest : public Test { TEST_F(LauncherTest, NoStoredInstancesOnModuleStart) { - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); err = mLauncher.Start(); @@ -268,8 +290,8 @@ TEST_F(LauncherTest, InitInstances) mStorage.Init(cStoredInfos); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); EXPECT_CALL(mRuntime0, InitInstances(Array(&cRuntime0Infos.front(), cRuntime0Infos.size()))) @@ -346,8 +368,8 @@ TEST_F(LauncherTest, SendActiveComponentNodeInstancesStatusOnModuleStart) return ErrorEnum::eNone; })); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); err = mLauncher.Start(); @@ -395,8 +417,8 @@ TEST_F(LauncherTest, DoNotSendUpdateInstancesStatusesBeforeModuleStart) return ErrorEnum::eNone; })); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); err = mLauncher.Start(); @@ -432,8 +454,8 @@ TEST_F(LauncherTest, LauncherStartsStoredInstancesOnModuleStart) mStorage.Init(cStoredInfos); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); EXPECT_CALL(mRuntime0, StartInstance).WillOnce(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { @@ -478,8 +500,8 @@ TEST_F(LauncherTest, StartNetworks_FlushFailure_FailsInstance) mStorage.Init(cStoredInfos); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); EXPECT_CALL(mInstanceIDProvider, GetInstanceID) @@ -545,8 +567,8 @@ TEST_F(LauncherTest, StopInstancesWithExpiredOfflineTTL) mStorage.Init(cStoredInfos); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); EXPECT_CALL(mRuntime0, StartInstance).WillOnce(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { @@ -609,6 +631,161 @@ TEST_F(LauncherTest, StopInstancesWithExpiredOfflineTTL) ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); } +TEST_F(LauncherTest, InstallUpdateItemFailed) +{ + const std::vector cInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + CreateInstanceInfo("item0", 1, "1.0.0", "runtime0"), + CreateInstanceInfo("item0", 2, "2.0.0", "runtime1"), + }; + const Array cInstances(cInstanceInfos.data(), cInstanceInfos.size()); + const Error cInstallError(ErrorEnum::eFailed, "update item install failed"); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mImageManager, InstallItem).Times(2).WillRepeatedly(Invoke([&](const auto& item) { + return item.mVersion == "1.0.0" ? cInstallError : Error(ErrorEnum::eNone); + })); + EXPECT_CALL(mRuntime0, StartInstance).Times(0); + EXPECT_CALL(mRuntime1, StartInstance).WillOnce(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mNetworkManager, CreateInstanceNetwork).Times(1).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mNetworkManager, StartInstanceNetwork).Times(1).WillOnce(Return(ErrorEnum::eNone)); + + err = mLauncher.UpdateInstances({}, cInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + ASSERT_EQ(mReceivedStatuses.Size(), cInstances.Size()); + + for (const auto& status : mReceivedStatuses) { + if (status.mVersion == "1.0.0") { + EXPECT_EQ(status.mState, InstanceStateEnum::eFailed); + EXPECT_EQ(status.mError, cInstallError); + } else { + EXPECT_EQ(status.mState, InstanceStateEnum::eActive); + } + } + + ASSERT_TRUE(Mock::VerifyAndClearExpectations(&mImageManager)); + ASSERT_TRUE(Mock::VerifyAndClearExpectations(&mRuntime0)); + + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mRuntime0, StartInstance) + .Times(2) + .WillRepeatedly(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mNetworkManager, CreateInstanceNetwork).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mNetworkManager, StartInstanceNetwork).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.UpdateInstances({}, Array(cInstanceInfos.data(), 2)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + ASSERT_EQ(mReceivedStatuses.Size(), cInstances.Size()); + for (const auto& status : mReceivedStatuses) { + EXPECT_EQ(status.mState, InstanceStateEnum::eActive); + EXPECT_TRUE(status.mError.IsNone()); + } + + EXPECT_CALL(mRuntime0, StopInstance).Times(2).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mRuntime1, StopInstance).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mRuntime0, Stop).WillOnce(Return(ErrorEnum::eNone)); + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, InstalledItemsReadFailedAndFailedInstanceRemoved) +{ + const std::vector cInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + CreateInstanceInfo("item1", 1, "1.0.0", "runtime1"), + }; + const Array cInstances(cInstanceInfos.data(), cInstanceInfos.size()); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke([](Array& statuses) { + UpdateItemStatus installed; + + installed.mItemID = "item0"; + installed.mVersion = "1.0.0"; + installed.mState = ItemStateEnum::eInstalled; + + (void)statuses.PushBack(installed); + + return ErrorEnum::eFailed; + })); + EXPECT_CALL(mImageManager, InstallItem).Times(2).WillRepeatedly(Invoke([](const auto& item) { + return item.mID == "item0" ? ErrorEnum::eFailed : ErrorEnum::eNone; + })); + EXPECT_CALL(mRuntime0, StartInstance).Times(0); + EXPECT_CALL(mRuntime1, StartInstance).WillOnce(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + })); + + err = mLauncher.UpdateInstances({}, cInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + ASSERT_EQ(mReceivedStatuses.Size(), 2); + for (const auto& status : mReceivedStatuses) { + EXPECT_EQ(status.mState, status.mItemID == "item0" ? InstanceStateEnum::eFailed : InstanceStateEnum::eActive); + } + + auto storedInstances = std::make_unique(); + err = mStorage.GetAllInstancesInfos(*storedInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + ASSERT_EQ(storedInstances->Size(), 1); + EXPECT_EQ((*storedInstances)[0].mItemID, "item1"); + + EXPECT_CALL(mRuntime0, StopInstance).WillOnce(Return(ErrorEnum::eNotFound)); + EXPECT_CALL(mNetworkManager, ReleaseInstanceNetwork).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Return(ErrorEnum::eNone)); + const InstanceIdent cStopInstance = cInstanceInfos[0]; + err = mLauncher.UpdateInstances(Array(&cStopInstance, 1), {}); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + ASSERT_EQ(mReceivedStatuses.Size(), 1); + EXPECT_EQ(mReceivedStatuses[0].mItemID, "item1"); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eActive); + + EXPECT_CALL(mRuntime1, StopInstance).WillOnce(Return(ErrorEnum::eNone)); + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + TEST_F(LauncherTest, UpdateInstances) { const std::vector cStoredInfos = { @@ -623,8 +800,8 @@ TEST_F(LauncherTest, UpdateInstances) mStorage.Init(cStoredInfos); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); EXPECT_CALL(mRuntime0, StartInstance).WillOnce(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { @@ -633,7 +810,7 @@ TEST_F(LauncherTest, UpdateInstances) return ErrorEnum::eNone; })); - EXPECT_CALL(mImageManager, GetAllInstalledItems).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Return(ErrorEnum::eNone)); err = mLauncher.Start(); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); @@ -665,20 +842,19 @@ TEST_F(LauncherTest, UpdateInstances) return ErrorEnum::eNone; })); - EXPECT_CALL(mImageManager, RemoveUpdateItem(cStoredInfos[0].mItemID, cStoredInfos[0].mVersion)) + EXPECT_CALL(mImageManager, RemoveItem(cStoredInfos[0].mItemID, cStoredInfos[0].mVersion)) .WillOnce(Return(ErrorEnum::eNone)); - EXPECT_CALL(mImageManager, InstallUpdateItem) - .WillRepeatedly(Invoke([&](const imagemanager::UpdateItemInfo& itemInfo) { - auto it = std::find_if( - cStartInstanceInfos.begin(), cStartInstanceInfos.end(), [&itemInfo](const InstanceInfo& info) { - return info.mItemID == itemInfo.mID && info.mVersion == itemInfo.mVersion; - }); + EXPECT_CALL(mImageManager, InstallItem).WillRepeatedly(Invoke([&](const imagemanager::UpdateItemInfo& itemInfo) { + auto it = std::find_if( + cStartInstanceInfos.begin(), cStartInstanceInfos.end(), [&itemInfo](const InstanceInfo& info) { + return info.mItemID == itemInfo.mID && info.mVersion == itemInfo.mVersion; + }); - EXPECT_NE(it, cStartInstanceInfos.end()); + EXPECT_NE(it, cStartInstanceInfos.end()); - return ErrorEnum::eNone; - })); + return ErrorEnum::eNone; + })); err = mLauncher.UpdateInstances(cStopInstances, cStartInstances); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); @@ -731,8 +907,8 @@ TEST_F(LauncherTest, UpdateInstancesRestartsInstancesWithModifiedParams) mStorage.Init(cStoredInfos); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); EXPECT_CALL(mRuntime0, StartInstance).WillOnce(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { @@ -741,7 +917,7 @@ TEST_F(LauncherTest, UpdateInstancesRestartsInstancesWithModifiedParams) return ErrorEnum::eNone; })); - EXPECT_CALL(mImageManager, GetAllInstalledItems).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Return(ErrorEnum::eNone)); err = mLauncher.Start(); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); @@ -796,6 +972,669 @@ TEST_F(LauncherTest, UpdateInstancesRestartsInstancesWithModifiedParams) ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); } +TEST_F(LauncherTest, UpdateInstancesInstallsOnlyNotInstalledItems) +{ + const std::vector cStartInstanceInfos = { + CreateInstanceInfo("item1", 1, "1.0.0", "runtime0"), + CreateInstanceInfo("item2", 2, "1.0.0", "runtime1"), + }; + const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 0); + + const auto startInstance = [](const InstanceInfo& instance, InstanceStatus& status) { + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + }; + + EXPECT_CALL(mRuntime0, StartInstance).WillOnce(Invoke(startInstance)); + EXPECT_CALL(mRuntime1, StartInstance).WillOnce(Invoke(startInstance)); + + // The reported versions which are not installed, e.g. removed and kept in the cache, are installed. + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke([](Array& statuses) { + UpdateItemStatus removed; + + removed.mItemID = "item1"; + removed.mVersion = "1.0.0"; + removed.mState = ItemStateEnum::eRemoved; + + UpdateItemStatus installed; + + installed.mItemID = "item2"; + installed.mVersion = "1.0.0"; + installed.mState = ItemStateEnum::eInstalled; + + EXPECT_TRUE(statuses.PushBack(removed).IsNone()); + EXPECT_TRUE(statuses.PushBack(installed).IsNone()); + + return ErrorEnum::eNone; + })); + + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Invoke([](const imagemanager::UpdateItemInfo& itemInfo) { + EXPECT_EQ(itemInfo.mID, "item1"); + + return ErrorEnum::eNone; + })); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mRuntime0, StopInstance(static_cast(cStartInstanceInfos[0]), _)) + .WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mRuntime1, StopInstance(static_cast(cStartInstanceInfos[1]), _)) + .WillOnce(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, StopCancelsItemsBeingInstalled) +{ + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mImageManager, Cancel).WillOnce(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, StopCancelsItemsRequestedAfterCancel) +{ + const std::vector cStartInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + }; + const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::promise launchStarted; + std::promise cancelCalled; + auto cancelCalledFuture = cancelCalled.get_future(); + + // The launch reaches the install after Stop has canceled the image manager. + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke([&](Array&) { + launchStarted.set_value(); + cancelCalledFuture.wait(); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManager, Cancel).WillOnce(Invoke([&cancelCalled]() { + cancelCalled.set_value(); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManager, InstallItem).Times(0); + EXPECT_CALL(mRuntime0, StartInstance).Times(0); + EXPECT_CALL(mRuntime0, StopInstance).WillOnce(Return(ErrorEnum::eNone)); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + launchStarted.get_future().wait(); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 1); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eFailed); + EXPECT_TRUE(mReceivedStatuses[0].mError.Is(ErrorEnum::eCanceled)) + << tests::utils::ErrorToStr(mReceivedStatuses[0].mError); +} + +TEST_F(LauncherTest, RepairsItemsOfInstancesFailedWithIntegrityError) +{ + const std::vector cStartInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + CreateInstanceInfo("item0", 1, "1.0.0", "runtime0"), + CreateInstanceInfo("item1", 2, "1.0.0", "runtime1"), + }; + const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke(ItemsInstalled({"item0", "item1"}))); + + std::atomic_int brokenStarts {2}; + + // The first start of each item0 instance fails, as the item content is broken. + EXPECT_CALL(mRuntime0, StartInstance) + .Times(4) + .WillRepeatedly(Invoke([&brokenStarts](const InstanceInfo& instance, InstanceStatus& status) { + if (brokenStarts-- > 0) { + return Error(ErrorEnum::eIntegrity); + } + + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return Error(ErrorEnum::eNone); + })); + EXPECT_CALL(mRuntime1, StartInstance).WillOnce(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + })); + + // The item is repaired once for all its broken instances. + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Invoke([](const imagemanager::UpdateItemInfo& itemInfo) { + EXPECT_EQ(itemInfo.mID, "item0"); + EXPECT_EQ(itemInfo.mVersion, "1.0.0"); + + return ErrorEnum::eNone; + })); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), cStartInstanceInfos.size()); + + for (const auto& status : mReceivedStatuses) { + EXPECT_EQ(status.mState, InstanceStateEnum::eActive) << status; + } + + EXPECT_CALL(mRuntime0, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + EXPECT_CALL(mRuntime1, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, InstancesOfNotRepairedItemFailWithRepairError) +{ + const std::vector cStartInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + }; + const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke(ItemsInstalled({"item0"}))); + EXPECT_CALL(mRuntime0, StartInstance).WillOnce(Return(ErrorEnum::eIntegrity)); + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Return(ErrorEnum::eRuntime)); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 1); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eFailed); + EXPECT_TRUE(mReceivedStatuses[0].mError.Is(ErrorEnum::eRuntime)) << mReceivedStatuses[0]; + + EXPECT_CALL(mRuntime0, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, RepairsItemOncePerRequest) +{ + const std::vector cStartInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + }; + const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke(ItemsInstalled({"item0"}))); + + // The CM copy is broken as well, so the second start fails again. + EXPECT_CALL(mRuntime0, StartInstance).Times(2).WillRepeatedly(Return(ErrorEnum::eIntegrity)); + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Return(ErrorEnum::eNone)); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 1); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eFailed); + EXPECT_TRUE(mReceivedStatuses[0].mError.Is(ErrorEnum::eIntegrity)) << mReceivedStatuses[0]; + + EXPECT_CALL(mRuntime0, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, DoesNotRepairItemsOfInstancesFailedWithOtherErrors) +{ + const std::vector cStartInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + }; + const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke(ItemsInstalled({"item0"}))); + EXPECT_CALL(mRuntime0, StartInstance).WillOnce(Return(ErrorEnum::eRuntime)); + EXPECT_CALL(mImageManager, InstallItem).Times(0); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 1); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eFailed); + EXPECT_TRUE(mReceivedStatuses[0].mError.Is(ErrorEnum::eRuntime)) << mReceivedStatuses[0]; + + EXPECT_CALL(mRuntime0, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, RepairsItemFailedOnModuleStartOnFirstRequest) +{ + const std::vector cStoredInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + }; + const Array cStartInstances(&cStoredInfos.front(), cStoredInfos.size()); + + mStorage.Init(cStoredInfos); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + // The repair needs the CM, which may be not connected on module start, so the broken item is not repaired. The + // first request starts the failed instance again and repairs the item. + EXPECT_CALL(mRuntime0, StartInstance) + .WillOnce(Return(ErrorEnum::eIntegrity)) + .WillOnce(Return(ErrorEnum::eIntegrity)) + .WillOnce(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mImageManager, InstallItem).Times(0); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 1); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eFailed); + EXPECT_TRUE(mReceivedStatuses[0].mError.Is(ErrorEnum::eIntegrity)) << mReceivedStatuses[0]; + + ASSERT_TRUE(Mock::VerifyAndClearExpectations(&mImageManager)); + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke(ItemsInstalled({"item0"}))); + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Return(ErrorEnum::eNone)); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 1); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eActive) << mReceivedStatuses[0]; + + EXPECT_CALL(mRuntime0, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, RepairsItemWithBrokenConfigs) +{ + const std::vector cStartInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + }; + const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke([](Array& statuses) { + UpdateItemStatus installed; + + installed.mItemID = "item0"; + installed.mVersion = "1.0.0"; + installed.mState = ItemStateEnum::eInstalled; + + EXPECT_TRUE(statuses.PushBack(installed).IsNone()); + + return ErrorEnum::eNone; + })); + + // The item config read fails, as the item content is broken, and succeeds after the item is repaired. + EXPECT_CALL(mOCISpec, LoadItemConfig) + .WillOnce(Return(ErrorEnum::eIntegrity)) + .WillOnce(DoAll(SetArgReferee<1>(mItemConfig), Return(ErrorEnum::eNone))); + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Invoke([](const imagemanager::UpdateItemInfo& itemInfo) { + EXPECT_EQ(itemInfo.mID, "item0"); + EXPECT_EQ(itemInfo.mVersion, "1.0.0"); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mRuntime0, StartInstance).WillOnce(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + })); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 1); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eActive) << mReceivedStatuses[0]; + + EXPECT_CALL(mRuntime0, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, RepairsItemWithBrokenConfigsOncePerRequest) +{ + const std::vector cStartInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + CreateInstanceInfo("item0", 1, "1.0.0", "runtime0"), + CreateInstanceInfo("item0", 2, "1.0.0", "runtime0"), + }; + const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke([](Array& statuses) { + UpdateItemStatus installed; + + installed.mItemID = "item0"; + installed.mVersion = "1.0.0"; + installed.mState = ItemStateEnum::eInstalled; + + EXPECT_TRUE(statuses.PushBack(installed).IsNone()); + + return ErrorEnum::eNone; + })); + + std::atomic_int brokenReads {static_cast(cStartInstanceInfos.size())}; + + // The item config read of each instance fails at the prepare stage, as the item content is broken. + EXPECT_CALL(mOCISpec, LoadItemConfig) + .Times(cStartInstanceInfos.size() * 2) + .WillRepeatedly(Invoke([this, &brokenReads](const String&, oci::ItemConfig& itemConfig) { + if (brokenReads-- > 0) { + return Error(ErrorEnum::eIntegrity); + } + + itemConfig = mItemConfig; + + return Error(ErrorEnum::eNone); + })); + + // The item is repaired once for all its instances, the start stage doesn't repair it again. + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Invoke([](const imagemanager::UpdateItemInfo& itemInfo) { + EXPECT_EQ(itemInfo.mID, "item0"); + EXPECT_EQ(itemInfo.mVersion, "1.0.0"); + + return ErrorEnum::eNone; + })); + + std::atomic_int brokenStarts {1}; + + EXPECT_CALL(mRuntime0, StartInstance) + .Times(cStartInstanceInfos.size()) + .WillRepeatedly(Invoke([&brokenStarts](const InstanceInfo& instance, InstanceStatus& status) { + if (brokenStarts-- > 0) { + return Error(ErrorEnum::eIntegrity); + } + + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return Error(ErrorEnum::eNone); + })); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), cStartInstanceInfos.size()); + + size_t numFailed = 0; + + for (const auto& status : mReceivedStatuses) { + if (status.mState == InstanceStateEnum::eFailed) { + EXPECT_TRUE(status.mError.Is(ErrorEnum::eIntegrity)) << status; + numFailed++; + + continue; + } + + EXPECT_EQ(status.mState, InstanceStateEnum::eActive) << status; + } + + EXPECT_EQ(numFailed, 1); + + EXPECT_CALL(mRuntime0, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, RepairsBrokenConfigsOfModuleStartOnFirstRequest) +{ + const std::vector cStoredInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + }; + const Array cStartInstances(&cStoredInfos.front(), cStoredInfos.size()); + + mStorage.Init(cStoredInfos); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + // The repair needs the CM, which may be not connected on module start, so the broken configs are not repaired. + // The first request prepares the running instance again, as its configs aren't loaded, and repairs the item. The + // configs are loaded then, so the next request doesn't prepare the running instance again. + EXPECT_CALL(mOCISpec, LoadItemConfig) + .Times(3) + .WillOnce(Return(ErrorEnum::eIntegrity)) + .WillOnce(Return(ErrorEnum::eIntegrity)) + .WillOnce(DoAll(SetArgReferee<1>(mItemConfig), Return(ErrorEnum::eNone))); + EXPECT_CALL(mImageManager, InstallItem).Times(0); + EXPECT_CALL(mRuntime0, StartInstance) + .Times(3) + .WillRepeatedly(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + })); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), cStoredInfos.size()); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eActive) << mReceivedStatuses[0]; + + ASSERT_TRUE(Mock::VerifyAndClearExpectations(&mImageManager)); + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke(ItemsInstalled({"item0"}))); + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Return(ErrorEnum::eNone)); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 1); + EXPECT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eActive) << mReceivedStatuses[0]; + + EXPECT_CALL(mItemStatusProvider, GetItemsStatuses).WillOnce(Invoke(ItemsInstalled({"item0"}))); + + err = mLauncher.UpdateInstances({}, cStartInstances); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mRuntime0, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(LauncherTest, InstallFailureKeepsRunningInstance) +{ + const std::vector cInstanceInfos = { + CreateInstanceInfo("item0", 0, "1.0.0", "runtime0"), + CreateInstanceInfo("item0", 1, "1.0.0", "runtime0"), + }; + const Error cInstallError(ErrorEnum::eIntegrity, "update item install failed"); + + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mLauncher.GetInstancesStatuses(mReceivedStatuses); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(mRuntime0, StartInstance) + .WillRepeatedly(Invoke([](const InstanceInfo& instance, InstanceStatus& status) { + SetInstanceStatus(instance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + })); + + err = mLauncher.UpdateInstances({}, Array(cInstanceInfos.data(), 1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), 1); + ASSERT_EQ(mReceivedStatuses[0].mState, InstanceStateEnum::eActive) << mReceivedStatuses[0]; + + ASSERT_TRUE(Mock::VerifyAndClearExpectations(&mImageManager)); + + // The item is installed again for the new instance and fails: the running instance keeps running. + EXPECT_CALL(mImageManager, InstallItem).WillOnce(Return(cInstallError)); + + err = mLauncher.UpdateInstances({}, Array(cInstanceInfos.data(), cInstanceInfos.size())); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mSender.WaitStatuses(mReceivedStatuses, cWaitTimeout); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(mReceivedStatuses.Size(), cInstanceInfos.size()); + + for (const auto& status : mReceivedStatuses) { + if (status.mInstance == cInstanceInfos[0].mInstance) { + EXPECT_EQ(status.mState, InstanceStateEnum::eActive) << status; + EXPECT_TRUE(status.mError.IsNone()) << status; + } else { + EXPECT_EQ(status.mState, InstanceStateEnum::eFailed) << status; + EXPECT_EQ(status.mError, cInstallError) << status; + } + } + + EXPECT_CALL(mRuntime0, StopInstance).WillRepeatedly(Return(ErrorEnum::eNone)); + + err = mLauncher.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + TEST_F(LauncherTest, ParallelUpdateInstancesDoesNotInterfere) { const std::vector cStartInstanceInfos = { @@ -805,8 +1644,8 @@ TEST_F(LauncherTest, ParallelUpdateInstancesDoesNotInterfere) const Array cStartFirstInstance(&cStartInstanceInfos.front(), 1); const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); err = mLauncher.Start(); @@ -851,8 +1690,8 @@ TEST_F(LauncherTest, ParallelUpdateInstancesDoesNotInterfere) TEST_F(LauncherTest, GetInstancesStatusesReturnsEmptyArray) { - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); auto storedData = std::make_unique(); @@ -878,8 +1717,8 @@ TEST_F(LauncherTest, GetInstancesStatuses) }; const Array cStartInstances(&cStartInstanceInfos.front(), cStartInstanceInfos.size()); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); EXPECT_CALL(mRuntime0, StartInstance(cStartInstanceInfos[0], _)) @@ -942,8 +1781,8 @@ TEST_F(LauncherTest, GetInstanceMonitoringParams) const Array cStartInstances(&startInstance, 1); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); EXPECT_CALL(mRuntime0, StartInstance(startInstance, _)) @@ -1000,8 +1839,8 @@ TEST_F(LauncherTest, GetInstanceMonitoringData) const Array cStartInstances(&cInstanceInfo, 1); const auto cMonitoringData = CreateMonitoringData(cInstanceInfo); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); err = mLauncher.Start(); @@ -1047,8 +1886,8 @@ TEST_F(LauncherTest, GetInstanceMonitoringData) TEST_F(LauncherTest, GetRuntimesInfos) { - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); err = mLauncher.Start(); @@ -1080,8 +1919,8 @@ TEST_F(LauncherTest, OnInstanceStatusChanged) mStorage.Init({cInstanceInfo}); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); err = mLauncher.SubscribeListener(mStatusListener); @@ -1126,8 +1965,8 @@ TEST_F(LauncherTest, RebootRuntimeOnStartInstance) mStorage.Init({cInstanceInfo}); - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); EXPECT_CALL(mRuntime0, StartInstance).WillOnce(Invoke([&](const InstanceInfo& instance, InstanceStatus& status) { @@ -1179,8 +2018,8 @@ TEST_F(LauncherTest, RebootRuntimeOnStartInstance) TEST_F(LauncherTest, RebootRuntime) { - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); err = mLauncher.SubscribeListener(mStatusListener); @@ -1217,8 +2056,8 @@ TEST_F(LauncherTest, RebootRuntime) TEST_F(LauncherTest, OnInstancesStatusesReceived) { - auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mSender, mStorage, mOCISpec, - mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); + auto err = mLauncher.Init(mAllocator, GetRuntimesArray(), mImageManager, mItemStatusProvider, mSender, mStorage, + mOCISpec, mItemInfoProvider, mCloudConnection, mNetworkManager, mInstanceIDProvider, mResourceInfoProvider); ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); err = mLauncher.SubscribeListener(mStatusListener); diff --git a/src/core/sm/launcher/tests/mocks/imagemanagermock.hpp b/src/core/sm/launcher/tests/mocks/imagemanagermock.hpp index 1b10fbda3..ced382f4a 100644 --- a/src/core/sm/launcher/tests/mocks/imagemanagermock.hpp +++ b/src/core/sm/launcher/tests/mocks/imagemanagermock.hpp @@ -16,9 +16,9 @@ namespace aos::sm::imagemanager { */ class ImageManagerMock : public ImageManagerItf { public: - MOCK_METHOD(Error, GetAllInstalledItems, (Array&), (const, override)); - MOCK_METHOD(Error, InstallUpdateItem, (const UpdateItemInfo&), (override)); - MOCK_METHOD(Error, RemoveUpdateItem, (const String&, const String&), (override)); + MOCK_METHOD(Error, InstallItem, (const UpdateItemInfo&), (override)); + MOCK_METHOD(Error, RemoveItem, (const String&, const String&), (override)); + MOCK_METHOD(Error, Cancel, (), (override)); }; } // namespace aos::sm::imagemanager diff --git a/src/core/sm/smclient/itf/smclient.hpp b/src/core/sm/smclient/itf/smclient.hpp index 2fc7fa938..a6908814f 100644 --- a/src/core/sm/smclient/itf/smclient.hpp +++ b/src/core/sm/smclient/itf/smclient.hpp @@ -8,10 +8,10 @@ #include #include +#include #include #include #include -#include #include #include @@ -28,7 +28,7 @@ class SMClientItf : public aos::alerts::SenderItf, public aos::monitoring::SenderItf, public aos::logging::SenderItf, public launcher::SenderItf, - public imagemanager::BlobInfoProviderItf, + public common::imagemanager::BlobInfoProviderItf, public cloudconnection::CloudConnectionItf, public aos::networkmanager::NetworkProviderItf, public ConnectionItf { diff --git a/src/core/sm/smclient/smclient.md b/src/core/sm/smclient/smclient.md index f204a8ebb..24501439c 100644 --- a/src/core/sm/smclient/smclient.md +++ b/src/core/sm/smclient/smclient.md @@ -8,7 +8,8 @@ It implements the following interfaces: * [aos::monitoring::SenderItf](../../common/monitoring/itf/sender.hpp) - sends monitoring to CM; * [aos::logging::SenderItf](../../common/logging/itf/sender.hpp) - sends system and instances logs to CM; * [aos::sm::launcher::SenderItf](../launcher/itf/sender.hpp) - sends update items statuses to CM; -* [aos::sm::imagemanager::BlobInfoProviderItf](../imagemanager/itf/blobinfoprovider.hpp) - provides blobs info; +* [aos::common::imagemanager::BlobInfoProviderItf](../../common/imagemanager/itf/blobinfoprovider.hpp) - provides + blobs info; * [aos::cloudconnection::CloudConnectionItf](../../common/cloudconnection/itf/cloudconnection.hpp) - provides cloud connection status. @@ -46,7 +47,7 @@ classDiagram <> } - class BlobInfoProviderI ["aos::sm::imagemanager::BlobInfoProviderItf"] { + class BlobInfoProviderI ["aos::common::imagemanager::BlobInfoProviderItf"] { <> } diff --git a/src/core/sm/tests/mocks/CMakeLists.txt b/src/core/sm/tests/mocks/CMakeLists.txt index b12819df0..7f7d37609 100644 --- a/src/core/sm/tests/mocks/CMakeLists.txt +++ b/src/core/sm/tests/mocks/CMakeLists.txt @@ -17,7 +17,6 @@ set(TARGET_NAME mocks) set(HEADERS instanceidprovidermock.hpp instancestatusreceivermock.hpp - iteminfoprovidermock.hpp launchermock.hpp networkmanagermock.hpp rebootermock.hpp diff --git a/src/core/sm/tests/mocks/iteminfoprovidermock.hpp b/src/core/sm/tests/mocks/iteminfoprovidermock.hpp deleted file mode 100644 index 6dace2568..000000000 --- a/src/core/sm/tests/mocks/iteminfoprovidermock.hpp +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_SM_TESTS_MOCKS_ITEMINFOPROVIDERMOCK_HPP_ -#define AOS_CORE_SM_TESTS_MOCKS_ITEMINFOPROVIDERMOCK_HPP_ - -#include - -#include - -namespace aos::sm::imagemanager { - -/** - * Item info provider mock. - */ -class ItemInfoProviderMock : public ItemInfoProviderItf { -public: - MOCK_METHOD(Error, GetBlobPath, (const String&, String&), (const, override)); - MOCK_METHOD(Error, GetLayerPath, (const String&, String&), (const, override)); -}; - -} // namespace aos::sm::imagemanager - -#endif diff --git a/src/core/sm/tests/mocks/tests/mocks.cpp b/src/core/sm/tests/mocks/tests/mocks.cpp index ec43622a0..8f9b73887 100644 --- a/src/core/sm/tests/mocks/tests/mocks.cpp +++ b/src/core/sm/tests/mocks/tests/mocks.cpp @@ -5,7 +5,6 @@ */ #include -#include #include #include #include