diff --git a/completions/bash/multipass b/completions/bash/multipass index 537641204b3..428c21bf7fe 100644 --- a/completions/bash/multipass +++ b/completions/bash/multipass @@ -268,7 +268,7 @@ _multipass_complete() _add_nonrepeating_args "--all --cancel --time" ;; "find") - _add_nonrepeating_args "--show-unsupported --force-update --format" + _add_nonrepeating_args "--show-unsupported --force-update --format --only-cached" ;; "unalias") _multipass_aliases diff --git a/docs/reference/command-line-interface/find.md b/docs/reference/command-line-interface/find.md index 2a2eb39130a..e31b9e6136b 100644 --- a/docs/reference/command-line-interface/find.md +++ b/docs/reference/command-line-interface/find.md @@ -33,6 +33,8 @@ The list of available images is updated periodically. The option `--force-update The option `--show-unsupported` includes old Ubuntu images, which were available at some point but are not supported anymore. This means that some features of Multipass might now work on these images and no user support is given. However, they are still available for testing. +The option `--only-cached` limits output to images that have already been downloaded and are stored locally. This is useful when you want to launch an instance without any network access, or when you want to know which images are ready for immediate use. + The command also supports searching through available images. For example, `multipass find mantic` returns: ```{code-block} text @@ -59,6 +61,7 @@ Options: --format Output list in the requested format. Valid formats are: table (default), json, csv and yaml --force-update Force the image information to update from the network + --only-cached Show only locally cached images Arguments: string An optional value to search for in [] diff --git a/include/multipass/vm_image_vault.h b/include/multipass/vm_image_vault.h index 19baecaeccb..fba18d7354b 100644 --- a/include/multipass/vm_image_vault.h +++ b/include/multipass/vm_image_vault.h @@ -87,6 +87,7 @@ class VMImageVault : private DisabledCopyMove virtual VMImageHost* image_host_for(const std::string& remote_name) const = 0; virtual std::vector> all_info_for( const Query& query) const = 0; + virtual std::vector> cached_images() const = 0; protected: VMImageVault() = default; diff --git a/src/client/cli/cmd/find.cpp b/src/client/cli/cmd/find.cpp index c25c53cad51..9dcb6e97cee 100644 --- a/src/client/cli/cmd/find.cpp +++ b/src/client/cli/cmd/find.cpp @@ -82,7 +82,9 @@ mp::ParseCode cmd::Find::parse_args(mp::ArgParser* parser) const QCommandLineOption force_manifest_network_download( "force-update", "Force the image information to update from the network"); - parser->addOptions({unsupportedOption, formatOption, force_manifest_network_download}); + QCommandLineOption onlyCachedOption("only-cached", "Show only locally cached images"); + parser->addOptions( + {unsupportedOption, formatOption, force_manifest_network_download, onlyCachedOption}); auto status = parser->commandParse(this); @@ -91,6 +93,12 @@ mp::ParseCode cmd::Find::parse_args(mp::ArgParser* parser) return status; } + if (parser->isSet(onlyCachedOption) && parser->positionalArguments().count() > 0) + { + cerr << "Cannot use --only-cached with a search string\n"; + return ParseCode::CommandLineError; + } + if (parser->positionalArguments().count() > 1) { cerr << "Wrong number of arguments\n"; @@ -119,6 +127,7 @@ mp::ParseCode cmd::Find::parse_args(mp::ArgParser* parser) request.set_allow_unsupported(parser->isSet(unsupportedOption)); request.set_force_manifest_network_download(parser->isSet(force_manifest_network_download)); + request.set_only_cached(parser->isSet(onlyCachedOption)); status = handle_format_option(parser, &chosen_formatter, cerr); diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp index bb55e48d11c..39f3b39fd6d 100644 --- a/src/daemon/daemon.cpp +++ b/src/daemon/daemon.cpp @@ -1598,6 +1598,32 @@ try server}; FindReply response; + if (request->only_cached()) + { + auto cached = config->vault->cached_images(); + for (const auto& [id, image] : cached) + { + auto entry = response.add_images_info(); + entry->set_os(image.os); + entry->set_release(image.original_release); + entry->set_version(image.release_date); + + if (!image.aliases.empty()) + { + for (const auto& alias : image.aliases) + entry->add_aliases(alias); + } + else + { + entry->add_aliases(id.substr(0, 12)); + } + } + + server->Write(response); + status_promise->set_value(grpc::Status::OK); + return; + } + if (!request->search_string().empty()) { if (!request->remote_name().empty()) diff --git a/src/daemon/default_vm_image_vault.cpp b/src/daemon/default_vm_image_vault.cpp index 038f02b8bc5..1e0c0e5a82b 100644 --- a/src/daemon/default_vm_image_vault.cpp +++ b/src/daemon/default_vm_image_vault.cpp @@ -816,3 +816,15 @@ void mp::DefaultVMImageVault::amend_db() } } } + +std::vector> mp::DefaultVMImageVault::cached_images() const +{ + std::vector> images; + + for (const auto& record : prepared_image_records) + { + images.emplace_back(record.first, record.second.image); + } + + return images; +} diff --git a/src/daemon/default_vm_image_vault.h b/src/daemon/default_vm_image_vault.h index 796a90bc6e3..8dc68dd01c5 100644 --- a/src/daemon/default_vm_image_vault.h +++ b/src/daemon/default_vm_image_vault.h @@ -66,6 +66,7 @@ class DefaultVMImageVault final : public BaseVMImageVault MemorySize minimum_image_size_for(const std::string& id) override; void clone(const std::string& source_instance_name, const std::string& destination_instance_name) override; + std::vector> cached_images() const override; private: VMImage image_instance_from(const VMImage& prepared_image, const Path& dest_dir); diff --git a/src/rpc/multipass.proto b/src/rpc/multipass.proto index 529c4d1a15c..619db6d7ea7 100644 --- a/src/rpc/multipass.proto +++ b/src/rpc/multipass.proto @@ -141,6 +141,7 @@ message FindRequest { int32 verbosity_level = 3; bool allow_unsupported = 4; bool force_manifest_network_download = 5; + bool only_cached = 6; } message FindReply { diff --git a/tests/unit/mock_vm_image_vault.h b/tests/unit/mock_vm_image_vault.h index fa96e84abba..7509a5c5876 100644 --- a/tests/unit/mock_vm_image_vault.h +++ b/tests/unit/mock_vm_image_vault.h @@ -81,6 +81,10 @@ class MockVMImageVault : public VMImageVault all_info_for, (const Query&), (const, override)); + MOCK_METHOD((std::vector>), + cached_images, + (), + (const, override)); private: TempFile dummy_image; diff --git a/tests/unit/stub_vm_image_vault.h b/tests/unit/stub_vm_image_vault.h index d78f634ad53..35726c7248d 100644 --- a/tests/unit/stub_vm_image_vault.h +++ b/tests/unit/stub_vm_image_vault.h @@ -39,16 +39,16 @@ struct StubVMImageVault final : public multipass::VMImageVault return prepare({dummy_image.name(), {}, {}, {}, {}, {}, {}}); }; - void remove(const std::string&) override{}; + void remove(const std::string&) override {}; bool has_record_for(const std::string&) override { return false; } - void prune_expired_images() override{}; + void prune_expired_images() override {}; void update_images(const FetchType& fetch_type, const PrepareAction& prepare, - const ProgressMonitor& monitor) override{}; + const ProgressMonitor& monitor) override {}; MemorySize minimum_image_size_for(const std::string& image) override { @@ -83,6 +83,11 @@ struct StubVMImageVault final : public multipass::VMImageVault { } + std::vector> cached_images() const override + { + return {}; + } + TempFile dummy_image; }; } // namespace test diff --git a/tests/unit/test_daemon_find.cpp b/tests/unit/test_daemon_find.cpp index 8712bbd831b..4a6e5fb2583 100644 --- a/tests/unit/test_daemon_find.cpp +++ b/tests/unit/test_daemon_find.cpp @@ -298,3 +298,44 @@ TEST_F(DaemonFind, findForceUpdateRemoteSearchNameCheckUpdateManifestsCalls) send_command({"find", "release:22.04", "--force-update"}); } + +TEST_F(DaemonFind, onlyCachedReturnsCachedImages) +{ + auto mock_image_vault = std::make_unique>(); + + mp::VMImage cached_image; + cached_image.id = "abc123def456"; + cached_image.original_release = "22.04 LTS"; + cached_image.release_date = "20240101"; + cached_image.os = "Ubuntu"; + cached_image.aliases = {"jammy"}; + + EXPECT_CALL(*mock_image_vault, cached_images()) + .WillOnce(Return( + std::vector>{{"abc123def456full", cached_image}})); + + config_builder.vault = std::move(mock_image_vault); + mp::Daemon daemon{config_builder.build()}; + + std::stringstream stream; + send_command({"find", "--only-cached"}, stream); + + EXPECT_THAT(stream.str(), + AllOf(HasSubstr("jammy"), HasSubstr("22.04 LTS"), HasSubstr("Ubuntu"))); +} + +TEST_F(DaemonFind, onlyCachedEmptyReturnsNoImages) +{ + auto mock_image_vault = std::make_unique>(); + + EXPECT_CALL(*mock_image_vault, cached_images()) + .WillOnce(Return(std::vector>{})); + + config_builder.vault = std::move(mock_image_vault); + mp::Daemon daemon{config_builder.build()}; + + std::stringstream stream; + send_command({"find", "--only-cached"}, stream); + + EXPECT_THAT(stream.str(), HasSubstr("No images found.")); +}