From b9ed6876ca1e4d3f5712175278b4f35abb24edc9 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Tue, 3 Nov 2026 16:41:05 +0200 Subject: [PATCH 01/10] sm: remove obsolete image manager interfaces and mocks The SM uses the blob info and item info providers of the common image manager instead. Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- .../sm/imagemanager/itf/blobinfoprovider.hpp | 44 ---------------- .../sm/imagemanager/itf/iteminfoprovider.hpp | 51 ------------------- .../tests/mocks/blobinfoprovidermock.hpp | 27 ---------- .../tests/mocks/iteminfoprovidermock.hpp | 27 ---------- src/core/sm/tests/mocks/CMakeLists.txt | 1 - .../sm/tests/mocks/iteminfoprovidermock.hpp | 27 ---------- src/core/sm/tests/mocks/tests/mocks.cpp | 1 - 7 files changed, 178 deletions(-) delete mode 100644 src/core/sm/imagemanager/itf/blobinfoprovider.hpp delete mode 100644 src/core/sm/imagemanager/itf/iteminfoprovider.hpp delete mode 100644 src/core/sm/imagemanager/tests/mocks/blobinfoprovidermock.hpp delete mode 100644 src/core/sm/imagemanager/tests/mocks/iteminfoprovidermock.hpp delete mode 100644 src/core/sm/tests/mocks/iteminfoprovidermock.hpp 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/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/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/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 From 895a85ba03037f1f09135867e62aee8fcae3df88 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Tue, 3 Nov 2026 16:41:05 +0200 Subject: [PATCH 02/10] sm: imagemanager: remove old implementation Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- src/core/sm/CMakeLists.txt | 3 +- src/core/sm/imagemanager/CMakeLists.txt | 55 - src/core/sm/imagemanager/config.hpp | 28 - src/core/sm/imagemanager/imagemanager.cpp | 1545 ----------------- src/core/sm/imagemanager/imagemanager.hpp | 195 --- src/core/sm/imagemanager/imagemanager.md | 43 - src/core/sm/imagemanager/itf/imagemanager.hpp | 118 -- src/core/sm/imagemanager/itf/storage.hpp | 118 -- src/core/sm/imagemanager/tests/CMakeLists.txt | 33 - .../sm/imagemanager/tests/imagemanager.cpp | 1059 ----------- .../imagemanager/tests/stubs/storagestub.hpp | 156 -- 11 files changed, 1 insertion(+), 3352 deletions(-) delete mode 100644 src/core/sm/imagemanager/CMakeLists.txt delete mode 100644 src/core/sm/imagemanager/config.hpp delete mode 100644 src/core/sm/imagemanager/imagemanager.cpp delete mode 100644 src/core/sm/imagemanager/imagemanager.hpp delete mode 100644 src/core/sm/imagemanager/imagemanager.md delete mode 100644 src/core/sm/imagemanager/itf/imagemanager.hpp delete mode 100644 src/core/sm/imagemanager/itf/storage.hpp delete mode 100644 src/core/sm/imagemanager/tests/CMakeLists.txt delete mode 100644 src/core/sm/imagemanager/tests/imagemanager.cpp delete mode 100644 src/core/sm/imagemanager/tests/stubs/storagestub.hpp diff --git a/src/core/sm/CMakeLists.txt b/src/core/sm/CMakeLists.txt index 701beb62b..4dfb7fc93 100644 --- a/src/core/sm/CMakeLists.txt +++ b/src/core/sm/CMakeLists.txt @@ -19,7 +19,7 @@ set(HEADERS config.hpp) set(INSTALL_HEADERS aos_core_sm_database aos_core_sm_logging aos_core_sm_nodeconfig aos_core_sm_resourcemanager) -set(INSTALL_LIBRARIES aos_core_sm_imagemanager aos_core_sm_launcher) +set(INSTALL_LIBRARIES aos_core_sm_launcher) if(WITH_TEST) list(APPEND INSTALL_HEADERS aos_core_sm_tests_mocks aos_core_sm_tests_stubs) @@ -43,7 +43,6 @@ set(TARGET_PREFIX ${TARGET_PREFIX}_sm) # ###################################################################################################################### add_subdirectory(database) -add_subdirectory(imagemanager) add_subdirectory(launcher) add_subdirectory(logging) add_subdirectory(networkmanager) diff --git a/src/core/sm/imagemanager/CMakeLists.txt b/src/core/sm/imagemanager/CMakeLists.txt deleted file mode 100644 index f9e22b824..000000000 --- a/src/core/sm/imagemanager/CMakeLists.txt +++ /dev/null @@ -1,55 +0,0 @@ -# -# Copyright (C) 2025 EPAM Systems, Inc. -# -# SPDX-License-Identifier: Apache-2.0 -# - -# ###################################################################################################################### -# Target name -# ###################################################################################################################### - -set(TARGET_NAME imagemanager) - -# ###################################################################################################################### -# Sources -# ###################################################################################################################### - -set(SOURCES imagemanager.cpp) - -# ###################################################################################################################### -# Headers -# ###################################################################################################################### - -set(HEADERS itf/blobinfoprovider.hpp itf/imagemanager.hpp itf/iteminfoprovider.hpp config.hpp imagemanager.hpp) - -# ###################################################################################################################### -# Libraries -# ###################################################################################################################### - -set(LIBRARIES aos::core::common::tools) - -# ###################################################################################################################### -# Target -# ###################################################################################################################### - -add_module( - TARGET_NAME - ${TARGET_NAME} - LOG_MODULE - STACK_USAGE - ${AOS_STACK_USAGE} - SOURCES - ${SOURCES} - HEADERS - ${HEADERS} - LIBRARIES - ${LIBRARIES} -) - -# ###################################################################################################################### -# Tests -# ###################################################################################################################### - -if(WITH_TEST) - add_subdirectory(tests) -endif() diff --git a/src/core/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 deleted file mode 100644 index bcbec05fc..000000000 --- a/src/core/sm/imagemanager/imagemanager.cpp +++ /dev/null @@ -1,1545 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include - -#include "imagemanager.hpp" - -namespace aos::sm::imagemanager { - -/*********************************************************************************************************************** - * Static - **********************************************************************************************************************/ - -namespace { - -Error SplitDigest(const String& digest, String& alg, String& hash) -{ - StaticArray, 2> digestList; - - if (auto err = digest.Split(digestList, ':'); !err.IsNone()) { - return err; - } - - if (digestList.Size() != 2) { - return ErrorEnum::eInvalidArgument; - } - - if (auto err = alg.Assign(digestList[0]); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = hash.Assign(digestList[1]); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error AddPathIfNotExist(Array>& list, const String& path) -{ - if (list.Contains(path)) { - return ErrorEnum::eNone; - } - - return list.PushBack(path); -} - -} // namespace - -/*********************************************************************************************************************** - * Public - **********************************************************************************************************************/ - -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) -{ - 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); - } - - if (auto err = fs::MakeDirAll(fs::JoinPath(mConfig.mImagePath, cLayersFolder)); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::Start() -{ - LockGuard lock {mMutex}; - - LOG_DBG() << "Start image manager"; - - if (auto err = UpdateOutdatedItems(); !err.IsNone()) { - LOG_ERR() << "Can't update outdated items" << Log::Field(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 = mTimer.Start( - mConfig.mRemoveOutdatedPeriod, - [this](void*) { - LockGuard lock {mMutex}; - - mProcessOutdatedItems = true; - - if (auto err = mCV.NotifyAll(); !err.IsNone()) { - LOG_ERR() << "Can't notify image manager" << Log::Field(AOS_ERROR_WRAP(err)); - } - }, - false); - !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = mThread.Run([this](void*) { ProcessOutdatedItems(); }); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::Stop() -{ - Error stopErr; - - { - LockGuard lock {mMutex}; - - LOG_DBG() << "Stop image manager"; - - if (auto err = mTimer.Stop(Timer::StopMode::WaitForCallbacks); !err.IsNone() && stopErr.IsNone()) { - stopErr = AOS_ERROR_WRAP(err); - } - - mClose = true; - - if (auto err = mCV.NotifyAll(); !err.IsNone()) { - LOG_ERR() << "Can't notify image manager" << Log::Field(AOS_ERROR_WRAP(err)); - } - } - - if (auto err = mThread.Join(); !err.IsNone() && stopErr.IsNone()) { - stopErr = AOS_ERROR_WRAP(err); - } - - return stopErr; -} - -Error ImageManager::GetAllInstalledItems(Array& statuses) const -{ - LockGuard lock {mMutex}; - - LOG_DBG() << "Get all installed 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::eInstalled) { - continue; - } - - if (auto err - = statuses.PushBack(UpdateItemStatus {itemData.mID, itemData.mType, itemData.mVersion, itemData.mState}); - !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - return ErrorEnum::eNone; -} - -Error ImageManager::InstallUpdateItem(const UpdateItemInfo& itemInfo) -{ - 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); - - auto [installItemIt, err] = CreateInstallingItem(itemInfo); - if (!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); - - err = InstallBlob(manifestDescriptor, &installItem); - if (!err.IsNone()) { - return err; - } - - auto manifest = MakeUnique(mAllocator); - if (!manifest) { - return AOS_ERROR_WRAP(ErrorEnum::eNoMemory); - } - - StaticString path; - - err = CreateBlobPath(itemInfo.mManifestDigest, path); - if (!err.IsNone()) { - return err; - } - - err = mOCISpec->LoadImageManifest(path, *manifest); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (manifest->mItemConfig.HasValue()) { - LOG_DBG() << "Install item config blob" << Log::Field("digest", manifest->mItemConfig->mDigest); - - err = InstallBlob(*manifest->mItemConfig, &installItem); - if (!err.IsNone()) { - return err; - } - } - - if (itemInfo.mType == UpdateItemTypeEnum::eService) { - err = InstallServiceLayers(*manifest, installItem); - if (!err.IsNone()) { - return err; - } - } else { - err = InstallComponentLayers(*manifest, installItem); - if (!err.IsNone()) { - return err; - } - } - - err = StoreUpdateItem(itemInfo); - if (!err.IsNone()) { - return err; - } - - return ErrorEnum::eNone; -} - -Error ImageManager::RemoveUpdateItem(const String& itemID, const String& version) -{ - 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); - } - - auto it = itemData.FindIf([&](const UpdateItemData& data) { return data.mVersion == version; }); - if (it == itemData.end()) { - return AOS_ERROR_WRAP(ErrorEnum::eNotFound); - } - - it->mState = ItemStateEnum::eRemoved; - it->mTimestamp = Time::Now(); - - 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 = mStorage->UpdateUpdateItem(*it); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::GetBlobPath(const String& digest, String& path) const -{ - if (auto err = CreateBlobPath(digest, path); !err.IsNone()) { - return 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 (!exists) { - return AOS_ERROR_WRAP(ErrorEnum::eNotFound); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::GetLayerPath(const String& digest, String& path) const -{ - if (auto err = CreateLayerPath(digest, path); !err.IsNone()) { - return err; - } - - if (auto err = path.Assign(fs::JoinPath(path, cUnpackedLayerFolder)); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - LOG_DBG() << "Get layer path" << Log::Field("digest", digest) << Log::Field("path", path); - - auto [exists, err] = fs::DirExist(path); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (!exists) { - return AOS_ERROR_WRAP(ErrorEnum::eNotFound); - } - - return ErrorEnum::eNone; -} - -/*********************************************************************************************************************** - * Private - **********************************************************************************************************************/ - -RetWithError ImageManager::RemoveItem(const String& id, const String& version) -{ - LockGuard lock {mMutex}; - - LOG_DBG() << "Remove item" << Log::Field("id", id) << Log::Field("version", version); - - 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); - } - - auto [size, err] = RemoveOrphans(); - if (!err.IsNone()) { - return RetWithError(0, AOS_ERROR_WRAP(err)); - } - - return size; -} - -Error ImageManager::CreateBlobPath(const String& digest, String& path) const -{ - StaticString alg; - StaticString hash; - - if (auto err = SplitDigest(digest, alg, hash); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = path.Assign(fs::JoinPath(mConfig.mImagePath, cBlobsFolder, alg, hash)); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::CreateLayerPath(const String& digest, String& path) const -{ - StaticString alg; - StaticString hash; - - if (auto err = SplitDigest(digest, alg, hash); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (auto err = path.Assign(fs::JoinPath(mConfig.mImagePath, cLayersFolder, alg, hash)); !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); - - StaticString alg; - StaticString hash; - - if (auto err = SplitDigest(digest, alg, hash); !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); - } - - fs::FileInfo fileInfo; - - if (auto err = mFileInfoProvider->GetFileInfo(path, fileInfo); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticArray hashBytes; - - if (auto err = hash.HexToByteArray(hashBytes); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (fileInfo.mCheckSum != hashBytes) { - return AOS_ERROR_WRAP(ErrorEnum::eInvalidChecksum); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::ValidateLayer(const String& path, const String& diffDigest) const -{ - LOG_DBG() << "Validate layer" << Log::Field("path", path) << Log::Field("diffDigest", diffDigest); - - auto [size, err] = fs::CalculateSize(*mAllocator, path); - - if (size > oci::cDigestLen) { - return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "not link to unpacked layer")); - } - - StaticString digest; - - err = fs::ReadFileToString(path, digest); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (digest != diffDigest) { - return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidChecksum, "wrong diff digest")); - } - - StaticString dstPath; - - err = CreateLayerPath(diffDigest, dstPath); - if (!err.IsNone()) { - return err; - } - - err = fs::ReadFileToString(fs::JoinPath(dstPath, cDigestFile), digest); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString layerDigest; - - Tie(layerDigest, err) = mImageHandler->GetUnpackedLayerDigest(fs::JoinPath(dstPath, cUnpackedLayerFolder)); - 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")); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::DownloadBlob(const String& path, const String& digest, size_t size) -{ - Error err; - UniquePtr space; - StaticString url; - - 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); - } - - ReleaseSpace(path, space.Get(), *releaseErr); - }); - - err = GetBlobURL(digest, url); - if (!err.IsNone()) { - return err; - } - - if (size) { - Tie(space, err) = mSpaceAllocator->AllocateSpace(size); - if (!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()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::InstallBlob(const oci::ContentDescriptor& descriptor, InstallItem* installItem, bool waitInstalling) -{ - if (waitInstalling) { - if (auto err = WaitForInstallingBlob(descriptor.mDigest); !err.IsNone()) { - return 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 (installItem) { - LockGuard lock {mMutex}; - - if (auto err = installItem->mBlobs.EmplaceBack(descriptor.mDigest); !err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - } - - LOG_DBG() << "Install blob" << Log::Field("digest", descriptor.mDigest) << Log::Field("size", descriptor.mSize); - - StaticString path; - - if (auto err = CreateBlobPath(descriptor.mDigest, path); !err.IsNone()) { - return err; - } - - LOG_DBG() << "Blob path" << Log::Field("digest", descriptor.mDigest) << Log::Field("path", path); - - auto [exists, err] = fs::FileExist(path); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - if (exists) { - err = ValidateBlob(path, descriptor.mDigest); - if (err.IsNone()) { - return ErrorEnum::eNone; - } - - err = fs::RemoveAll(path); - if (!err.IsNone()) { - LOG_ERR() << "Failed to remove blob" << Log::Field("digest", descriptor.mDigest) << Log::Field(err); - } - } - - StaticString parentPath; - - err = fs::ParentPath(path, parentPath); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = fs::MakeDirAll(parentPath); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = DownloadBlob(path, descriptor.mDigest, descriptor.mSize); - if (!err.IsNone()) { - return err; - } - - err = ValidateBlob(path, descriptor.mDigest); - if (!err.IsNone()) { - return err; - } - - return ErrorEnum::eNone; -} - -Error ImageManager::CreateLayerMetadata(const String& path, size_t size, spaceallocator::SpaceItf* space) -{ - auto [digest, err] = mImageHandler->GetUnpackedLayerDigest(fs::JoinPath(path, cUnpackedLayerFolder)); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - LOG_DBG() << "Create layer metadata" << Log::Field("path", path) << Log::Field("digest", digest) - << Log::Field("size", size); - - err = space->Resize(space->Size() + digest.Size()); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = fs::WriteStringToFile(fs::JoinPath(path, cDigestFile), digest, 0600); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - StaticString<64> sizeStr; - - err = sizeStr.Convert(size); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = space->Resize(space->Size() + sizeStr.Size()); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - err = fs::WriteStringToFile(fs::JoinPath(path, cSizeFile), sizeStr, 0600); - if (!err.IsNone()) { - return AOS_ERROR_WRAP(err); - } - - return ErrorEnum::eNone; -} - -Error ImageManager::UnpackLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffDigest) -{ - LOG_DBG() << "Unpack layer" << Log::Field("diffDigest", diffDigest) << Log::Field("path", path); - - StaticString dstPath; - - if (auto err = CreateLayerPath(diffDigest, dstPath); !err.IsNone()) { - return err; - } - - LOG_DBG() << "Layer destination" << Log::Field("diffDigest", diffDigest) << Log::Field("path", dstPath); - - if (auto err = fs::ClearDir(dstPath); !err.IsNone()) { - LOG_ERR() << "Failed to remove layer" << Log::Field("diffDigest", diffDigest) << Log::Field(err); - } - - 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); - } - - UniquePtr space; - - 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); - } - - ReleaseSpace(dstPath, space.Get(), *releaseErr); - }); - - 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++; - } - } - - return {removed, err}; -} - -Error ImageManager::UpdateOutdatedItems() -{ - 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; - } - } - } - - 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); - } - - mSpaceAllocator->FreeSpace(size); - } -} - -} // namespace aos::sm::imagemanager diff --git a/src/core/sm/imagemanager/imagemanager.hpp b/src/core/sm/imagemanager/imagemanager.hpp deleted file mode 100644 index fc1e9f330..000000000 --- a/src/core/sm/imagemanager/imagemanager.hpp +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_SM_IMAGEMANAGER_IMAGEMANAGER_HPP_ -#define AOS_CORE_SM_IMAGEMANAGER_IMAGEMANAGER_HPP_ - -#include -#include -#include -#include -#include -#include -#include - -#include "itf/blobinfoprovider.hpp" -#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 - * @{ - */ - -/** - * Image manager. - */ -class ImageManager : public ImageManagerItf, public ItemInfoProviderItf, public spaceallocator::ItemRemoverItf { -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 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. - */ - Error Start(); - - /** - * 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. - * - * @param itemInfo update item info. - * @return Error. - */ - Error InstallUpdateItem(const UpdateItemInfo& itemInfo) override; - - /** - * Removes update item. - * - * @param itemID update item ID. - * @param version update item version. - * @return Error. - */ - Error RemoveUpdateItem(const String& itemID, const String& version) override; - - /** - * Returns blob path by its digest. - * - * @param digest blob digest. - * @param[out] path result blob path. - * @return Error. - */ - Error GetBlobPath(const String& digest, String& path) const override; - - /** - * Returns layer path by its digest. - * - * @param digest layer digest. - * @param[out] path result layer path. - * @return Error. - */ - Error GetLayerPath(const String& digest, 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 {}; -}; - -/** @}*/ - -} // namespace aos::sm::imagemanager - -#endif diff --git a/src/core/sm/imagemanager/imagemanager.md b/src/core/sm/imagemanager/imagemanager.md deleted file mode 100644 index cc7b0145e..000000000 --- a/src/core/sm/imagemanager/imagemanager.md +++ /dev/null @@ -1,43 +0,0 @@ -# Image manager - -Image manager installs, removes and manages update items on SM side. - -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. - -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). - -```mermaid -classDiagram - direction LR - - class ImageManager ["aos::sm::imagemanager::ImageManager"] { - } - - class ImageManagerItf ["aos::sm::imagemanager::ImageManagerItf"] { - <> - } - - class ItemInfoProviderItf ["aos::sm::imagemanager::ItemInfoProviderItf"] { - <> - } - - class ImageHandlerItf ["aos::sm::imagemanager::ImageHandlerItf"] { - <> - } - - class BlobInfoProviderItf ["aos::sm::imagemanager::BlobInfoProviderItf"] { - <> - } - - ImageManager ..|> ImageManagerItf - ImageManager ..|> ItemInfoProviderItf - - ImageManager ..> ImageHandlerItf - ImageManager ..> BlobInfoProviderItf -``` diff --git a/src/core/sm/imagemanager/itf/imagemanager.hpp b/src/core/sm/imagemanager/itf/imagemanager.hpp deleted file mode 100644 index 3669828ff..000000000 --- a/src/core/sm/imagemanager/itf/imagemanager.hpp +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_SM_IMAGEMANAGER_ITF_IMAGEMANAGER_HPP_ -#define AOS_CORE_SM_IMAGEMANAGER_ITF_IMAGEMANAGER_HPP_ - -#include -#include - -namespace aos::sm::imagemanager { - -/** @addtogroup sm Service Manager - * @{ - */ - -/** - * Update item info. - */ -struct UpdateItemInfo { - StaticString mID; - UpdateItemType mType; - StaticString mVersion; - StaticString mManifestDigest; - - /** - * Compares update item info. - * - * @param rhs info to compare. - * @return bool. - */ - friend bool operator==(const UpdateItemInfo& lhs, const UpdateItemInfo& rhs) - { - return lhs.mID == rhs.mID && lhs.mType == rhs.mType && lhs.mVersion == rhs.mVersion - && lhs.mManifestDigest == rhs.mManifestDigest; - }; - - /** - * Compares update item info. - * - * @param rhs info to compare. - * @return bool. - */ - 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. - */ -class ImageManagerItf { -public: - /** - * Destructor. - */ - 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; - - /** - * Removes update item. - * - * @param itemID update item ID. - * @param version update item version. - * @return Error. - */ - virtual Error RemoveUpdateItem(const String& itemID, const String& version) = 0; -}; - -/** @}*/ - -} // namespace aos::sm::imagemanager - -#endif diff --git a/src/core/sm/imagemanager/itf/storage.hpp b/src/core/sm/imagemanager/itf/storage.hpp deleted file mode 100644 index daf7fa786..000000000 --- a/src/core/sm/imagemanager/itf/storage.hpp +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2026 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef AOS_CORE_SM_IMAGEMANAGER_ITF_STORAGE_HPP_ -#define AOS_CORE_SM_IMAGEMANAGER_ITF_STORAGE_HPP_ - -#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. - */ -constexpr auto cMaxNumStoredUpdateItems = cMaxNumUpdateItems * 2; - -/** - * Update item data structure. - */ -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 { -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. - * - * @return RetWithError. - */ - virtual RetWithError GetUpdateItemsCount() = 0; -}; - -/** @}*/ - -} // namespace aos::sm::imagemanager - -#endif diff --git a/src/core/sm/imagemanager/tests/CMakeLists.txt b/src/core/sm/imagemanager/tests/CMakeLists.txt deleted file mode 100644 index 14ca3d44d..000000000 --- a/src/core/sm/imagemanager/tests/CMakeLists.txt +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright (C) 2025 EPAM Systems, Inc. -# -# SPDX-License-Identifier: Apache-2.0 -# - -set(TARGET_NAME imagemanager_test) - -# ###################################################################################################################### -# Sources -# ###################################################################################################################### - -set(SOURCES imagemanager.cpp) - -# ###################################################################################################################### -# Libraries -# ###################################################################################################################### - -set(LIBRARIES aos::core::sm::imagemanager aos::core::common::tests::utils GTest::gmock_main) - -# ###################################################################################################################### -# Target -# ###################################################################################################################### - -add_test( - TARGET_NAME - ${TARGET_NAME} - LOG_MODULE - SOURCES - ${SOURCES} - LIBRARIES - ${LIBRARIES} -) diff --git a/src/core/sm/imagemanager/tests/imagemanager.cpp b/src/core/sm/imagemanager/tests/imagemanager.cpp deleted file mode 100644 index 82f5a2b68..000000000 --- a/src/core/sm/imagemanager/tests/imagemanager.cpp +++ /dev/null @@ -1,1059 +0,0 @@ -/* - * Copyright (C) 2025 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "mocks/blobinfoprovidermock.hpp" -#include "mocks/imagehandlermock.hpp" -#include "stubs/storagestub.hpp" - -using namespace testing; - -namespace aos::sm::imagemanager { - -namespace { - -/*********************************************************************************************************************** - * Consts - **********************************************************************************************************************/ - -constexpr auto cTestImagePath = "/tmp/imagemanager_test/images"; -constexpr auto cUpdateItemTTL = 10 * Time::cSeconds; -constexpr auto cRemoveOutdatedPeriod = 5 * Time::cSeconds; - -/*********************************************************************************************************************** - * Static - **********************************************************************************************************************/ - -Error SplitDigest(const String& digest, String& alg, String& hash) -{ - StaticArray, 2> digestList; - - if (auto err = digest.Split(digestList, ':'); !err.IsNone()) { - return err; - } - - if (digestList.Size() != 2) { - return ErrorEnum::eInvalidArgument; - } - - alg = digestList[0]; - hash = digestList[1]; - - return ErrorEnum::eNone; -} - -fs::FileInfo GetFileInfoByDigest(const String& digest, size_t size = 1024) -{ - 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; -}; - -fs::FileInfo GetFileInfoByPath(const String& path, size_t size = 1024) -{ - 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; -}; - -StaticString GetURL(const String& digest) -{ - 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; -} - -StaticString GetBlobPath(const String& digest) -{ - StaticString path; - StaticString alg; - StaticString hash; - - auto err = SplitDigest(digest, alg, hash); - EXPECT_TRUE(err.IsNone()) << "Failed to split digest: " << tests::utils::ErrorToStr(err); - - path = fs::JoinPath(cTestImagePath, "blobs", alg, hash); - - return path; -} - -StaticString GetLayerPath(const String& digest) -{ - StaticString path; - StaticString alg; - StaticString hash; - - auto err = SplitDigest(digest, alg, hash); - EXPECT_TRUE(err.IsNone()) << "Failed to split digest: " << tests::utils::ErrorToStr(err); - - path = fs::JoinPath(cTestImagePath, "layers", alg, hash); - - return path; -} - -std::string ReadFileToString(const std::string& filename) -{ - 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(); - - return content; -} - -void CreateFile(const std::string& path, const std::string& content = "") -{ - std::ofstream file(path); - EXPECT_TRUE(file.is_open()) << "Failed to create file: " << path; - - file << content; - - file.close(); -} - -} // namespace - -/*********************************************************************************************************************** - * Suite - **********************************************************************************************************************/ - -class ImageManagerTest : public Test { -protected: - static void SetUpTestSuite() - { - tests::utils::InitLog(); - - LOG_INF() << "Image manager size" << Log::Field("size", sizeof(ImageManager)); - } - - void SetUp() override - { - Config config {cTestImagePath, 0, cUpdateItemTTL, cRemoveOutdatedPeriod}; - - 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); - - EXPECT_CALL(mBlobInfoProviderMock, GetBlobsInfo(_, _)) - .WillRepeatedly(Invoke( - [](const Array>& digests, Array>& urls) -> Error { - (void)digests; - (void)urls; - - if (!digests.Size()) { - return ErrorEnum::eInvalidArgument; - } - - if (auto err = urls.EmplaceBack(); !err.IsNone()) { - return err; - } - - urls[0] = GetURL(digests[0]); - - return ErrorEnum::eNone; - })); - EXPECT_CALL(mSpaceAllocatorMock, AllocateSpace(_)) - .WillRepeatedly(Invoke([this](uint64_t) -> RetWithError> { - auto space = MakeUnique(&mAllocator); - EXPECT_TRUE(space); - - 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)); - - return {Move(space), 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 - - constexpr auto cManifestDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - constexpr auto cLayerDigest = "sha256:4a6f6b8f5f5e3e7b9c4d3e2f1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b"; - - UpdateItemInfo itemInfo {"component1", UpdateItemTypeEnum::eComponent, "1.0.0", cManifestDigest}; - - auto manifestPath = GetBlobPath(cManifestDigest); - auto layerPath = GetBlobPath(cLayerDigest); - - auto imageManifest = std::make_unique(); - - 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}); - - // Expected calls - - 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))); - - // Install update item - - 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); - - 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(); - - CreateFile(blobPath.CStr()); - - StaticString path; - - auto err = mImageManager.GetBlobPath(cDigest, path); - EXPECT_TRUE(err.IsNone()) << "Failed to get blob path: " << tests::utils::ErrorToStr(err); - EXPECT_EQ(path, blobPath); -} - -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); -} - -TEST_F(ImageManagerTest, GetLayerPath) -{ - constexpr auto cDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - auto layerPath = fs::JoinPath(GetLayerPath(cDigest), "layer"); - - bool created = std::filesystem::create_directories(layerPath.CStr()); - EXPECT_TRUE(created) << "Failed to create layer directory at path: " << layerPath.CStr(); - - StaticString path; - - auto err = mImageManager.GetLayerPath(cDigest, path); - EXPECT_TRUE(err.IsNone()) << "Failed to get layer path: " << tests::utils::ErrorToStr(err); - EXPECT_EQ(path, layerPath); -} - -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); - - 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; - })); - EXPECT_CALL(mOCISpecMock, LoadImageConfig(_, _)) - .WillRepeatedly(Invoke([](const String& path, oci::ImageConfig& imageConfig) -> Error { - (void)path; - - imageConfig.mRootfs.mDiffIDs.EmplaceBack( - "sha256:0f9e8d7c6b5a4b3c2b1a0b9c8d7e6f5e4d3c2b1a0f9e8d7c6b5a4b3c2b1a0f9e"); - - 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"))); - - // Install update item - - for (const auto& item : installItems) { - auto err = mImageManager.InstallUpdateItem(item); - EXPECT_TRUE(err.IsNone()) << "Failed to install item: " << tests::utils::ErrorToStr(err); - } - - 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); - } - - auto installedItems = std::make_unique>(); - - auto err = mImageManager.GetAllInstalledItems(*installedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << tests::utils::ErrorToStr(err); - - for (size_t i = 0; i < installedItems->Size(); ++i) { - UpdateItemStatus status - = {installItems[i].mID, installItems[i].mType, installItems[i].mVersion, ItemStateEnum::eInstalled}; - - EXPECT_NE(installedItems->Find(status), installedItems->end()) - << "Installed item not found: " << (*installedItems)[i].mID.CStr() << " " - << (*installedItems)[i].mVersion.CStr(); - } -} - -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); - - std::vector> futures; - - for (size_t i = 0; i < 2; ++i) { - futures.emplace_back(mStorageStub.GetRemoveFuture()); - } - - 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 - - for (size_t i = 0; i < 2; ++i) { - ASSERT_EQ(futures[i].wait_for(std::chrono::milliseconds(cUpdateItemTTL.Milliseconds() * 2)), - std::future_status::ready); - - auto itemData = futures[i].get(); - - EXPECT_TRUE(itemData.mID == "item1" || itemData.mID == "item2") - << "Unexpected item ID: " << itemData.mID.CStr(); - } - - // Verify that last two removed items are still present in storage - - auto storedItems = std::make_unique(); - - err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all stored items: " << tests::utils::ErrorToStr(err); - - for (const auto& item : *storedItems) { - EXPECT_TRUE(item.mID == "item3" || item.mID == "item4") << "Unexpected item ID: " << item.mID.CStr(); - } - - // 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); - - // Verify that last two removed items are removed after remove outdated period - - futures.clear(); - - for (size_t i = 0; i < 2; ++i) { - futures.emplace_back(mStorageStub.GetRemoveFuture()); - } - - for (size_t i = 0; i < 2; ++i) { - ASSERT_EQ(futures[i].wait_for(std::chrono::milliseconds(cUpdateItemTTL.Milliseconds() * 2)), - std::future_status::ready); - - auto itemData = futures[i].get(); - - EXPECT_TRUE(itemData.mID == "item3" || itemData.mID == "item4") - << "Unexpected item ID: " << itemData.mID.CStr(); - } - - // Verify that no stored items are present in storage - - 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); -} - -TEST_F(ImageManagerTest, MaxItemVersions) -{ - UpdateItemInfo itemInfo {"service1", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"}; - - auto imageManifest = std::make_unique(); - - imageManifest->mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 1024; - - 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))); - - // 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); - } - - auto storedItems = std::make_unique(); - - // Expect 2 last installed items - - auto err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << 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 == "1.0.0"; }), - storedItems->end()) - << "Unexpected installed item version 1.0.0 found"; - - // Remove specific version - - err = mImageManager.RemoveUpdateItem("service1", "3.0.0"); - EXPECT_TRUE(err.IsNone()) << "Failed to remove update item: " << tests::utils::ErrorToStr(err); - - err = mImageManager.InstallUpdateItem({"service1", UpdateItemTypeEnum::eService, "4.0.0", - "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"}); - EXPECT_TRUE(err.IsNone()) << "Failed to install update item: " << tests::utils::ErrorToStr(err); - - // Expect this version to be removed - - storedItems->Clear(); - - err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << 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"; -} - -TEST_F(ImageManagerTest, MaxStoredItems) -{ - UpdateItemInfo itemInfo {"service1", UpdateItemTypeEnum::eService, "1.0.0", - "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"}; - - auto imageManifest = std::make_unique(); - - imageManifest->mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 1024; - - 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))); - - // Install update items up to the max limit - - 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 - - auto err = itemInfo.mID.Format("service%d", cMaxNumStoredUpdateItems + 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); - - // Expect the first installed item to be removed - - auto storedItems = std::make_unique(); - - err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << tests::utils::ErrorToStr(err); - - 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"; - - // Remove specific version and install again to verify space is freed - - err = mImageManager.RemoveUpdateItem("service42", "1.0.0"); - EXPECT_TRUE(err.IsNone()) << "Failed to remove update item: " << 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); - - err = mImageManager.InstallUpdateItem(itemInfo); - EXPECT_TRUE(err.IsNone()) << "Failed to install update item: " << tests::utils::ErrorToStr(err); - - // Expect the removed item to be gone - - storedItems->Clear(); - - err = mStorageStub.GetAllUpdateItems(*storedItems); - EXPECT_TRUE(err.IsNone()) << "Failed to get all installed items: " << 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"; -} - -TEST_F(ImageManagerTest, ValidateIntegrity) -{ - 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; - } - - 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; - })); - 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; - } - } - - fileInfo = GetFileInfoByPath(path); - - return ErrorEnum::eNone; - })); - - std::vector> futures; - - for (size_t i = 0; i < 2; ++i) { - futures.emplace_back(mStorageStub.GetRemoveFuture()); - } - - auto err = mImageManager.Start(); - EXPECT_TRUE(err.IsNone()) << "Failed to start image manager: " << tests::utils::ErrorToStr(err); - - for (size_t i = 0; i < 2; ++i) { - ASSERT_EQ( - futures[i].wait_for(std::chrono::milliseconds(cUpdateItemTTL.Milliseconds())), std::future_status::ready); - - auto itemData = futures[i].get(); - - EXPECT_TRUE(itemData.mID == "service2" || itemData.mID == "component2") - << "Unexpected item ID: " << itemData.mID.CStr(); - } - - err = mImageManager.Stop(); - EXPECT_TRUE(err.IsNone()) << "Failed to stop image manager: " << tests::utils::ErrorToStr(err); -} - -TEST_F(ImageManagerTest, RemoveOrphanBlobs) -{ - 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()}, - }; - - auto imageManifest = std::make_unique(); - - imageManifest->mConfig.mMediaType = "application/vnd.oci.image.config.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 512; - - mStorageStub.Init(itemsInfo); - - auto blobsPath = fs::JoinPath(cTestImagePath, "blobs", "sha256"); - - std::filesystem::create_directories(blobsPath.CStr()); - - // Create image config blob - - CreateFile(GetBlobPath(imageManifest->mConfig.mDigest).CStr()); - - // Create used blobs - - for (const auto& item : itemsInfo) { - CreateFile(GetBlobPath(item.mManifestDigest).CStr()); - } - - // Create orphan blobs - - for (size_t i = 0; i < 10; i++) { - StaticString digest; - - digest.Format("sha256:%064d", i + 1); - - CreateFile(GetBlobPath(digest).CStr()); - } - - std::promise promise; - - 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(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); - - for (const auto& entry : std::filesystem::directory_iterator(blobsPath.CStr())) { - bool found = false; - - for (const auto& item : itemsInfo) { - if (entry.path().c_str() == GetBlobPath(item.mManifestDigest)) { - found = true; - - break; - } - } - - if (!found) { - if (entry.path().c_str() == GetBlobPath(imageManifest->mConfig.mDigest)) { - found = true; - } - } - - EXPECT_TRUE(found) << "Orphan blob not removed: " << entry.path().c_str(); - } -} - -TEST_F(ImageManagerTest, RemoveOrphanLayers) -{ - 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()}, - }; - - auto imageManifest = std::make_unique(); - - 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}); - - auto imageConfig = std::make_unique(); - - imageConfig->mRootfs.mDiffIDs.EmplaceBack( - "sha256:5555555555555555555555555555555555555555555555555555555555555555"); - - mStorageStub.Init(itemsInfo); - - auto blobsPath = fs::JoinPath(cTestImagePath, "blobs", "sha256"); - auto layersPath = fs::JoinPath(cTestImagePath, "layers", "sha256"); - - std::filesystem::create_directories(blobsPath.CStr()); - std::filesystem::create_directories(layersPath.CStr()); - - // Create image config blob - - CreateFile(GetBlobPath(imageManifest->mConfig.mDigest).CStr()); - - // Create layer blob - - CreateFile(GetBlobPath(imageManifest->mLayers[0].mDigest).CStr(), imageConfig->mRootfs.mDiffIDs[0].CStr()); - - // Create used layers - - auto layerPath = GetLayerPath(imageConfig->mRootfs.mDiffIDs[0]); - - std::filesystem::create_directories(layerPath.CStr()); - - // Create layer digest - - CreateFile(fs::JoinPath(layerPath, "digest").CStr(), imageConfig->mRootfs.mDiffIDs[0].CStr()); - - // Create orphan layers - - for (size_t i = 0; i < 10; i++) { - StaticString digest; - - digest.Format("sha256:%064d", i + 1); - - auto layerPath = GetLayerPath(digest); - - std::filesystem::create_directories(layerPath.CStr()); - } - - std::promise promise; - - 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(mSpaceAllocatorMock, FreeSpace(_)).WillOnce(Invoke([&](size_t size) -> Error { - promise.set_value(size); - - 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]))); - - 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); - - 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(); - } -} - -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). - - constexpr auto cManifestDigest = "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - - UpdateItemInfo itemInfo {"component1", UpdateItemTypeEnum::eComponent, "1.0.0", cManifestDigest}; - - auto manifestPath = GetBlobPath(cManifestDigest); - - auto imageManifest = std::make_unique(); - - imageManifest->mConfig.mMediaType = "application/vnd.oci.empty.v1+json"; - imageManifest->mConfig.mDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"; - imageManifest->mConfig.mSize = 2; - - // Block the manifest download after the file is created so RemoveOrphans can run - // while the install is still in progress. - - std::promise downloadStartedPromise; - std::promise downloadResumePromise; - - auto downloadStartedFuture = downloadStartedPromise.get_future(); - auto downloadResumeFuture = downloadResumePromise.get_future(); - - 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; - })); - - EXPECT_CALL(mFileInfoProviderMock, GetFileInfo(_, _, _)) - .WillOnce(DoAll(SetArgReferee<1>(GetFileInfoByDigest(cManifestDigest)), Return(ErrorEnum::eNone))); - - EXPECT_CALL(mOCISpecMock, LoadImageManifest(manifestPath, _)) - .WillOnce(DoAll(SetArgReferee<1>(*imageManifest), Return(ErrorEnum::eNone))); - - // Install in background: will block inside the manifest downloader. - - auto installFuture = std::async(std::launch::async, [&]() { return mImageManager.InstallUpdateItem(itemInfo); }); - - // 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). - - ASSERT_EQ(downloadStartedFuture.wait_for(std::chrono::seconds(5)), std::future_status::ready); - - // 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. - - static_cast(&mImageManager)->RemoveItem("nonexistent", "1.0.0"); - - EXPECT_TRUE(std::filesystem::exists(manifestPath.CStr())) - << "Manifest blob was incorrectly deleted as orphan during active install"; - - // Let the download finish and verify the install completes successfully. - - downloadResumePromise.set_value(); - - auto err = installFuture.get(); - EXPECT_TRUE(err.IsNone()) << "Install failed: " << tests::utils::ErrorToStr(err); -} - -} // namespace aos::sm::imagemanager diff --git a/src/core/sm/imagemanager/tests/stubs/storagestub.hpp b/src/core/sm/imagemanager/tests/stubs/storagestub.hpp deleted file mode 100644 index fb4948faf..000000000 --- a/src/core/sm/imagemanager/tests/stubs/storagestub.hpp +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2026 EPAM Systems, Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ -#ifndef AOS_SM_IMAGE_MANAGER_TESTS_STUBS_STORAGESTUB_HPP_ -#define AOS_SM_IMAGE_MANAGER_TESTS_STUBS_STORAGESTUB_HPP_ - -#include -#include -#include - -#include - -namespace aos::sm::imagemanager { - -class StorageStub : public StorageItf { -public: - void Init(std::vector initialItems) - { - for (const auto& item : initialItems) { - mItemsList.push_back(item); - } - } - - /** - * Returns future to wait when item removed. - * - * @return future. - */ - auto GetRemoveFuture() - { - mRemovePromises.emplace(); - - return mRemovePromises.back().get_future(); - } - - /** - * Adds update item to storage. - * - * @param updateItem update item to add. - * @return Error. - */ - Error AddUpdateItem(const UpdateItemData& updateItem) 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 ErrorEnum::eNone; - } - - /** - * Updates update item in storage. - * - * @param updateItem update item to update. - * @return Error. - */ - Error UpdateUpdateItem(const UpdateItemData& updateItem) 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 ErrorEnum::eNone; - } - - mItemsList.push_back(updateItem); - - return ErrorEnum::eNone; - } - - /** - * 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 - { - 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; - } - - if (!mRemovePromises.empty()) { - mRemovePromises.front().set_value(*it); - mRemovePromises.pop(); - } - - mItemsList.erase(it); - - return ErrorEnum::eNone; - } - - /** - * 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; - } - } - } - - 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; - } - } - - return ErrorEnum::eNone; - } - - /** - * Returns count of stored update items. - * - * @return RetWithError. - */ - RetWithError GetUpdateItemsCount() override { return RetWithError(mItemsList.size()); } - -private: - std::list mItemsList; - std::queue> mRemovePromises; -}; - -} // namespace aos::sm::imagemanager - -#endif From 769dfe2991f5b5f4b2034a3e0692ee024aec74b7 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Tue, 3 Nov 2026 16:41:05 +0200 Subject: [PATCH 03/10] sm: imagemanager: add new implementation Implement the SM image manager on top of the common image manager. It downloads blobs from the CM and keeps the unpacked layers. Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- src/core/sm/CMakeLists.txt | 3 +- src/core/sm/imagemanager/CMakeLists.txt | 55 ++ src/core/sm/imagemanager/imagemanager.cpp | 663 +++++++++++++++ src/core/sm/imagemanager/imagemanager.hpp | 141 ++++ src/core/sm/imagemanager/imagemanager.md | 101 +++ src/core/sm/imagemanager/itf/imagemanager.hpp | 88 ++ src/core/sm/imagemanager/itf/storage.hpp | 37 + src/core/sm/imagemanager/tests/CMakeLists.txt | 33 + .../sm/imagemanager/tests/imagemanager.cpp | 795 ++++++++++++++++++ .../imagemanager/tests/stubs/storagestub.hpp | 94 +++ 10 files changed, 2009 insertions(+), 1 deletion(-) create mode 100644 src/core/sm/imagemanager/CMakeLists.txt create mode 100644 src/core/sm/imagemanager/imagemanager.cpp create mode 100644 src/core/sm/imagemanager/imagemanager.hpp create mode 100644 src/core/sm/imagemanager/imagemanager.md create mode 100644 src/core/sm/imagemanager/itf/imagemanager.hpp create mode 100644 src/core/sm/imagemanager/itf/storage.hpp create mode 100644 src/core/sm/imagemanager/tests/CMakeLists.txt create mode 100644 src/core/sm/imagemanager/tests/imagemanager.cpp create mode 100644 src/core/sm/imagemanager/tests/stubs/storagestub.hpp diff --git a/src/core/sm/CMakeLists.txt b/src/core/sm/CMakeLists.txt index 4dfb7fc93..701beb62b 100644 --- a/src/core/sm/CMakeLists.txt +++ b/src/core/sm/CMakeLists.txt @@ -19,7 +19,7 @@ set(HEADERS config.hpp) set(INSTALL_HEADERS aos_core_sm_database aos_core_sm_logging aos_core_sm_nodeconfig aos_core_sm_resourcemanager) -set(INSTALL_LIBRARIES aos_core_sm_launcher) +set(INSTALL_LIBRARIES aos_core_sm_imagemanager aos_core_sm_launcher) if(WITH_TEST) list(APPEND INSTALL_HEADERS aos_core_sm_tests_mocks aos_core_sm_tests_stubs) @@ -43,6 +43,7 @@ set(TARGET_PREFIX ${TARGET_PREFIX}_sm) # ###################################################################################################################### add_subdirectory(database) +add_subdirectory(imagemanager) add_subdirectory(launcher) add_subdirectory(logging) add_subdirectory(networkmanager) diff --git a/src/core/sm/imagemanager/CMakeLists.txt b/src/core/sm/imagemanager/CMakeLists.txt new file mode 100644 index 000000000..3ca541402 --- /dev/null +++ b/src/core/sm/imagemanager/CMakeLists.txt @@ -0,0 +1,55 @@ +# +# Copyright (C) 2025 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# ###################################################################################################################### +# Target name +# ###################################################################################################################### + +set(TARGET_NAME imagemanager) + +# ###################################################################################################################### +# Sources +# ###################################################################################################################### + +set(SOURCES imagemanager.cpp) + +# ###################################################################################################################### +# Headers +# ###################################################################################################################### + +set(HEADERS itf/imagehandler.hpp itf/imagemanager.hpp itf/storage.hpp imagemanager.hpp) + +# ###################################################################################################################### +# Libraries +# ###################################################################################################################### + +set(LIBRARIES aos::core::common::imagemanager aos::core::common::tools) + +# ###################################################################################################################### +# Target +# ###################################################################################################################### + +add_module( + TARGET_NAME + ${TARGET_NAME} + LOG_MODULE + STACK_USAGE + ${AOS_STACK_USAGE} + SOURCES + ${SOURCES} + HEADERS + ${HEADERS} + LIBRARIES + ${LIBRARIES} +) + +# ###################################################################################################################### +# Tests +# ###################################################################################################################### + +if(WITH_TEST) + add_subdirectory(tests) +endif() diff --git a/src/core/sm/imagemanager/imagemanager.cpp b/src/core/sm/imagemanager/imagemanager.cpp new file mode 100644 index 000000000..07f5c4f10 --- /dev/null +++ b/src/core/sm/imagemanager/imagemanager.cpp @@ -0,0 +1,663 @@ +/* + * Copyright (C) 2025 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "imagemanager.hpp" + +namespace aos::sm::imagemanager { + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +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) +{ + LOG_DBG() << "Init SM image manager"; + + 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 (auto err = common::imagemanager::ImageManager::Init( + config, blobInfoProvider, storage, spaceAllocator, fileInfoProvider, ociSpec, fsPlatform, allocator); + !err.IsNone()) { + return err; + } + + if (auto err = mLayersPath.Assign(fs::JoinPath(config.mImagePath, cLayersFolder)); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = fs::MakeDirAll(mLayersPath); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::InstallItem(const UpdateItemInfo& itemInfo) +{ + 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 common::imagemanager::ImageManager::InstallItem( + {itemInfo.mID, itemInfo.mType, itemInfo.mVersion, itemInfo.mManifestDigest, false}); +} + +Error ImageManager::RemoveItem(const String& itemID, const String& version) +{ + LOG_INF() << "Remove item" << Log::Field("itemID", itemID) << Log::Field("version", version); + + return common::imagemanager::ImageManager::RemoveItem(itemID, version); +} + +Error ImageManager::Cancel() +{ + LOG_INF() << "Cancel items"; + + // 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); + + auto err = common::imagemanager::ImageManager::Cancel(); + + SetLayersCanceled(false); + + return err; +} + +Error ImageManager::GetLayerPath(const String& diffID, String& path) const +{ + StaticString layerPath; + + if (auto err = CreateLayerPath(diffID, layerPath); !err.IsNone()) { + return err; + } + + if (auto err = path.Assign(fs::JoinPath(layerPath, cUnpackedLayerFolder)); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + LOG_DBG() << "Get layer path" << Log::Field("diffID", diffID) << Log::Field("path", path); + + auto [exists, err] = fs::DirExist(path); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (!exists) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "unpacked layer not found")); + } + + return ErrorEnum::eNone; +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ + +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); + + // 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); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::ValidateLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) +{ + (void)path; + + // 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 = CheckLayerMediaType(descriptor.mMediaType); !err.IsNone()) { + return err; + } + + StaticString layerPath; + + if (auto err = CreateLayerPath(diffID, layerPath); !err.IsNone()) { + return err; + } + + if (auto err = LockLayer(diffID); !err.IsNone()) { + return err; + } + + auto unlockLayer = DeferRelease([this, &diffID]() { UnlockLayer(diffID); }); + + if (auto err = ValidateUnpackedLayer(layerPath); !err.IsNone()) { + return err; + } + + return ErrorEnum::eNone; +} + +Error ImageManager::UnpackLayer(const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) +{ + // 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")); + } + + if (auto err = CheckLayerMediaType(descriptor.mMediaType); !err.IsNone()) { + return err; + } + + StaticString layerPath; + + if (auto err = CreateLayerPath(diffID, layerPath); !err.IsNone()) { + return err; + } + + if (auto err = LockLayer(diffID); !err.IsNone()) { + return err; + } + + auto unlockLayer = DeferRelease([this, &diffID]() { UnlockLayer(diffID); }); + + // 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); + + return ErrorEnum::eNone; + } + + // RemoveLayer keeps the layer of the item being installed, as the item references it, so it is removed here. + auto [size, err] = RemoveLayerFiles(layerPath); + + // The size of the files removed before a failure is freed as well. + if (size > 0) { + GetSpaceAllocator().FreeSpace(size); + } + + if (!err.IsNone()) { + return err; + } + + if (err = InstallUnpackedLayer(path, descriptor.mMediaType, layerPath); !err.IsNone()) { + return err; + } + + RemoveLayerBlob(path); + + return ErrorEnum::eNone; +} + +RetWithError ImageManager::RemoveLayer( + const String& path, const oci::ContentDescriptor& descriptor, const String& diffID) +{ + (void)path; + (void)descriptor; + + if (diffID.IsEmpty()) { + return {0, ErrorEnum::eNone}; + } + + StaticString layerPath; + + if (auto err = CreateLayerPath(diffID, layerPath); !err.IsNone()) { + return {0, 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}; + } + + auto unlockLayer = DeferRelease([this, &diffID]() { UnlockLayer(diffID); }); + + auto [used, err] = mStorage->IsLayerUsed(diffID); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (used) { + return {0, ErrorEnum::eNone}; + } + + 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 {size, ErrorEnum::eNone}; +} + +RetWithError ImageManager::RemoveOrphanLayers() +{ + LOG_DBG() << "Remove orphan layers"; + + size_t freedSize = 0; + Error firstErr; + + // 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); + + if (firstErr.IsNone()) { + firstErr = err; + } + } + + freedSize += size; + }; + + for (auto algIt = fs::DirIterator(mLayersPath); algIt.Next();) { + const auto algPath = fs::JoinPath(mLayersPath, algIt->mPath); + + // The layers folder contains only the algorithm folders. + if (!algIt->mIsDir) { + removeLayer(algPath, ""); + + continue; + } + + for (auto layerIt = fs::DirIterator(algPath); layerIt.Next();) { + StaticString diffID; + + // 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); + } + + removeLayer(fs::JoinPath(algPath, layerIt->mPath), diffID); + } + } + + return {freedSize, firstErr}; +} + +Error ImageManager::CreateLayerPath(const String& diffID, String& path) const +{ + StaticString algorithm; + StaticString hash; + + if (auto err = ParseDigest(diffID, algorithm, hash); !err.IsNone()) { + return err; + } + + if (auto err = path.Assign(fs::JoinPath(mLayersPath, algorithm, hash)); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::ValidateUnpackedLayer(const String& layerPath) const +{ + LOG_DBG() << "Validate unpacked layer" << Log::Field("path", layerPath); + + const auto unpackedPath = fs::JoinPath(layerPath, cUnpackedLayerFolder); + const auto digestPath = fs::JoinPath(layerPath, cDigestFile); + + auto [exists, err] = fs::DirExist(unpackedPath); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (!exists) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "unpacked layer not found")); + } + + // The metadata is written last, so the layer without it is not completely unpacked. + const StaticString metadataPaths[] = {digestPath, fs::JoinPath(layerPath, cSizeFile)}; + + for (const auto& metadataPath : metadataPaths) { + if (Tie(exists, err) = fs::FileExist(metadataPath); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (!exists) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "layer metadata not found")); + } + } + + StaticString storedDigest; + + if (err = fs::ReadFileToString(digestPath, storedDigest); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + StaticString layerDigest; + + if (Tie(layerDigest, err) = mImageHandler->GetUnpackedLayerDigest(unpackedPath); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (layerDigest != storedDigest) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidChecksum, "wrong layer checksum")); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::InstallUnpackedLayer(const String& path, const String& mediaType, const String& layerPath) +{ + LOG_DBG() << "Unpack layer" << Log::Field("path", path) << Log::Field("layerPath", layerPath); + + auto [unpackedSize, sizeErr] = mImageHandler->GetUnpackedLayerSize(path, mediaType); + if (!sizeErr.IsNone() && !sizeErr.Is(ErrorEnum::eNotSupported)) { + return AOS_ERROR_WRAP(sizeErr); + } + + auto [space, err] = GetSpaceAllocator().AllocateSpace(unpackedSize); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + err = UnpackToLayerPath(path, mediaType, layerPath, unpackedSize, *space); + if (err.IsNone()) { + if (err = space->Accept(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; + } + + 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); + + return err; + } + + 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); + } + + 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; + } + + // 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); + } + + if (err = space.Resize(size); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error ImageManager::CreateLayerMetadata(const String& layerPath, size_t unpackedSize) +{ + auto [digest, err] = mImageHandler->GetUnpackedLayerDigest(fs::JoinPath(layerPath, cUnpackedLayerFolder)); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + LOG_DBG() << "Create layer metadata" << Log::Field("path", layerPath) << Log::Field("digest", digest) + << Log::Field("size", unpackedSize); + + StaticString<64> sizeStr; + + if (err = sizeStr.Convert(unpackedSize); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (err = fs::WriteStringToFile(fs::JoinPath(layerPath, cSizeFile), sizeStr, 0600); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (err = fs::WriteStringToFile(fs::JoinPath(layerPath, cDigestFile), digest, 0600); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +RetWithError ImageManager::RemoveLayerFiles(const String& layerPath) +{ + 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)}; + } + + if (!exists) { + if (Tie(exists, err) = fs::DirExist(layerPath); !err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + } + + if (!exists) { + return {0, ErrorEnum::eNone}; + } + + size_t size = 0; + + if (Tie(size, err) = fs::CalculateSize(*mAllocator, 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)}; + } + + return {size, ErrorEnum::eNone}; +} + +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 (err = space.Accept(); !err.IsNone()) { + LOG_ERR() << "Failed to accept space" << Log::Field("path", layerPath) << Log::Field(AOS_ERROR_WRAP(err)); + } +} + +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()) { + LOG_ERR() << "Failed to remove layer blob" << Log::Field("path", path) << Log::Field(AOS_ERROR_WRAP(err)); + + return; + } + + if (!exists) { + return; + } + + size_t size = 0; + + 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; + } + + 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; + } + + if (size > 0) { + GetSpaceAllocator().FreeSpace(size); + } +} + +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; +} + +RetWithError ImageManager::RemoveOrphanLayer(const String& layerPath, const String& diffID) +{ + 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}; + } + + auto unlockLayer = DeferRelease([this, &diffID]() { UnlockLayer(diffID); }); + + auto [used, err] = mStorage->IsLayerUsed(diffID); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (used) { + return {0, ErrorEnum::eNone}; + } + + LOG_DBG() << "Remove orphan layer" << Log::Field("diffID", diffID); + + 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); + } + + if (mNumCancels > 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eCanceled, "layer lock canceled")); + } + + if (auto err = mLayersInProgress.PushBack(diffID); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +bool ImageManager::TryLockLayer(const String& diffID) +{ + LockGuard lock {mLayersMutex}; + + if (IsLayerInProgress(diffID)) { + return false; + } + + if (mLayersInProgress.IsFull()) { + LOG_ERR() << "Layers in progress list is full" << Log::Field("diffID", diffID); + + return false; + } + + if (auto err = mLayersInProgress.PushBack(diffID); !err.IsNone()) { + return false; + } + + return true; +} + +void ImageManager::SetLayersCanceled(bool canceled) +{ + LockGuard lock {mLayersMutex}; + + // Counted, as Cancel may be called concurrently. + if (canceled) { + mNumCancels++; + } else { + mNumCancels--; + } + + if (auto err = mLayersCondVar.NotifyAll(); !err.IsNone()) { + LOG_ERR() << "Can't notify condition variable" << Log::Field(err); + } +} + +void ImageManager::UnlockLayer(const String& diffID) +{ + LockGuard lock {mLayersMutex}; + + (void)mLayersInProgress.RemoveIf([&diffID](const String& layer) { return layer == diffID; }); + + if (auto err = mLayersCondVar.NotifyAll(); !err.IsNone()) { + LOG_ERR() << "Can't notify condition variable" << Log::Field(err); + } +} + +} // namespace aos::sm::imagemanager diff --git a/src/core/sm/imagemanager/imagemanager.hpp b/src/core/sm/imagemanager/imagemanager.hpp new file mode 100644 index 000000000..ab00955c9 --- /dev/null +++ b/src/core/sm/imagemanager/imagemanager.hpp @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2025 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_CORE_SM_IMAGEMANAGER_IMAGEMANAGER_HPP_ +#define AOS_CORE_SM_IMAGEMANAGER_IMAGEMANAGER_HPP_ + +#include +#include + +#include "itf/imagehandler.hpp" +#include "itf/imagemanager.hpp" +#include "itf/storage.hpp" + +namespace aos::sm::imagemanager { + +/** @addtogroup sm Service 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 common::imagemanager::ImageManager, public ImageManagerItf { +public: + /** + * Initializes image manager. + * + * @param config image manager config. + * @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 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 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); + + /** + * 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 InstallItem(const UpdateItemInfo& itemInfo) override; + + /** + * 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 RemoveItem(const String& itemID, const String& version) override; + + /** + * 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. + * + * @return Error. + */ + Error Cancel() override; + + /** + * 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 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& diffID, String& path) const override; + +private: + static constexpr auto cLayersFolder = "layers"; + static constexpr auto cUnpackedLayerFolder = "layer"; + static constexpr auto cDigestFile = "digest"; + static constexpr auto cSizeFile = "size"; + // 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 {}; +}; + +/** @}*/ + +} // namespace aos::sm::imagemanager + +#endif diff --git a/src/core/sm/imagemanager/imagemanager.md b/src/core/sm/imagemanager/imagemanager.md new file mode 100644 index 000000000..d636a8c07 --- /dev/null +++ b/src/core/sm/imagemanager/imagemanager.md @@ -0,0 +1,101 @@ +# Image manager + +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, 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::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 + direction LR + + class ImageManager ["aos::sm::imagemanager::ImageManager"] { + } + + class CommonImageManager ["aos::common::imagemanager::ImageManager"] { + } + + class ImageManagerItf ["aos::sm::imagemanager::ImageManagerItf"] { + <> + } + + class ItemInfoProviderItf ["aos::common::imagemanager::ItemInfoProviderItf"] { + <> + } + + class ItemStatusProviderItf ["aos::common::imagemanager::ItemStatusProviderItf"] { + <> + } + + class ImageHandlerItf ["aos::sm::imagemanager::ImageHandlerItf"] { + <> + } + + class StorageItf ["aos::sm::imagemanager::StorageItf"] { + <> + } + + class BlobInfoProviderItf ["aos::common::imagemanager::BlobInfoProviderItf"] { + <> + } + + class DownloaderItf ["aos::downloader::DownloaderItf"] { + <> + } + + ImageManager --|> CommonImageManager + ImageManager ..|> ImageManagerItf + CommonImageManager ..|> ItemInfoProviderItf + CommonImageManager ..|> ItemStatusProviderItf + + ImageManager ..> ImageHandlerItf + ImageManager ..> StorageItf + ImageManager ..> DownloaderItf + CommonImageManager ..> BlobInfoProviderItf +``` diff --git a/src/core/sm/imagemanager/itf/imagemanager.hpp b/src/core/sm/imagemanager/itf/imagemanager.hpp new file mode 100644 index 000000000..5447b38ec --- /dev/null +++ b/src/core/sm/imagemanager/itf/imagemanager.hpp @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2025 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_CORE_SM_IMAGEMANAGER_ITF_IMAGEMANAGER_HPP_ +#define AOS_CORE_SM_IMAGEMANAGER_ITF_IMAGEMANAGER_HPP_ + +#include +#include + +namespace aos::sm::imagemanager { + +/** @addtogroup sm Service Manager + * @{ + */ + +/** + * Update item info. + */ +struct UpdateItemInfo { + StaticString mID; + UpdateItemType mType; + StaticString mVersion; + StaticString mManifestDigest; + + /** + * Compares update item info. + * + * @param rhs info to compare. + * @return bool. + */ + friend bool operator==(const UpdateItemInfo& lhs, const UpdateItemInfo& rhs) + { + return lhs.mID == rhs.mID && lhs.mType == rhs.mType && lhs.mVersion == rhs.mVersion + && lhs.mManifestDigest == rhs.mManifestDigest; + }; + + /** + * Compares update item info. + * + * @param rhs info to compare. + * @return bool. + */ + friend bool operator!=(const UpdateItemInfo& lhs, const UpdateItemInfo& rhs) { return !(lhs == rhs); }; +}; + +/** + * Image manager interface. + */ +class ImageManagerItf { +public: + /** + * Destructor. + */ + virtual ~ImageManagerItf() = default; + + /** + * Installs update item. + * + * @param itemInfo update item info. + * @return Error. + */ + virtual Error InstallItem(const UpdateItemInfo& itemInfo) = 0; + + /** + * Removes update item. + * + * @param itemID update item ID. + * @param version update item version. + * @return Error. + */ + 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; +}; + +/** @}*/ + +} // namespace aos::sm::imagemanager + +#endif diff --git a/src/core/sm/imagemanager/itf/storage.hpp b/src/core/sm/imagemanager/itf/storage.hpp new file mode 100644 index 000000000..10c9329d9 --- /dev/null +++ b/src/core/sm/imagemanager/itf/storage.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_CORE_SM_IMAGEMANAGER_ITF_STORAGE_HPP_ +#define AOS_CORE_SM_IMAGEMANAGER_ITF_STORAGE_HPP_ + +#include + +namespace aos::sm::imagemanager { + +/** @addtogroup sm Service Manager + * @{ + */ + +/** + * SM image manager storage interface. + */ +class StorageItf : public virtual common::imagemanager::StorageItf { +public: + /** + * Checks if layer is referenced by any existing deployable item, regardless of the item state. Blob references + * left without an item are ignored. + * + * @param diffID layer diff ID. + * @return RetWithError. + */ + virtual RetWithError IsLayerUsed(const String& diffID) = 0; +}; + +/** @}*/ + +} // namespace aos::sm::imagemanager + +#endif diff --git a/src/core/sm/imagemanager/tests/CMakeLists.txt b/src/core/sm/imagemanager/tests/CMakeLists.txt new file mode 100644 index 000000000..14ca3d44d --- /dev/null +++ b/src/core/sm/imagemanager/tests/CMakeLists.txt @@ -0,0 +1,33 @@ +# +# Copyright (C) 2025 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +set(TARGET_NAME imagemanager_test) + +# ###################################################################################################################### +# Sources +# ###################################################################################################################### + +set(SOURCES imagemanager.cpp) + +# ###################################################################################################################### +# Libraries +# ###################################################################################################################### + +set(LIBRARIES aos::core::sm::imagemanager aos::core::common::tests::utils GTest::gmock_main) + +# ###################################################################################################################### +# Target +# ###################################################################################################################### + +add_test( + TARGET_NAME + ${TARGET_NAME} + LOG_MODULE + SOURCES + ${SOURCES} + LIBRARIES + ${LIBRARIES} +) diff --git a/src/core/sm/imagemanager/tests/imagemanager.cpp b/src/core/sm/imagemanager/tests/imagemanager.cpp new file mode 100644 index 000000000..beccd8a7c --- /dev/null +++ b/src/core/sm/imagemanager/tests/imagemanager.cpp @@ -0,0 +1,795 @@ +/* + * Copyright (C) 2025 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "mocks/imagehandlermock.hpp" +#include "stubs/storagestub.hpp" + +using namespace testing; + +namespace aos::sm::imagemanager { + +namespace { + +/*********************************************************************************************************************** + * Consts + **********************************************************************************************************************/ + +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 + **********************************************************************************************************************/ + +std::string DigestPath(const std::string& folder, const std::string& digest) +{ + auto pos = digest.find(':'); + + return std::string(cTestImagePath) + "/" + folder + "/" + digest.substr(0, pos) + "/" + digest.substr(pos + 1); +} + +std::string BlobPath(const std::string& digest) +{ + return DigestPath("blobs", digest); +} + +std::string LayerPath(const std::string& diffID) +{ + return DigestPath("layers", diffID); +} + +std::string UnpackedPath(const std::string& diffID) +{ + return LayerPath(diffID) + "/layer"; +} + +bool Exists(const std::string& path) +{ + auto [exists, err] = fs::FileExist(path.c_str()); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + if (exists) { + return true; + } + + Tie(exists, err) = fs::DirExist(path.c_str()); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + return exists; +} + +void WriteFile(const std::string& path, const std::string& content) +{ + auto err = fs::MakeDirAll(fs::Dir(path.c_str())); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::ofstream file(path); + ASSERT_TRUE(file.is_open()) << path; + + file << content; +} + +std::string ReadFile(const std::string& path) +{ + std::ifstream file(path, std::ios::binary); + + return std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); +} + +oci::ContentDescriptor Descriptor(const String& mediaType, const String& digest) +{ + return oci::ContentDescriptor {mediaType, digest, cBlobSize}; +} + +} // namespace + +/*********************************************************************************************************************** + * Suite + **********************************************************************************************************************/ + +class SMImageManagerTest : public Test { +protected: + static void SetUpTestSuite() + { + tests::utils::InitLog(); + + LOG_INF() << "Image manager size" << Log::Field("size", sizeof(ImageManager)); + } + + void SetUp() override + { + auto err = fs::RemoveAll(cTestDir); + ASSERT_TRUE(err.IsNone()) << 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); + + ON_CALL(mBlobInfoProvider, GetBlobInfo).WillByDefault(Invoke([](const String& digest, BlobInfo& info) -> Error { + info.mDigest = digest; + info.mSize = cBlobSize; + + auto err = info.mURLs.EmplaceBack("https://cm/blob"); + if (!err.IsNone()) { + return err; + } + + return info.mURLs.EmplaceBack("https://cm/fallback"); + })); + + ON_CALL(mDownloader, Download) + .WillByDefault(Invoke([this](CancelContext&, const String& digest, const Array>&, + const String& path) -> Error { + { + std::lock_guard lock {mMutex}; + + mDownloaded.insert(digest.CStr()); + } + + WriteFile(path.CStr(), cBlobContent); + + return ErrorEnum::eNone; + })); + + ON_CALL(mFileInfoProvider, GetFileInfo) + .WillByDefault(Invoke([this](const String& path, fs::FileInfo& info, crypto::Hash) -> Error { + const std::string filePath = path.CStr(); + + StaticString hash; + + hash = filePath.substr(filePath.rfind('/') + 1).c_str(); + + if (auto err = hash.HexToByteArray(info.mCheckSum); !err.IsNone()) { + return err; + } + + std::lock_guard lock {mMutex}; + + if (mBrokenBlobs.count(filePath) != 0) { + info.mCheckSum.Clear(); + } + + info.mSize = ReadFile(filePath).size(); + + return ErrorEnum::eNone; + })); + + ON_CALL(mOCISpec, LoadImageManifest) + .WillByDefault(Invoke([this](const String& path, oci::ImageManifest& manifest) -> Error { + manifest = *mManifests.at(path.CStr()); + + return ErrorEnum::eNone; + })); + + ON_CALL(mOCISpec, LoadImageConfig) + .WillByDefault(Invoke([this](const String&, oci::ImageConfig& config) -> Error { + config = *mImageConfig; + + return ErrorEnum::eNone; + })); + + ON_CALL(mImageHandler, GetUnpackedLayerSize).WillByDefault(Return(RetWithError(cUnpackedSize))); + + ON_CALL(mImageHandler, UnpackLayer) + .WillByDefault(Invoke([](const String&, const String& dst, const String&) -> Error { + WriteFile(std::string(dst.CStr()) + "/data", cUnpackedData); + + return ErrorEnum::eNone; + })); + + ON_CALL(mImageHandler, GetUnpackedLayerDigest) + .WillByDefault(Invoke([](const String& path) -> RetWithError> { + StaticString digest; + + digest = ReadFile(std::string(path.CStr()) + "/data").c_str(); + + return digest; + })); + + ON_CALL(mSpaceAllocator, AllocateSpace) + .WillByDefault(Invoke([this](size_t size) -> RetWithError> { + auto space = MakeUnique>(&mAllocator); + + ON_CALL(*space, Accept()).WillByDefault(Invoke([this, size]() { + mAcceptedAllocations.push_back(size); + + return ErrorEnum::eNone; + })); + ON_CALL(*space, Release()).WillByDefault(Invoke([this]() { + mReleasedSpaces++; + + return ErrorEnum::eNone; + })); + ON_CALL(*space, Resize(_)).WillByDefault(Invoke([this](size_t resized) { + mLastResize = resized; + + return ErrorEnum::eNone; + })); + + return {Move(space), ErrorEnum::eNone}; + })); + + AddService(cManifestDigest, cLayerDigest); + + mImageConfig = std::make_unique(); + mImageConfig->mRootfs.mDiffIDs.EmplaceBack(cDiffID); + } + + void TearDown() override + { + auto err = fs::RemoveAll(cTestDir); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + } + + static common::imagemanager::Config CreateConfig(const String& imagePath) + { + common::imagemanager::Config config; + + config.mImagePath = imagePath; + config.mItemCacheTTL = cItemCacheTTL; + config.mRemoveOutdatedPeriod = cRemoveOutdatedPeriod; + + return config; + } + + void AddService(const std::string& manifestDigest, const std::string& layerDigest) + { + auto manifest = std::make_unique(); + + manifest->mConfig = Descriptor(cConfigType, cConfigDigest); + manifest->mLayers.EmplaceBack(Descriptor(cLayerType, layerDigest.c_str())); + + mManifests[BlobPath(manifestDigest)] = std::move(manifest); + } + + void AddComponent(const std::string& manifestDigest, const std::string& layerDigest) + { + auto manifest = std::make_unique(); + + manifest->mConfig = oci::ContentDescriptor {oci::cMediaTypeEmptyBlob, cEmptyDigest, 0}; + manifest->mLayers.EmplaceBack(Descriptor(cLayerType, layerDigest.c_str())); + + mManifests[BlobPath(manifestDigest)] = std::move(manifest); + } + + static UpdateItemInfo ServiceInfo(const String& itemID, const String& digest = cManifestDigest) + { + return UpdateItemInfo {itemID, UpdateItemTypeEnum::eService, "1.0.0", digest}; + } + + std::optional GetItemState(const String& itemID) + { + auto statuses = std::make_unique>(); + + auto err = mImageManager.GetItemsStatuses(*statuses); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + for (const auto& status : *statuses) { + if (status.mItemID == itemID) { + return status.mState; + } + } + + return std::nullopt; + } + + // 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 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 {}; +}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +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; + + StaticString imagePath; + + imagePath.Resize(maxBlobImagePathSize); + + for (auto& ch : imagePath) { + ch = 'a'; + } + + ImageManager imageManager; + + 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(SMImageManagerTest, InstallServiceUnpacksLayerAndRemovesBlob) +{ + EXPECT_CALL(mDownloader, + Download(_, _, Truly([](const Array>& urls) { return urls.Size() == 2; }), _)) + .Times(3); + std::string unpackSrc; + std::string unpackDst; + std::string unpackType; + + 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(); + + WriteFile(unpackDst + "/data", cUnpackedData); + + return ErrorEnum::eNone; + })); + + // The layer blob is removed after the layer is unpacked and its space is freed. + EXPECT_CALL(mSpaceAllocator, FreeSpace(strlen(cBlobContent))); + + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(unpackSrc, BlobPath(cLayerDigest)); + EXPECT_EQ(unpackDst, UnpackedPath(cDiffID)); + EXPECT_EQ(unpackType, cLayerType); + + 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)); + + // Metadata blobs and the layer blob are allocated with the blob size, the layer with its unpacked size. + EXPECT_EQ(mAcceptedAllocations.back(), cUnpackedSize); + + // 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()); + + StaticString path; + + err = mImageManager.GetLayerPath(cDiffID, path); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + EXPECT_EQ(UnpackedPath(cDiffID), path.CStr()); +} + +TEST_F(SMImageManagerTest, InstallComponentKeepsLayerPacked) +{ + AddComponent(cManifest2Digest, cLayerDigest); + + EXPECT_CALL(mImageHandler, UnpackLayer).Times(0); + + auto err = mImageManager.InstallItem( + UpdateItemInfo {"component", UpdateItemTypeEnum::eComponent, "1.0.0", cManifest2Digest}); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetItemState("component"), ItemState(ItemStateEnum::eInstalled)); + EXPECT_TRUE(Exists(BlobPath(cLayerDigest))); + EXPECT_FALSE(Exists(LayerPath(cDiffID))); +} + +TEST_F(SMImageManagerTest, InstallAgainKeepsValidLayer) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + // The packed blob is not needed if the unpacked layer is valid. + ASSERT_FALSE(Exists(BlobPath(cLayerDigest))); + + mDownloaded.clear(); + + EXPECT_CALL(mImageHandler, UnpackLayer).Times(0); + + err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_TRUE(mDownloaded.empty()); +} + +TEST_F(SMImageManagerTest, InstallAgainRepairsBrokenLayer) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + WriteFile(UnpackedPath(cDiffID) + "/data", "broken"); + WriteFile(UnpackedPath(cDiffID) + "/garbage", "garbage"); + + const auto oldSize + = strlen("broken") + strlen("garbage") + strlen(cUnpackedData) + std::to_string(cUnpackedSize).size(); + + mDownloaded.clear(); + + // 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_CALL(mSpaceAllocator, FreeSpace(oldSize)); + EXPECT_CALL(mSpaceAllocator, FreeSpace(strlen(cBlobContent))); + } + + EXPECT_CALL(mImageHandler, UnpackLayer); + + err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << 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))); +} + +TEST_F(SMImageManagerTest, InstallAgainFinishesInterruptedUnpack) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_TRUE(fs::RemoveAll((LayerPath(cDiffID) + "/digest").c_str()).IsNone()); + + EXPECT_CALL(mImageHandler, UnpackLayer); + + err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_TRUE(Exists(LayerPath(cDiffID) + "/digest")); +} + +TEST_F(SMImageManagerTest, UnpackFailureLeavesNoLayer) +{ + EXPECT_CALL(mImageHandler, UnpackLayer) + .WillOnce(Invoke([](const String&, const String& dst, const String&) -> Error { + WriteFile(std::string(dst.CStr()) + "/partial", "partial"); + + return ErrorEnum::eFailed; + })); + + // The layer blob is kept on a failed unpack. + EXPECT_CALL(mSpaceAllocator, FreeSpace).Times(0); + + auto err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.Is(ErrorEnum::eFailed)) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(Exists(LayerPath(cDiffID))); + EXPECT_TRUE(Exists(BlobPath(cLayerDigest))); + EXPECT_EQ(mReleasedSpaces, 1); + EXPECT_EQ(GetItemState("service"), ItemState(ItemStateEnum::eFailed)); +} + +TEST_F(SMImageManagerTest, SpaceAllocationFailureLeavesNoLayer) +{ + EXPECT_CALL(mSpaceAllocator, AllocateSpace(_)).WillRepeatedly(DoDefault()); + EXPECT_CALL(mSpaceAllocator, AllocateSpace(static_cast(cUnpackedSize))) + .WillOnce(Return(RetWithError>(nullptr, ErrorEnum::eNoMemory))); + + auto err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.Is(ErrorEnum::eNoMemory)) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(Exists(LayerPath(cDiffID))); +} + +TEST_F(SMImageManagerTest, UnsupportedLayerMediaTypeFails) +{ + auto manifest = std::make_unique(); + + manifest->mConfig = Descriptor(cConfigType, cConfigDigest); + manifest->mLayers.EmplaceBack(Descriptor("application/vnd.oci.image.layer.v1.tar+zstd", cLayerDigest)); + + mManifests[BlobPath(cManifestDigest)] = std::move(manifest); + + EXPECT_CALL(mImageHandler, UnpackLayer).Times(0); + + 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(SMImageManagerTest, BrokenDownloadFails) +{ + { + std::lock_guard lock {mMutex}; + + mBrokenBlobs.insert(BlobPath(cManifestDigest)); + } + + auto err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidChecksum)) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetItemState("service"), ItemState(ItemStateEnum::eFailed)); +} + +TEST_F(SMImageManagerTest, DownloadErrorIsReturned) +{ + EXPECT_CALL(mDownloader, Download).WillOnce(Return(ErrorEnum::eIntegrity)); + + auto err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.Is(ErrorEnum::eIntegrity)) << tests::utils::ErrorToStr(err); +} + +TEST_F(SMImageManagerTest, CancelStopsDownload) +{ + std::promise started; + + EXPECT_CALL(mDownloader, Download) + .WillOnce(Invoke([&](CancelContext& ctx, const String&, const Array>&, const String&) { + started.set_value(); + + return ctx.Wait(); + })) + .WillRepeatedly(DoDefault()); + + auto install + = std::async(std::launch::async, [this]() { return mImageManager.InstallItem(ServiceInfo("service")); }); + + started.get_future().wait(); + + auto err = mImageManager.Cancel(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = install.get(); + EXPECT_TRUE(err.Is(ErrorEnum::eCanceled)) << tests::utils::ErrorToStr(err); + + // The items installed after the cancel are not affected. + err = mImageManager.InstallItem(ServiceInfo("service")); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +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)); + + WriteFile(std::string(dst.CStr()) + "/data", cUnpackedData); + + return ErrorEnum::eNone; + })); + + 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)); }); + + auto err = install1.get(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = install2.get(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + // The blob of the layer unpacked or found valid is not kept. + EXPECT_FALSE(Exists(BlobPath(cLayerDigest))); + EXPECT_FALSE(Exists(BlobPath(cLayer2Digest))); +} + +TEST_F(SMImageManagerTest, CancelStopsWaitForLayer) +{ + AddService(cManifest2Digest, cLayer2Digest); + + std::promise unpackStarted; + std::promise unpackReleased; + auto unpackReleasedFuture = unpackReleased.get_future(); + + EXPECT_CALL(mImageHandler, UnpackLayer) + .WillOnce(Invoke([&](const String&, const String& dst, const String&) -> Error { + unpackStarted.set_value(); + unpackReleasedFuture.wait(); + + WriteFile(std::string(dst.CStr()) + "/data", cUnpackedData); + + return ErrorEnum::eNone; + })); + + auto install1 + = std::async(std::launch::async, [this]() { return mImageManager.InstallItem(ServiceInfo("service1")); }); + + unpackStarted.get_future().wait(); + + // 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)); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto cancel = std::async(std::launch::async, [this]() { return mImageManager.Cancel(); }); + + // The wait for the layer is canceled while the first item still unpacks it. + auto status = install2.wait_for(std::chrono::seconds(5)); + + unpackReleased.set_value(); + + EXPECT_EQ(status, std::future_status::ready); + + auto err = install2.get(); + EXPECT_TRUE(err.Is(ErrorEnum::eCanceled)) << tests::utils::ErrorToStr(err); + + // The first item may finish the unpack or fail with eCanceled on the next blob. + install1.wait(); + + err = cancel.get(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(SMImageManagerTest, RemoveItemKeepsLayerInCache) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mSpaceAllocator, AddOutdatedItem(String("service"), String("1.0.0"), _)); + + err = mImageManager.RemoveItem("service", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(GetItemState("service"), ItemState(ItemStateEnum::eRemoved)); + EXPECT_TRUE(Exists(UnpackedPath(cDiffID))); +} + +TEST_F(SMImageManagerTest, ItemRemoverRemovesUnusedLayer) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mImageManager.RemoveItem("service", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + const auto layerSize = strlen(cUnpackedData) * 2 + std::to_string(cUnpackedSize).size(); + + size_t size = 0; + + // The space allocator frees the returned size, so nothing is freed by the image manager. + EXPECT_CALL(mSpaceAllocator, FreeSpace).Times(0); + + Tie(size, err) = mImageManager.GetItemRemover().RemoveItem("service", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << 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))); +} + +TEST_F(SMImageManagerTest, ItemRemoverKeepsLayerUsedByAnotherItem) +{ + AddService(cManifest2Digest, cLayer2Digest); + + auto err = mImageManager.InstallItem(ServiceInfo("service1")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mImageManager.InstallItem(ServiceInfo("service2", cManifest2Digest)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mImageManager.RemoveItem("service1", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + size_t size = 0; + + Tie(size, err) = mImageManager.GetItemRemover().RemoveItem("service1", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(Exists(BlobPath(cLayerDigest))); + EXPECT_TRUE(Exists(UnpackedPath(cDiffID))); + + err = mImageManager.RemoveItem("service2", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + Tie(size, err) = mImageManager.GetItemRemover().RemoveItem("service2", "1.0.0"); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(Exists(LayerPath(cDiffID))); +} + +TEST_F(SMImageManagerTest, CleanupRemovesOrphanLayers) +{ + auto err = mImageManager.InstallItem(ServiceInfo("service")); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + const auto orphanFile = UnpackedPath(cOrphanDiffID) + "/data"; + const auto strayFile = std::string(cTestImagePath) + "/layers/stray"; + + WriteFile(orphanFile, "orphan"); + WriteFile(strayFile, "stray"); + + std::promise freed; + + EXPECT_CALL(mSpaceAllocator, FreeSpace(strlen("orphan") + strlen("stray"))).WillOnce(InvokeWithoutArgs([&]() { + freed.set_value(); + })); + + err = mImageManager.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(freed.get_future().wait_for(std::chrono::seconds(5)), std::future_status::ready); + + err = mImageManager.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_FALSE(Exists(LayerPath(cOrphanDiffID))); + EXPECT_FALSE(Exists(strayFile)); + EXPECT_TRUE(Exists(UnpackedPath(cDiffID))); +} + +TEST_F(SMImageManagerTest, GetLayerPathErrors) +{ + StaticString path; + + auto err = mImageManager.GetLayerPath("malformed", path); + EXPECT_TRUE(err.Is(ErrorEnum::eInvalidArgument)) << tests::utils::ErrorToStr(err); + + err = mImageManager.GetLayerPath(cDiffID, path); + EXPECT_TRUE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); +} + +TEST_F(SMImageManagerTest, GetBlobPath) +{ + StaticString path; + + auto err = mImageManager.GetBlobPath(cLayerDigest, path); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(BlobPath(cLayerDigest), path.CStr()); +} + +} // namespace aos::sm::imagemanager diff --git a/src/core/sm/imagemanager/tests/stubs/storagestub.hpp b/src/core/sm/imagemanager/tests/stubs/storagestub.hpp new file mode 100644 index 000000000..b970d14dc --- /dev/null +++ b/src/core/sm/imagemanager/tests/stubs/storagestub.hpp @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef AOS_SM_IMAGE_MANAGER_TESTS_STUBS_STORAGESTUB_HPP_ +#define AOS_SM_IMAGE_MANAGER_TESTS_STUBS_STORAGESTUB_HPP_ + +#include + +#include +#include + +namespace aos::sm::imagemanager { + +/** + * Storage stub. + */ +class StorageStub : public StorageItf { +public: + Error AddDeployableItem(const common::imagemanager::DeployableItemInfo& item) override + { + return mStorage.AddDeployableItem(item); + } + + Error UpdateDeployableItem(const common::imagemanager::DeployableItemInfo& item) override + { + return mStorage.UpdateDeployableItem(item); + } + + Error RemoveDeployableItem(const String& itemID, const String& version) override + { + return mStorage.RemoveDeployableItem(itemID, version); + } + + Error GetDeployableItem( + const String& itemID, const String& version, common::imagemanager::DeployableItemInfo& item) override + { + return mStorage.GetDeployableItem(itemID, version, item); + } + + Error GetAllDeployableItems(Array& items) override + { + return mStorage.GetAllDeployableItems(items); + } + + RetWithError GetDeployableItemsCount() override { return mStorage.GetDeployableItemsCount(); } + + Error AddItemBlob( + const String& itemID, const String& version, const common::imagemanager::ItemBlobInfo& blob) override + { + return mStorage.AddItemBlob(itemID, version, blob); + } + + Error GetItemBlobs( + const String& itemID, const String& version, Array& blobs) override + { + return mStorage.GetItemBlobs(itemID, version, blobs); + } + + RetWithError IsBlobUsed(const String& digest) override { return mStorage.IsBlobUsed(digest); } + + RetWithError IsLayerUsed(const String& diffID) override + { + auto items = std::make_unique>(); + + if (auto err = mStorage.GetAllDeployableItems(*items); !err.IsNone()) { + return {false, err}; + } + + for (const auto& item : *items) { + auto blobs = std::make_unique>(); + + if (auto err = mStorage.GetItemBlobs(item.mItemID, item.mVersion, *blobs); !err.IsNone()) { + return {false, err}; + } + + for (const auto& blob : *blobs) { + if (blob.mIsLayer && blob.mDiffID == diffID) { + return true; + } + } + } + + return false; + } + +private: + common::imagemanager::StorageStub mStorage; +}; + +} // namespace aos::sm::imagemanager + +#endif From 022994fbabf7e9b4c8744d762577ca0a0ba0baa6 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Tue, 3 Nov 2026 16:41:05 +0200 Subject: [PATCH 04/10] sm: use new image manager Use the common item info, item status and blob info providers in the launcher and smclient, and cancel installed items on launcher stop. Make ValidateBlob of the common image manager protected for the SM image manager. Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- src/core/sm/launcher/launcher.cpp | 20 +- src/core/sm/launcher/launcher.hpp | 17 +- src/core/sm/launcher/launcher.md | 8 +- src/core/sm/launcher/tests/launcher.cpp | 201 +++++++++++++----- .../launcher/tests/mocks/imagemanagermock.hpp | 6 +- src/core/sm/smclient/itf/smclient.hpp | 4 +- src/core/sm/smclient/smclient.md | 5 +- 7 files changed, 184 insertions(+), 77 deletions(-) diff --git a/src/core/sm/launcher/launcher.cpp b/src/core/sm/launcher/launcher.cpp index ec891c03a..e8e918221 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; @@ -116,6 +118,11 @@ Error Launcher::Stop() stopErr = AOS_ERROR_WRAP(err); } + // Don't wait for the items being installed by the launch in progress. + if (auto err = mImageManager->Cancel(); !err.IsNone() && stopErr.IsNone()) { + stopErr = AOS_ERROR_WRAP(err); + } + { UniqueLock lock {mMutex}; @@ -1396,7 +1403,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)); @@ -1417,7 +1424,8 @@ void Launcher::RemoveUpdateItems(const Array& removeItems) void Launcher::InstallUpdateItems(const Array& startInstances) { - 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); @@ -1431,13 +1439,13 @@ void Launcher::InstallUpdateItems(const Array& startInstances) return; } - 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)); } 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; diff --git a/src/core/sm/launcher/launcher.hpp b/src/core/sm/launcher/launcher.hpp index 6602be13e..789e2e7fe 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. @@ -241,10 +245,11 @@ class Launcher : public LauncherItf, 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..f4ec9fec4 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 diff --git a/src/core/sm/launcher/tests/launcher.cpp b/src/core/sm/launcher/tests/launcher.cpp index 4dde8006e..246df64e2 100644 --- a/src/core/sm/launcher/tests/launcher.cpp +++ b/src/core/sm/launcher/tests/launcher.cpp @@ -13,11 +13,12 @@ #include #include #include +#include +#include #include #include #include #include -#include #include #include #include @@ -215,20 +216,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 +243,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 +270,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 +348,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 +397,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 +434,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 +480,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 +547,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) { @@ -623,8 +625,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 +635,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,10 +667,10 @@ 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) + EXPECT_CALL(mImageManager, InstallItem) .WillRepeatedly(Invoke([&](const imagemanager::UpdateItemInfo& itemInfo) { auto it = std::find_if( cStartInstanceInfos.begin(), cStartInstanceInfos.end(), [&itemInfo](const InstanceInfo& info) { @@ -731,8 +733,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 +743,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 +798,91 @@ 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, ParallelUpdateInstancesDoesNotInterfere) { const std::vector cStartInstanceInfos = { @@ -805,8 +892,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 +938,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 +965,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 +1029,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 +1087,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 +1134,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 +1167,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 +1213,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 +1266,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 +1304,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"] { <> } From a3daa7161801fed508a1d23bf829c64f8b6f5af1 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Wed, 4 Nov 2026 10:29:28 +0200 Subject: [PATCH 05/10] sm: launcher: restart instance on integrity check failure Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- src/core/sm/launcher/launcher.cpp | 339 +++++++++-- src/core/sm/launcher/launcher.hpp | 30 +- src/core/sm/launcher/launcher.md | 87 ++- src/core/sm/launcher/tests/launcher.cpp | 770 +++++++++++++++++++++++- 4 files changed, 1139 insertions(+), 87 deletions(-) diff --git a/src/core/sm/launcher/launcher.cpp b/src/core/sm/launcher/launcher.cpp index e8e918221..a4989a07f 100644 --- a/src/core/sm/launcher/launcher.cpp +++ b/src/core/sm/launcher/launcher.cpp @@ -72,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); @@ -119,9 +120,7 @@ Error Launcher::Stop() } // Don't wait for the items being installed by the launch in progress. - if (auto err = mImageManager->Cancel(); !err.IsNone() && stopErr.IsNone()) { - stopErr = AOS_ERROR_WRAP(err); - } + CancelInstalls(); { UniqueLock lock {mMutex}; @@ -677,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; } @@ -743,6 +745,21 @@ void Launcher::UpdateInstancesImpl(Array& stopInstances, const Ar mFirstStart = false; }); + // 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)); @@ -772,16 +789,32 @@ void Launcher::UpdateInstancesImpl(Array& stopInstances, const Ar LOG_INF() << "[profiling] Install items begin" << Log::Field("removeCount", removeItems->Size()) << Log::Field("installCount", startInstances.Size()); + auto installItems = MakeUnique>(mAllocator); + if (!installItems) { + LOG_ERR() << "Failed to allocate install items" << Log::Field(ErrorEnum::eNoMemory); + + return; + } + 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)); @@ -946,11 +979,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; @@ -959,43 +994,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)); } } @@ -1006,6 +1064,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*) { @@ -1169,7 +1247,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()); @@ -1194,6 +1272,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)); } } @@ -1204,6 +1289,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); @@ -1274,6 +1462,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}; @@ -1422,25 +1659,20 @@ void Launcher::RemoveUpdateItems(const Array& removeItems) } } -void Launcher::InstallUpdateItems(const Array& startInstances) +Error Launcher::InstallUpdateItems(const Array& startInstances, Array& installItems) { // 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 = 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) { @@ -1451,23 +1683,31 @@ void Launcher::InstallUpdateItems(const Array& startInstances) 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)); @@ -1476,8 +1716,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) @@ -1512,7 +1754,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); } @@ -1577,10 +1819,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 789e2e7fe..60aa9e3c3 100644 --- a/src/core/sm/launcher/launcher.hpp +++ b/src/core/sm/launcher/launcher.hpp @@ -167,6 +167,7 @@ class Launcher : public LauncherItf, InstanceStatus mStatus; StaticString mInstanceID; Duration mOfflineTTL; + bool mConfigsLoaded {}; }; struct UpdateItemInfo { @@ -174,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; @@ -194,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( @@ -207,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( @@ -241,6 +255,8 @@ 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; diff --git a/src/core/sm/launcher/launcher.md b/src/core/sm/launcher/launcher.md index f4ec9fec4..1c5066197 100644 --- a/src/core/sm/launcher/launcher.md +++ b/src/core/sm/launcher/launcher.md @@ -127,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 246df64e2..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 @@ -141,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 /*********************************************************************************************************************** @@ -611,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 = { @@ -670,17 +845,16 @@ TEST_F(LauncherTest, UpdateInstances) EXPECT_CALL(mImageManager, RemoveItem(cStoredInfos[0].mItemID, cStoredInfos[0].mVersion)) .WillOnce(Return(ErrorEnum::eNone)); - 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_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); @@ -883,6 +1057,584 @@ TEST_F(LauncherTest, StopCancelsItemsBeingInstalled) 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 = { From adfee1532ae7a7494ef61525aef518083ace5fea Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Wed, 4 Nov 2026 15:17:32 +0200 Subject: [PATCH 06/10] common: tools: don't follow symlinks in CalculateSize CalculateSize and DirIterator used stat, so symlinks to directories were traversed, the targets of file symlinks were counted and dangling symlinks stopped the directory walk. Use lstat to count the links themselves and return errno on failure. Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- src/core/common/tools/fs.cpp | 27 +++++++++++++++++++-------- src/core/common/tools/tests/fs.cpp | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) 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"; From cb7c941224500650ba0bf7852e1b22425d268c2e Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Fri, 6 Nov 2026 10:46:19 +0200 Subject: [PATCH 07/10] sm: database: inherit image manager storage virtually DatabaseItf lists all storage interfaces the SM database implements. common::database::Database of aos_core_cpp implements the common image manager storage, which creates a diamond in sm::Database. Inherit StorageItf virtually to keep a single instance of it: StorageItf is a pure interface without data. This deviates from MISRA C++:2023 advisory rule 13.1.1. Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- sonar-project.properties | 9 +++++++-- src/core/sm/database/database.md | 12 +++++++++++- src/core/sm/database/itf/database.hpp | 6 +++++- 3 files changed, 23 insertions(+), 4 deletions(-) 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/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 { }; /** @}*/ From 5e9f3762a3d22f21e76a9d379476bb4c516b2e50 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Fri, 6 Nov 2026 14:56:54 +0200 Subject: [PATCH 08/10] sm: launcher: allocate update buffers before starting thread pool UpdateInstancesImpl returned on a failed allocation of the remove or install items after the launch thread pool was started, so the pool was left running and the next request started it again. Allocate all buffers before starting the pool. Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- src/core/sm/launcher/launcher.cpp | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/core/sm/launcher/launcher.cpp b/src/core/sm/launcher/launcher.cpp index a4989a07f..d9249373b 100644 --- a/src/core/sm/launcher/launcher.cpp +++ b/src/core/sm/launcher/launcher.cpp @@ -745,6 +745,21 @@ void Launcher::UpdateInstancesImpl(Array& stopInstances, const Ar mFirstStart = false; }); + // 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; + } + + auto installItems = MakeUnique>(mAllocator); + if (!installItems) { + LOG_ERR() << "Failed to allocate install items" << Log::Field(ErrorEnum::eNoMemory); + + return; + } + // 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) { @@ -770,13 +785,6 @@ void Launcher::UpdateInstancesImpl(Array& stopInstances, const Ar LOG_ERR() << "Failed to append instances with modified params to stop list" << Log::Field(AOS_ERROR_WRAP(err)); } - auto removeItems = MakeUnique>(mAllocator); - if (!removeItems) { - LOG_ERR() << "Failed to allocate remove update items" << Log::Field(ErrorEnum::eNoMemory); - - return; - } - if (!mFirstStart) { GetRemoveUpdateItems(stopInstances, startInstances, *removeItems); } @@ -789,13 +797,6 @@ void Launcher::UpdateInstancesImpl(Array& stopInstances, const Ar LOG_INF() << "[profiling] Install items begin" << Log::Field("removeCount", removeItems->Size()) << Log::Field("installCount", startInstances.Size()); - auto installItems = MakeUnique>(mAllocator); - if (!installItems) { - LOG_ERR() << "Failed to allocate install items" << Log::Field(ErrorEnum::eNoMemory); - - return; - } - RemoveUpdateItems(*removeItems); auto installError = InstallUpdateItems(startInstances, *installItems); From 92d6947fae677f87a2086a4258a3adf669dcdfa8 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Fri, 6 Nov 2026 18:06:18 +0200 Subject: [PATCH 09/10] common: spaceallocator: fit digests in outdated item ID CM image manager registers download files as outdated items using the blob digest as ID. A sha256 digest is 71 chars, longer than cIDLen, so assigning it to OutdatedItem::mID triggered the String assignment assert and aborted CM on start when a leftover download file was present. Size the outdated item ID to hold either an item ID or a digest and return an error instead of asserting when the ID or version doesn't fit. Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- .../common/spaceallocator/spaceallocator.hpp | 18 ++++++-- .../spaceallocator/tests/spaceallocator.cpp | 46 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) 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; From c76090442bbef7bceb22c8607abe759be4a1d0e6 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Fri, 6 Nov 2026 18:39:40 +0200 Subject: [PATCH 10/10] common: imagemanager: allow image config without diff IDs A component manifest may reference a regular image config without rootfs instead of the empty config. Installing such an item failed with "diff ID's size mismatch" as the check required a diff ID for every layer. Treat a config without diff IDs as the empty config: its layers are kept as packed blobs without diff IDs. Signed-off-by: Mykhailo Lohvynenko Reviewed-by: Oleksandr Grytsov --- src/core/common/imagemanager/imagemanager.cpp | 5 ++++- .../imagemanager/tests/imagemanager.cpp | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) 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);