diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index de258d29d..4e71c3f9b 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2287,6 +2287,12 @@ For privacy information about this product please visit https://aka.ms/privacy.< Inspect images. + + Import an image from a tarball. + + + Imports the contents of a tarball to create a filesystem image. Optionally tag the image with a repository and tag name. + List images. @@ -2529,6 +2535,9 @@ On first run, creates the file with all settings commented out at their defaults Image name + + File or - to read from stdin + Provides path to the tar archive file containing the image diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index a7bfe4e9b..d6456e644 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -61,6 +61,7 @@ _(Help, "help", WSLC_CLI_HELP_ARG, Kind::Flag, L _(Hostname, "hostname", L"h", Kind::Value, Localization::WSLCCLI_HostnameArgDescription()) \ _(ImageForce, "force", L"f", Kind::Flag, Localization::WSLCCLI_ImageForceArgDescription()) \ _(ImageId, "image", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ImageIdArgDescription()) \ +_(ImportFile, "file", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_ImportFileArgDescription()) \ _(Input, "input", L"i", Kind::Value, Localization::WSLCCLI_InputArgDescription()) \ _(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \ _(Label, "label", NO_ALIAS, Kind::Value, L"Volume metadata setting") \ diff --git a/src/windows/wslc/commands/ImageCommand.cpp b/src/windows/wslc/commands/ImageCommand.cpp index 5b2492745..99507f695 100644 --- a/src/windows/wslc/commands/ImageCommand.cpp +++ b/src/windows/wslc/commands/ImageCommand.cpp @@ -27,6 +27,7 @@ std::vector> ImageCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/commands/ImageCommand.h b/src/windows/wslc/commands/ImageCommand.h index 01476af13..604c22161 100644 --- a/src/windows/wslc/commands/ImageCommand.h +++ b/src/windows/wslc/commands/ImageCommand.h @@ -91,6 +91,22 @@ struct ImageLoadCommand final : public Command void ExecuteInternal(CLIExecutionContext& context) const override; }; +// Import Command +struct ImageImportCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"import"; + + ImageImportCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + // Remove Command struct ImageRemoveCommand final : public Command { diff --git a/src/windows/wslc/commands/ImageImportCommand.cpp b/src/windows/wslc/commands/ImageImportCommand.cpp new file mode 100644 index 000000000..cbd4b8b20 --- /dev/null +++ b/src/windows/wslc/commands/ImageImportCommand.cpp @@ -0,0 +1,52 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ImageImportCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "ImageCommand.h" +#include "CLIExecutionContext.h" +#include "ImageTasks.h" +#include "SessionTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Image Import Command +std::vector ImageImportCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::ImportFile, true), + Argument::Create(ArgType::ImageId), + Argument::Create(ArgType::Session), + }; +} + +std::wstring ImageImportCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ImageImportDesc(); +} + +std::wstring ImageImportCommand::LongDescription() const +{ + return Localization::WSLCCLI_ImageImportLongDesc(); +} + +void ImageImportCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context // + << CreateSession // + << ImportImage; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/RootCommand.cpp b/src/windows/wslc/commands/RootCommand.cpp index 5b28bb3f4..e78d0aed2 100644 --- a/src/windows/wslc/commands/RootCommand.cpp +++ b/src/windows/wslc/commands/RootCommand.cpp @@ -41,6 +41,7 @@ std::vector> RootCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName(), true)); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp index 578f2698c..60be8a77c 100644 --- a/src/windows/wslc/services/ImageService.cpp +++ b/src/windows/wslc/services/ImageService.cpp @@ -187,6 +187,31 @@ void ImageService::Load(wsl::windows::wslc::models::Session& session, const std: THROW_IF_FAILED(session.Get()->LoadImage(ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart)); } +void ImageService::Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName) +{ + HANDLE imageHandle = nullptr; + wil::unique_hfile imageFile; + ULONGLONG contentLength = 0; + + if (input == L"-") + { + imageHandle = GetStdHandle(STD_INPUT_HANDLE); + } + else + { + imageFile.reset(CreateFileW(input.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!imageFile); + + LARGE_INTEGER fileSize{}; + THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize)); + + imageHandle = imageFile.get(); + contentLength = fileSize.QuadPart; + } + + THROW_IF_FAILED(session.Get()->ImportImage(ToCOMInputHandle(imageHandle), imageName.c_str(), nullptr, contentLength)); +} + void ImageService::Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune) { WSLCDeleteImageOptions options{}; diff --git a/src/windows/wslc/services/ImageService.h b/src/windows/wslc/services/ImageService.h index 5f3d0697e..6f2f5e36f 100644 --- a/src/windows/wslc/services/ImageService.h +++ b/src/windows/wslc/services/ImageService.h @@ -34,6 +34,7 @@ class ImageService static std::vector List(wsl::windows::wslc::models::Session& session); static void Load(wsl::windows::wslc::models::Session& session, const std::wstring& input); + static void Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName); static void Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune); static wsl::windows::common::wslc_schema::InspectImage Inspect(wsl::windows::wslc::models::Session& session, const std::string& image); static void Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback); diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index bba509ee4..89f4c499e 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -187,6 +187,22 @@ void LoadImage(CLIExecutionContext& context) THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_ImageLoadNoInputError()); } +void ImportImage(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + WI_ASSERT(context.Args.Contains(ArgType::ImportFile)); + auto& session = context.Data.Get(); + + std::string imageName; + if (context.Args.Contains(ArgType::ImageId)) + { + imageName = WideToMultiByte(context.Args.Get()); + } + + auto& input = context.Args.Get(); + services::ImageService::Import(session, input, imageName); +} + void InspectImages(CLIExecutionContext& context) { WI_ASSERT(context.Data.Contains(Data::Session)); diff --git a/src/windows/wslc/tasks/ImageTasks.h b/src/windows/wslc/tasks/ImageTasks.h index b95953a0d..c56c41976 100644 --- a/src/windows/wslc/tasks/ImageTasks.h +++ b/src/windows/wslc/tasks/ImageTasks.h @@ -21,6 +21,7 @@ void BuildImage(CLIExecutionContext& context); void GetImages(CLIExecutionContext& context); void ListImages(CLIExecutionContext& context); void LoadImage(CLIExecutionContext& context); +void ImportImage(CLIExecutionContext& context); void PullImage(CLIExecutionContext& context); void PushImage(CLIExecutionContext& context); void DeleteImage(CLIExecutionContext& context); diff --git a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp index 3a59802a2..077707c31 100644 --- a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp @@ -534,6 +534,7 @@ class WSLCE2EGlobalTests {L"create", Localization::WSLCCLI_ContainerCreateDesc()}, {L"exec", Localization::WSLCCLI_ContainerExecDesc()}, {L"images", Localization::WSLCCLI_ImageListDesc()}, + {L"import", Localization::WSLCCLI_ImageImportDesc()}, {L"inspect", Localization::WSLCCLI_InspectDesc()}, {L"kill", Localization::WSLCCLI_ContainerKillDesc()}, {L"list", Localization::WSLCCLI_ContainerListDesc()}, diff --git a/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp new file mode 100644 index 000000000..d46e633bb --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp @@ -0,0 +1,152 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2EImageImportTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC image import. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2EImageImportTests +{ + WSLC_TEST_CLASS(WSLCE2EImageImportTests) + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureImageIsDeleted(DebianImage); + EnsureImageIsDeleted(ImportedImage); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + EnsureImageIsLoaded(DebianImage); + EnsureImageIsDeleted(ImportedImage); + SavedArchivePath = wsl::windows::common::filesystem::GetTempFilename(); + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + DeleteFileW(SavedArchivePath.c_str()); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_HelpCommand) + { + auto result = RunWslc(L"image import --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_MissingFile) + { + const auto result = RunWslc(L"image import"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'file'\r\n", .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_Success) + { + // Save image as a tarball + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag())); + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + // Import the tarball as a new image with a tag + auto importResult = RunWslc(std::format(L"image import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag())); + importResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Verify the imported image is listed + VerifyImageIsListed(ImportedImage.NameAndTag()); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_WithoutTag) + { + // Save image as a tarball + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag())); + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + // Import without specifying an image name + auto importResult = RunWslc(std::format(L"image import \"{}\"", SavedArchivePath.wstring())); + importResult.Verify({.Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_InvalidPath) + { + const auto result = + RunWslc(std::format(L"image import \"{}\" {}", L"C:\\nonexistent\\path\\image.tar", ImportedImage.NameAndTag())); + result.Verify({.ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Import_FromRoot) + { + // Save image as a tarball + auto saveResult = RunWslc(std::format(L"image save --output \"{}\" {}", SavedArchivePath.wstring(), DebianImage.NameAndTag())); + saveResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + // Import using the root 'import' alias + auto importResult = RunWslc(std::format(L"import \"{}\" {}", SavedArchivePath.wstring(), ImportedImage.NameAndTag())); + importResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Verify the imported image is listed + VerifyImageIsListed(ImportedImage.NameAndTag()); + } + +private: + const TestImage DebianImage = DebianTestImage(); + const TestImage ImportedImage{L"wslc-test-imported", L"latest", L""}; + + std::filesystem::path SavedArchivePath{}; + + std::wstring GetHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() // + << GetDescription() // + << GetUsage() // + << GetAvailableCommands() // + << GetAvailableOptions(); + return output.str(); + } + + std::wstring GetDescription() const + { + return Localization::WSLCCLI_ImageImportLongDesc() + L"\r\n\r\n"; + } + + std::wstring GetUsage() const + { + return L"Usage: wslc image import [] []\r\n\r\n"; + } + + std::wstring GetAvailableCommands() const + { + std::wstringstream commands; + commands << L"The following arguments are available:\r\n" // + << L" file " << Localization::WSLCCLI_ImportFileArgDescription() << L"\r\n" // + << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n" // + << L"\r\n"; + return commands.str(); + } + + std::wstring GetAvailableOptions() const + { + std::wstringstream options; + options << L"The following options are available:\r\n" // + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" // + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" // + << L"\r\n"; + return options.str(); + } +}; +} // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCE2EImageTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp index 7ade427bd..466839b58 100644 --- a/test/windows/wslc/e2e/WSLCE2EImageTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp @@ -72,6 +72,7 @@ class WSLCE2EImageTests {L"inspect", Localization::WSLCCLI_ImageInspectDesc()}, {L"list", Localization::WSLCCLI_ImageListDesc()}, {L"load", Localization::WSLCCLI_ImageLoadDesc()}, + {L"import", Localization::WSLCCLI_ImageImportDesc()}, {L"prune", Localization::WSLCCLI_ImagePruneDesc()}, {L"pull", Localization::WSLCCLI_ImagePullDesc()}, {L"push", Localization::WSLCCLI_ImagePushDesc()},