@samitouri / QOSAMI-WSL / commits / 83d8aa17

CLI: Add --pull option to container create and run (#41304)

David Bennett committed Aug 10, 2026 at 15:40 UTC 83d8aa178d082358005aa64f49cdfc34a1ea326c
17 files changed +169 -12
localization/strings/en-US/Resources.resw
+5 -1
@@ -3228,7 +3228,7 @@ On first run, creates the file with all settings commented out at their defaults
3228 <comment>{Locked="none"}{Locked="ansi"}Command line arguments should not be translated</comment>
3229 </data>
3230 <data name="WSLCCLI_PullArgDescription" xml:space="preserve">
3231 - <value>Image pull policy (always|missing|never) (default:never)</value>
3231 + <value>Image pull policy (always|missing|never) (default: missing)</value>
3232 <comment>{Locked="always"}{Locked="missing"}{Locked="never"}Command line arguments should not be translated</comment>
3233 </data>
3234 <data name="WSLCCLI_SchemeArgDescription" xml:space="preserve">
@@ -3263,6 +3263,10 @@ On first run, creates the file with all settings commented out at their defaults
3263 <value>Invalid {} value: {} is not a recognized format type. Supported format types are: {}.</value>
3264 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3265 </data>
3266 + <data name="WSLCCLI_InvalidPullPolicyError" xml:space="preserve">
3267 + <value>Invalid {} value: {} is not a recognized pull policy. Supported pull policies are: {}.</value>
3268 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3269 + </data>
3270 <data name="WSLCCLI_InvalidInspectError" xml:space="preserve">
3271 <value>Invalid {} value: {} is not a recognized inspect type. Supported inspect types are: {}.</value>
3272 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/wslc/arguments/ArgumentConvertedTypes.h
+1
@@ -38,6 +38,7 @@ namespace wsl::windows::wslc::argument::details {
38 using FormatType = wsl::windows::wslc::models::FormatType;
39 using InspectType = wsl::windows::wslc::models::InspectType;
40 using JsonIndent = int;
41 +using PullPolicy = wsl::windows::wslc::models::PullPolicy;
42 using WSLCSignal = ::WSLCSignal;
43 using UlimitValue = std::tuple<std::string, int64_t, int64_t>;
44 using KeyValuePair = std::pair<std::string, std::string>;
src/windows/wslc/arguments/ArgumentDefinitions.h
+1 -1
@@ -112,7 +112,7 @@ _(Path, "path", NO_ALIAS, Kind::Positional,
112 /*_(Progress, "progress", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_ProgressArgDescription())*/ \
113 _(Publish, "publish", L"p", Kind::Value, NoConversion, Localization::WSLCCLI_PublishArgDescription()) \
114 _(PublishAll, "publish-all", L"P", Kind::Flag, NoConversion, Localization::WSLCCLI_PublishAllArgDescription()) \
115 -/*_(Pull, "pull", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_PullArgDescription())*/ \
115 +_(Pull, "pull", NO_ALIAS, Kind::Value, PullPolicy, Localization::WSLCCLI_PullArgDescription()) \
116 _(Quiet, "quiet", L"q", Kind::Flag, NoConversion, Localization::WSLCCLI_QuietArgDescription()) \
117 _(Remove, "rm", NO_ALIAS, Kind::Flag, NoConversion, Localization::WSLCCLI_RemoveArgDescription()) \
118 /*_(Scheme, "scheme", NO_ALIAS, Kind::Value, NoConversion, Localization::WSLCCLI_SchemeArgDescription())*/ \
src/windows/wslc/arguments/ArgumentValidation.cpp
+4
@@ -94,6 +94,10 @@ void Argument::Validate(ArgMap& execArgs) const
94 CacheConverted<ArgType::InspectFormat>(execArgs, m_name, validation::GetInspectJsonIndentFromString);
95 break;
96
97 + case ArgType::Pull:
98 + CacheConverted<ArgType::Pull>(execArgs, m_name, validation::GetPullPolicyFromString);
99 + break;
100 +
101 case ArgType::Signal:
102 CacheConverted<ArgType::Signal>(execArgs, m_name, validation::GetWSLCSignalFromString);
103 break;
src/windows/wslc/arguments/SpecParsing.cpp
+30
@@ -652,6 +652,36 @@ int GetInspectJsonIndentFromString(const std::wstring& input, const std::wstring
652 return wsl::shared::c_jsonCompactIndent;
653 }
654
655 +models::PullPolicy GetPullPolicyFromString(const std::wstring& input, const std::wstring& argName)
656 +{
657 + static constexpr std::pair<std::wstring_view, models::PullPolicy> c_pullPolicies[] = {
658 + {L"always", models::PullPolicy::Always},
659 + {L"missing", models::PullPolicy::Missing},
660 + {L"never", models::PullPolicy::Never},
661 + };
662 +
663 + for (const auto& [name, policy] : c_pullPolicies)
664 + {
665 + if (IsEqual(input, name))
666 + {
667 + return policy;
668 + }
669 + }
670 +
671 + std::wstring supportedValues;
672 + for (const auto& pullPolicy : c_pullPolicies)
673 + {
674 + if (!supportedValues.empty())
675 + {
676 + supportedValues += L", ";
677 + }
678 +
679 + supportedValues += pullPolicy.first;
680 + }
681 +
682 + throw ArgumentException(Localization::WSLCCLI_InvalidPullPolicyError(argName, input, supportedValues));
683 +}
684 +
685 models::InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName)
686 {
687 if (IsEqual(input, L"image"))
src/windows/wslc/arguments/SpecParsing.h
+3
@@ -84,6 +84,9 @@ models::FormatType GetFormatTypeFromString(const std::wstring& input, const std:
84 // Parses the inspect family's sole supported format ("json") into its compact json::dump() indent.
85 int GetInspectJsonIndentFromString(const std::wstring& input, const std::wstring& argName = {});
86
87 +// Parses an image pull policy ("always"/"missing"/"never").
88 +models::PullPolicy GetPullPolicyFromString(const std::wstring& input, const std::wstring& argName = {});
89 +
90 // Parses an inspect target ("image"/"container"/"network"/"volume") into an InspectType.
91 models::InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName);
92
src/windows/wslc/commands/ContainerCreateCommand.cpp
+1
@@ -60,6 +60,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
60 Argument::Create(ArgType::NoHealthcheck),
61 Argument::Create(ArgType::Publish, false, Limit::Unlimited),
62 Argument::Create(ArgType::PublishAll),
63 + Argument::Create(ArgType::Pull),
64 Argument::Create(ArgType::Remove),
65 // Argument::Create(ArgType::Scheme),
66 Argument::Create(ArgType::ShmSize),
src/windows/wslc/commands/ContainerRunCommand.cpp
+1 -1
@@ -60,7 +60,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
60 Argument::Create(ArgType::NoHealthcheck),
61 Argument::Create(ArgType::Publish, false, Limit::Unlimited),
62 Argument::Create(ArgType::PublishAll),
63 - // Argument::Create(ArgType::Pull),
63 + Argument::Create(ArgType::Pull),
64 Argument::Create(ArgType::Remove),
65 // Argument::Create(ArgType::Scheme),
66 Argument::Create(ArgType::ShmSize),
src/windows/wslc/services/ContainerModel.h
+8
@@ -27,6 +27,13 @@ enum class FormatType
27 Json,
28 };
29
30 +enum class PullPolicy
31 +{
32 + Missing,
33 + Always,
34 + Never,
35 +};
36 +
37 struct ContainerOptions
38 {
39 std::vector<std::string> Arguments;
@@ -65,6 +72,7 @@ struct ContainerOptions
72 std::optional<int64_t> MemoryBytes{};
73 std::optional<int64_t> NanoCpus{};
74 std::vector<std::tuple<std::string, int64_t, int64_t>> Ulimits;
75 + PullPolicy Pull = PullPolicy::Missing;
76 };
77
78 struct CreateContainerResult
src/windows/wslc/services/ContainerService.cpp
+15 -9
@@ -41,10 +41,22 @@ static void SetContainerArguments(WSLCProcessOptions& options, std::vector<const
41 options.CommandLine = {.Values = argsStorage.data(), .Count = static_cast<ULONG>(argsStorage.size())};
42 }
43
44 +static void PullImage(Terminal& terminal, Session& session, const std::string& image)
45 +{
46 + ImageProgressCallback callback(terminal, Terminal::Level::Info);
47 + ImageService imageService;
48 + imageService.Pull(terminal, session, image, &callback);
49 +}
50 +
51 static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& terminal, Session& session, const std::string& image, const ContainerOptions& options)
52 {
53 WarningCallback warningCallback(terminal);
54
55 + if (options.Pull == PullPolicy::Always)
56 + {
57 + PullImage(terminal, session, image);
58 + }
59 +
60 auto processFlags = WSLCProcessFlagsNone;
61 WI_SetFlagIf(processFlags, WSLCProcessFlagsStdin, options.Interactive);
62 WI_SetFlagIf(processFlags, WSLCProcessFlagsTty, options.TTY);
@@ -245,16 +257,10 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& termi
257 }
258
259 auto [result, runningContainer] = containerLauncher.CreateNoThrow(*session.Get(), &warningCallback);
248 - if (result == WSLC_E_IMAGE_NOT_FOUND)
260 + if (result == WSLC_E_IMAGE_NOT_FOUND && options.Pull == PullPolicy::Missing)
261 {
250 - {
251 - // Implicit pull for run/create: progress goes to Info (stderr), keeping stdout for the
252 - // container id/output.
253 - ImageProgressCallback callback(terminal, Terminal::Level::Info);
254 - terminal.Info(L"{}\n", Localization::WSLCCLI_ImageNotFoundPulling(wsl::shared::string::MultiByteToWide(image)));
255 - ImageService imageService;
256 - imageService.Pull(terminal, session, image, &callback);
257 - }
262 + terminal.Info(L"{}\n", Localization::WSLCCLI_ImageNotFoundPulling(wsl::shared::string::MultiByteToWide(image)));
263 + PullImage(terminal, session, image);
264 return containerLauncher.Create(*session.Get(), &warningCallback);
265 }
266
src/windows/wslc/tasks/ContainerTasks.cpp
+5
@@ -642,6 +642,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
642 {
643 ContainerOptions options;
644
645 + if (context.Args.Contains(ArgType::Pull))
646 + {
647 + options.Pull = context.Args.GetValue<ArgType::Pull>();
648 + }
649 +
650 if (context.Args.Contains(ArgType::CIDFile))
651 {
652 options.CidFile = context.Args.GetValue<ArgType::CIDFile>();
test/windows/wslc/CommandLineTestCases.h
+8
@@ -92,6 +92,10 @@ COMMAND_LINE_TEST_CASE(
92 true)
93 COMMAND_LINE_TEST_CASE(L"container run ubuntu bash -c 'echo Hello World'", L"run", true)
94 COMMAND_LINE_TEST_CASE(L"container run ubuntu", L"run", true)
95 +COMMAND_LINE_TEST_CASE(L"container run --pull=always ubuntu", L"run", true)
96 +COMMAND_LINE_TEST_CASE(L"container run --pull missing ubuntu", L"run", true)
97 +COMMAND_LINE_TEST_CASE(L"container run --pull never ubuntu", L"run", true)
98 +COMMAND_LINE_TEST_CASE(L"container run --pull invalid ubuntu", L"run", false)
99 COMMAND_LINE_TEST_CASE(L"container run --cidfile C:\\temp\\cidfile ubuntu", L"run", true)
100 COMMAND_LINE_TEST_CASE(L"container run -it --name foo ubuntu", L"run", true)
101 COMMAND_LINE_TEST_CASE(L"container run --rm -it --name foo ubuntu", L"run", true)
@@ -107,6 +111,10 @@ COMMAND_LINE_TEST_CASE(L"container start --attach cont", L"start", true)
111 COMMAND_LINE_TEST_CASE(L"container start -a cont", L"start", true)
112 COMMAND_LINE_TEST_CASE(L"create ubuntu:latest", L"create", true)
113 COMMAND_LINE_TEST_CASE(L"container create --name foo ubuntu", L"create", true)
114 +COMMAND_LINE_TEST_CASE(L"container create --pull=always ubuntu", L"create", true)
115 +COMMAND_LINE_TEST_CASE(L"container create --pull missing ubuntu", L"create", true)
116 +COMMAND_LINE_TEST_CASE(L"container create --pull never ubuntu", L"create", true)
117 +COMMAND_LINE_TEST_CASE(L"container create --pull invalid ubuntu", L"create", false)
118 COMMAND_LINE_TEST_CASE(L"container create --cidfile C:\\temp\\cidfile --name foo ubuntu", L"create", true)
119 COMMAND_LINE_TEST_CASE(L"create --workdir /app ubuntu", L"create", true)
120 COMMAND_LINE_TEST_CASE(L"create -w /app ubuntu", L"create", true)
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+14
@@ -153,6 +153,15 @@ class WSLCCLIArgumentUnitTests
153 VERIFY_NO_THROW(validation::ValidateFormatTypeFromString({L"json", L"table"}, L"formatArg"));
154 VERIFY_THROWS(validation::ValidateFormatTypeFromString({L"JSON", L"TABLE", L"csv"}, L"formatArg"), ArgumentException);
155
156 + // Verify image pull policy
157 + auto pullPolicy = validation::GetPullPolicyFromString(L"always");
158 + VERIFY_ARE_EQUAL(pullPolicy, PullPolicy::Always);
159 + pullPolicy = validation::GetPullPolicyFromString(L"missing");
160 + VERIFY_ARE_EQUAL(pullPolicy, PullPolicy::Missing);
161 + pullPolicy = validation::GetPullPolicyFromString(L"never");
162 + VERIFY_ARE_EQUAL(pullPolicy, PullPolicy::Never);
163 + VERIFY_THROWS(validation::GetPullPolicyFromString(L"invalid"), ArgumentException);
164 +
165 // Verify GPU device argument
166 VERIFY_NO_THROW(validation::ValidateGpus({L"all"}, L"gpusArg"));
167 VERIFY_THROWS(validation::ValidateGpus({L"none"}, L"gpusArg"), ArgumentException);
@@ -339,6 +348,11 @@ class WSLCCLIArgumentUnitTests
348 // string -> json::dump() indentation
349 VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::InspectFormat>(L"json"), wsl::shared::c_jsonCompactIndent);
350
351 + // string -> PullPolicy
352 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Pull>(L"missing"), PullPolicy::Missing);
353 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Pull>(L"always"), PullPolicy::Always);
354 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Pull>(L"never"), PullPolicy::Never);
355 +
356 // string -> WSLCSignal (Signal and StopSignal share the converter)
357 VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Signal>(L"SIGTERM"), WSLCSignalSIGTERM);
358 VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::StopSignal>(L"SIGKILL"), WSLCSignalSIGKILL);
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+33
@@ -32,6 +32,7 @@ class WSLCE2EContainerCreateTests
32 {
33 EnsureImageIsLoaded(AlpineImage);
34 EnsureImageIsLoaded(DebianImage);
35 + EnsureImageIsLoaded(HelloWorldImage);
36
37 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), HostEnvVariableValue.c_str()));
38 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName2.c_str(), HostEnvVariableValue2.c_str()));
@@ -44,6 +45,7 @@ class WSLCE2EContainerCreateTests
45 EnsureContainerDoesNotExist(WslcContainerName);
46 EnsureImageIsDeleted(AlpineImage);
47 EnsureImageIsDeleted(DebianImage);
48 + EnsureImageIsDeleted(HelloWorldImage);
49 EnsureNetworkDoesNotExist(TestNetworkName);
50
51 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), nullptr));
@@ -102,6 +104,36 @@ class WSLCE2EContainerCreateTests
104 result.Verify({.Stderr = expectedError.str(), .ExitCode = 1});
105 }
106
107 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_PullPolicy)
108 + {
109 + auto session = OpenDefaultElevatedSession();
110 + auto [registryContainer, registryAddress] = StartLocalRegistry(*session);
111 + auto registryImage = TagImageForRegistry(HelloWorldImage.NameAndTag(), string::MultiByteToWide(registryAddress));
112 + auto cleanup = wil::scope_exit([&]() {
113 + EnsureContainerDoesNotExist(WslcContainerName);
114 + RunWslc(std::format(L"image delete --force {}", registryImage));
115 + });
116 +
117 + auto result = RunWslc(std::format(L"container create --pull=never --name {} {}", WslcContainerName, registryImage));
118 + result.Verify({.Stderr = L"", .ExitCode = 0});
119 + VerifyContainerIsListed(WslcContainerName, L"created");
120 + EnsureContainerDoesNotExist(WslcContainerName);
121 +
122 + result = RunWslc(std::format(L"container create --pull=always --name {} {}", WslcContainerName, registryImage));
123 + const auto errorMessage = std::format(
124 + L"manifest for {} not found: manifest unknown: manifest unknown\r\nError code: WSLC_E_IMAGE_NOT_FOUND\r\n", registryImage);
125 + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
126 + VerifyContainerIsNotListed(WslcContainerName);
127 +
128 + RunWslcAndVerify(std::format(L"push {}", registryImage), {.Stderr = L"", .ExitCode = 0});
129 + RunWslcAndVerify(std::format(L"image delete --force {}", registryImage), {.ExitCode = 0});
130 +
131 + result = RunWslc(std::format(L"container create --pull=missing --name {} {}", WslcContainerName, registryImage));
132 + result.Verify({.ExitCode = 0});
133 + VerifyContainerIsListed(WslcContainerName, L"created");
134 + EnsureContainerDoesNotExist(WslcContainerName);
135 + }
136 +
137 WSLC_TEST_METHOD(WSLCE2E_Container_Create_Valid)
138 {
139 VerifyContainerIsNotListed(WslcContainerName);
@@ -1518,6 +1550,7 @@ private:
1550 // Test images
1551 const TestImage& AlpineImage = AlpineTestImage();
1552 const TestImage& DebianImage = DebianTestImage();
1553 + const TestImage& HelloWorldImage = HelloWorldTestImage();
1554 const TestImage& PythonImage = PythonTestImage();
1555 const TestImage& InvalidImage = InvalidTestImage();
1556
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+33
@@ -25,6 +25,7 @@ class WSLCE2EContainerRunTests
25 TEST_CLASS_SETUP(ClassSetup)
26 {
27 EnsureImageIsLoaded(DebianImage);
28 + EnsureImageIsLoaded(HelloWorldImage);
29 EnsureImageIsLoaded(PythonImage);
30
31 VERIFY_IS_TRUE(::SetEnvironmentVariableW(HostEnvVariableName.c_str(), HostEnvVariableValue.c_str()));
@@ -42,6 +43,7 @@ class WSLCE2EContainerRunTests
43 EnsureContainerDoesNotExist(WslcContainerName);
44 EnsureContainerDoesNotExist(WslcContainerName2);
45 EnsureImageIsDeleted(DebianImage);
46 + EnsureImageIsDeleted(HelloWorldImage);
47 EnsureImageIsDeleted(PythonImage);
48 EnsureVolumeDoesNotExist(WslcVolumeName);
49 EnsureNetworkDoesNotExist(TestNetworkName);
@@ -91,6 +93,36 @@ class WSLCE2EContainerRunTests
93 VerifyContainerIsListed(WslcContainerName, L"exited");
94 }
95
96 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_PullPolicy)
97 + {
98 + auto session = OpenDefaultElevatedSession();
99 + auto [registryContainer, registryAddress] = StartLocalRegistry(*session);
100 + auto registryImage = TagImageForRegistry(HelloWorldImage.NameAndTag(), wsl::shared::string::MultiByteToWide(registryAddress));
101 + auto cleanup = wil::scope_exit([&]() {
102 + EnsureContainerDoesNotExist(WslcContainerName);
103 + RunWslc(std::format(L"image delete --force {}", registryImage));
104 + });
105 +
106 + auto result = RunWslc(std::format(L"container run --pull=never --rm --name {} {}", WslcContainerName, registryImage));
107 + result.Verify({.Stderr = L"", .ExitCode = 0});
108 + VERIFY_IS_TRUE(result.Stdout.has_value());
109 + VERIFY_IS_FALSE(result.Stdout->empty());
110 +
111 + result = RunWslc(std::format(L"container run --pull=always --rm --name {} {}", WslcContainerName, registryImage));
112 + const auto errorMessage = std::format(
113 + L"manifest for {} not found: manifest unknown: manifest unknown\r\nError code: WSLC_E_IMAGE_NOT_FOUND\r\n", registryImage);
114 + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1});
115 + VerifyContainerIsNotListed(WslcContainerName);
116 +
117 + RunWslcAndVerify(std::format(L"push {}", registryImage), {.Stderr = L"", .ExitCode = 0});
118 + RunWslcAndVerify(std::format(L"image delete --force {}", registryImage), {.ExitCode = 0});
119 +
120 + result = RunWslc(std::format(L"container run --pull=missing --rm --name {} {}", WslcContainerName, registryImage));
121 + result.Verify({.ExitCode = 0});
122 + VERIFY_IS_TRUE(result.Stdout.has_value());
123 + VERIFY_IS_FALSE(result.Stdout->empty());
124 + }
125 +
126 WSLC_TEST_METHOD(WSLCE2E_Container_Run_CIDFile_Valid)
127 {
128 // Prepare a CID file path that does not exist
@@ -1383,6 +1415,7 @@ private:
1415
1416 // Test images
1417 const TestImage& DebianImage = DebianTestImage();
1418 + const TestImage& HelloWorldImage = HelloWorldTestImage();
1419 const TestImage& PythonImage = PythonTestImage();
1420
1421 // Test environment variable files
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+6
@@ -118,6 +118,12 @@ const TestImage& DebianTestImage()
118 return image;
119 }
120
121 +const TestImage& HelloWorldTestImage()
122 +{
123 + static const TestImage image{L"hello-world", L"latest", std::filesystem::path{g_testDataPath} / L"HelloWorldSaved.tar"};
124 + return image;
125 +}
126 +
127 const TestImage& PythonTestImage()
128 {
129 static const TestImage image{L"python", L"3.12-alpine", std::filesystem::path{g_testDataPath} / L"python-3_12-alpine.tar"};
test/windows/wslc/e2e/WSLCE2EHelpers.h
+1
@@ -70,6 +70,7 @@ struct TestImage
70
71 const TestImage& AlpineTestImage();
72 const TestImage& DebianTestImage();
73 +const TestImage& HelloWorldTestImage();
74 const TestImage& PythonTestImage();
75 const TestImage& InvalidTestImage();
76