Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2287,6 +2287,12 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<data name="WSLCCLI_ImageInspectLongDesc" xml:space="preserve">
<value>Inspect images.</value>
</data>
<data name="WSLCCLI_ImageImportDesc" xml:space="preserve">
<value>Import an image from a tarball.</value>
</data>
<data name="WSLCCLI_ImageImportLongDesc" xml:space="preserve">
<value>Imports the contents of a tarball to create a filesystem image. Optionally tag the image with a repository and tag name.</value>
</data>
<data name="WSLCCLI_ImageListDesc" xml:space="preserve">
<value>List images.</value>
</data>
Expand Down Expand Up @@ -2529,6 +2535,9 @@ On first run, creates the file with all settings commented out at their defaults
<data name="WSLCCLI_ImageIdArgDescription" xml:space="preserve">
<value>Image name</value>
</data>
<data name="WSLCCLI_ImportFileArgDescription" xml:space="preserve">
<value>File or - to read from stdin</value>
</data>
<data name="WSLCCLI_InputArgDescription" xml:space="preserve">
<value>Provides path to the tar archive file containing the image</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/arguments/ArgumentDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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") \
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ImageCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ std::vector<std::unique_ptr<Command>> ImageCommand::GetCommands() const
commands.push_back(std::make_unique<ImageInspectCommand>(FullName()));
commands.push_back(std::make_unique<ImageListCommand>(FullName()));
commands.push_back(std::make_unique<ImageLoadCommand>(FullName()));
commands.push_back(std::make_unique<ImageImportCommand>(FullName()));
commands.push_back(std::make_unique<ImagePruneCommand>(FullName()));
commands.push_back(std::make_unique<ImagePullCommand>(FullName()));
commands.push_back(std::make_unique<ImagePushCommand>(FullName()));
Expand Down
16 changes: 16 additions & 0 deletions src/windows/wslc/commands/ImageCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Argument> 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
{
Expand Down
52 changes: 52 additions & 0 deletions src/windows/wslc/commands/ImageImportCommand.cpp
Original file line number Diff line number Diff line change
@@ -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<Argument> ImageImportCommand::GetArguments() const
{
return {
Argument::Create(ArgType::ImportFile, true),
Argument::Create(ArgType::ImageId),
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image positional argument is currently optional, but the underlying import API requires an image name that includes a tag (repo:tag). As-is, wslc image import <file> will parse and execute but fail later in the service layer. Consider making ArgType::ImageId required for this command (and updating help/usage/tests accordingly), or add CLI-side validation/defaulting so the command cannot reach execution with an invalid/missing image reference.

Suggested change
Argument::Create(ArgType::ImageId),
Argument::Create(ArgType::ImageId, true),

Copilot uses AI. Check for mistakes.
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
1 change: 1 addition & 0 deletions src/windows/wslc/commands/RootCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ std::vector<std::unique_ptr<Command>> RootCommand::GetCommands() const
commands.push_back(std::make_unique<ContainerCreateCommand>(FullName()));
commands.push_back(std::make_unique<ContainerExecCommand>(FullName()));
commands.push_back(std::make_unique<ImageListCommand>(FullName(), true));
commands.push_back(std::make_unique<ImageImportCommand>(FullName()));
commands.push_back(std::make_unique<InspectCommand>(FullName()));
commands.push_back(std::make_unique<ContainerKillCommand>(FullName()));
commands.push_back(std::make_unique<ContainerListCommand>(FullName()));
Expand Down
25 changes: 25 additions & 0 deletions src/windows/wslc/services/ImageService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Comment on lines +196 to +213
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When input == L"-", contentLength is left as 0 and the handle is passed directly into ToCOMInputHandle(). For redirected stdin from a pipe/console, ToCOMInputHandle() will throw (FILE_TYPE_CHAR) or the Docker request will be sent with Content-Length: 0, which makes stdin-based imports fail or behave incorrectly. Compute the real size when stdin is a disk file (GetFileSizeEx), and for unsupported stdin types (console/pipe) either buffer to a temp file to determine length or fail fast with a clear user-facing error message.

Copilot uses AI. Check for mistakes.

void ImageService::Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune)
{
WSLCDeleteImageOptions options{};
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/services/ImageService.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class ImageService

static std::vector<wsl::windows::wslc::models::ImageInformation> 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);
Expand Down
16 changes: 16 additions & 0 deletions src/windows/wslc/tasks/ImageTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Data::Session>();

std::string imageName;
if (context.Args.Contains(ArgType::ImageId))
{
imageName = WideToMultiByte(context.Args.Get<ArgType::ImageId>());
}

Comment on lines +196 to +201
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imageName defaults to an empty string when the image argument is omitted, but the underlying session API rejects imports without an explicit repo:tag (it throws if no tag is present). This means wslc image import <file> will consistently fail at runtime. Either require ArgType::ImageId for this command (and/or default missing tags to :latest), or add explicit validation here that produces a user-facing error before calling into the service.

Suggested change
std::string imageName;
if (context.Args.Contains(ArgType::ImageId))
{
imageName = WideToMultiByte(context.Args.Get<ArgType::ImageId>());
}
THROW_HR_IF_MSG(E_INVALIDARG, !context.Args.Contains(ArgType::ImageId), "The image name must include a repository and tag.");
auto imageName = WideToMultiByte(context.Args.Get<ArgType::ImageId>());
const auto tagSeparator = imageName.rfind(':');
THROW_HR_IF_MSG(
E_INVALIDARG,
(tagSeparator == std::string::npos) || (tagSeparator == 0) || (tagSeparator == imageName.size() - 1),
"The image name must include a repository and tag.");

Copilot uses AI. Check for mistakes.
auto& input = context.Args.Get<ArgType::ImportFile>();
services::ImageService::Import(session, input, imageName);
}

void InspectImages(CLIExecutionContext& context)
{
WI_ASSERT(context.Data.Contains(Data::Session));
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/tasks/ImageTasks.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()},
Expand Down
152 changes: 152 additions & 0 deletions test/windows/wslc/e2e/WSLCE2EImageImportTests.cpp
Original file line number Diff line number Diff line change
@@ -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});
Comment on lines +79 to +81
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image import without an image/tag is asserted to succeed here, but the session import API currently requires a repo:tag and throws when the tag is missing. Unless the implementation is updated to support untagged imports, this test will fail and should either supply a valid image reference (with tag) or assert the expected failure behavior/message.

Suggested change
// Import without specifying an image name
auto importResult = RunWslc(std::format(L"image import \"{}\"", SavedArchivePath.wstring()));
importResult.Verify({.Stderr = L"", .ExitCode = 0});
// Import without specifying an image name or tag should fail because the current import API requires a repo:tag.
auto importResult = RunWslc(std::format(L"image import \"{}\"", SavedArchivePath.wstring()));
importResult.Verify({.ExitCode = 1});

Copilot uses AI. Check for mistakes.
}

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 [<options>] <file> [<image>]\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
1 change: 1 addition & 0 deletions test/windows/wslc/e2e/WSLCE2EImageTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()},
Expand Down
Loading