master
cpp 456 lines 16.3 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 ImageTasks.cpp
8
9 Abstract:
10
11 Implementation of image command related execution logic.
12
13 --*/
14 #include "Argument.h"
15 #include "ArgumentConvertedTypes.h"
16 #include "BuildImageCallback.h"
17 #include "CLIExecutionContext.h"
18 #include "ContainerService.h"
19 #include "ImageModel.h"
20 #include "ImageService.h"
21 #include "ImageTasks.h"
22 #include "ImageProgressCallback.h"
23 #include "TableOutput.h"
24 #include "Task.h"
25 #include <format>
26 #include <unordered_map>
27 #include <wslutil.h>
28
29 using namespace wsl::shared;
30 using namespace wsl::windows::common;
31 using namespace wsl::windows::common::string;
32 using namespace wsl::windows::common::timestamp;
33 using namespace wsl::windows::common::wslutil;
34 using namespace wsl::windows::wslc::execution;
35 using namespace wsl::windows::wslc::models;
36 using namespace wsl::windows::wslc::services;
37
38 namespace wsl::windows::wslc::task {
39
40 namespace {
41
42 class DECLSPEC_UUID("91EF98A7-99A8-41C2-893C-43CDFB7DB69F") WSLCImageLoadCallback
43 : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IImageLoadCallback, IFastRundown>
44 {
45 public:
46 explicit WSLCImageLoadCallback(Terminal& terminal) : m_terminal(terminal)
47 {
48 }
49
50 HRESULT OnImageLoaded(LPCSTR Reference, EnumReferenceFormat Format) override
51 try
52 {
53 if (Format == EnumReferenceFormatDigest)
54 {
55 m_terminal.Output(L"{}\n", Localization::WSLCCLI_ImageLoadedId(Reference));
56 }
57 else if (Format == EnumReferenceFormatTag)
58 {
59 m_terminal.Output(L"{}\n", Localization::WSLCCLI_ImageLoaded(Reference));
60 }
61 else
62 {
63 THROW_HR_MSG(E_UNEXPECTED, "Unexpected reference type: %d, '%hs'", Format, Reference);
64 }
65
66 return S_OK;
67 }
68 CATCH_RETURN();
69
70 private:
71 Terminal& m_terminal;
72 };
73
74 // Placeholder for values that are unavailable. wslc does not track image digests or layer sharing.
75 constexpr std::string_view c_imageNotAvailable = "N/A";
76
77 // Builds the representation of an image, shared by the table and json output so the two cannot
78 // drift. Every value is emitted as a string, "<none>" is used for missing repository/tag data,
79 // and the id is truncated unless --no-trunc is passed, in which case it keeps the algorithm prefix.
80 ImageOutputInformation ToImageOutput(const ImageInformation& image, bool truncate)
81 {
82 ImageOutputInformation entry;
83 entry.Containers = image.Containers < 0 ? std::string{c_imageNotAvailable} : std::to_string(image.Containers);
84
85 entry.CreatedAt = EpochToLocalDisplayTime(image.Created);
86 entry.CreatedSince = WideToMultiByte(FormatRelativeTime(image.Created));
87 entry.Digest = c_none;
88 entry.ID = truncate ? TruncateId(image.Id, true) : image.Id;
89 entry.Repository = image.Repository.value_or(std::string{c_none});
90 entry.SharedSize = c_imageNotAvailable;
91 entry.Size = WideToMultiByte(FormatHumanReadableSize(static_cast<uint64_t>(std::max<int64_t>(image.Size, 0))));
92 entry.Tag = image.Tag.value_or(std::string{c_none});
93 entry.UniqueSize = c_imageNotAvailable;
94
95 return entry;
96 }
97
98 } // namespace
99
100 static bool TryInspectImage(Terminal& terminal, Session& session, const std::string& imageId, std::optional<wslc_schema::InspectImage>& inspectData)
101 {
102 try
103 {
104 inspectData = ImageService::Inspect(session, imageId);
105 return true;
106 }
107 catch (const wil::ResultException& ex)
108 {
109 if (ex.GetErrorCode() == WSLC_E_IMAGE_NOT_FOUND)
110 {
111 terminal.Error(L"{}\n", Localization::MessageWslcImageNotFound(imageId.c_str()));
112 return false;
113 }
114
115 throw;
116 }
117 }
118
119 void BuildImage(CLIExecutionContext& context)
120 {
121 WI_ASSERT(context.Data.Contains(Data::Session));
122 WI_ASSERT(context.Args.Contains(ArgType::Path));
123 auto& session = context.Data.Get<Data::Session>();
124 auto& contextPath = context.Args.GetValue<ArgType::Path>();
125
126 auto tags = context.Args.GetAllValues<ArgType::Tag>();
127 auto buildArgs = context.Args.GetAllValues<ArgType::BuildArg>();
128 auto labels = context.Args.GetAllValues<ArgType::BuildLabel>();
129 auto secrets = context.Args.GetAllValues<ArgType::Secret>();
130
131 std::wstring dockerfilePath;
132 if (context.Args.Contains(ArgType::File))
133 {
134 dockerfilePath = context.Args.GetValue<ArgType::File>();
135 }
136
137 std::wstring target;
138 if (context.Args.Contains(ArgType::BuildTarget))
139 {
140 target = context.Args.GetValue<ArgType::BuildTarget>();
141 }
142
143 std::optional<services::BuildOutput> output;
144 if (context.Args.Contains(ArgType::BuildOutput))
145 {
146 output = context.Args.GetValue<ArgType::BuildOutput>();
147 }
148
149 std::optional<std::wstring> iidFilePath;
150 if (context.Args.Contains(ArgType::IidFile))
151 {
152 iidFilePath = context.Args.GetValue<ArgType::IidFile>();
153 }
154
155 WSLCBuildImageFlags flags = WSLCBuildImageFlagsNone;
156 WI_SetFlagIf(flags, WSLCBuildImageFlagsVerbose, context.Args.GetValue<ArgType::Verbose>());
157 WI_SetFlagIf(flags, WSLCBuildImageFlagsNoCache, context.Args.GetValue<ArgType::NoCache>());
158 WI_SetFlagIf(flags, WSLCBuildImageFlagsPull, context.Args.GetValue<ArgType::BuildPull>());
159
160 auto progressMode = context.Args.GetValue<ArgType::Progress>(ProgressMode::Auto);
161
162 // Resolve Auto based on whether progress output (stderr) is an interactive VT console.
163 if (progressMode == ProgressMode::Auto)
164 {
165 progressMode = context.Terminal.IsVTEnabled(Terminal::Level::Info) ? ProgressMode::Tty : ProgressMode::Plain;
166 }
167
168 auto cancelEvent = context.CreateCancelEvent();
169 BuildImageCallback callback(context.Terminal, cancelEvent, context.Args.GetValue<ArgType::Verbose>(), progressMode);
170 services::ImageService::Build(
171 session, contextPath, tags, buildArgs, labels, secrets, dockerfilePath, target, output, iidFilePath, flags, &callback, cancelEvent);
172 }
173
174 void GetImages(CLIExecutionContext& context)
175 {
176 WI_ASSERT(context.Data.Contains(Data::Session));
177 auto& session = context.Data.Get<Data::Session>();
178
179 // Filter values are parsed and cached during argument validation.
180 auto filters = context.Args.GetAllValues<ArgType::Filter>();
181
182 // The container count is only reported by json output, and gathering it costs an extra query in
183 // the service, so it is only requested when it will be shown.
184 const bool containerCounts =
185 context.Args.GetValue<ArgType::Format>(FormatType::Table) == FormatType::Json && !context.Args.GetValue<ArgType::Quiet>();
186
187 auto images = ImageService::List(session, filters, containerCounts);
188 context.Data.Add<Data::Images>(std::move(images));
189 }
190
191 void ListImages(CLIExecutionContext& context)
192 {
193 WI_ASSERT(context.Data.Contains(Data::Images));
194 auto& images = context.Data.Get<Data::Images>();
195
196 if (context.Args.GetValue<ArgType::Quiet>())
197 {
198 bool trunc = !context.Args.GetValue<ArgType::NoTrunc>();
199 for (const auto& image : images)
200 {
201 context.Terminal.Output(L"{}\n", trunc ? TruncateId(image.Id, true) : image.Id);
202 }
203
204 return;
205 }
206
207 const auto format = context.Args.GetValue<ArgType::Format>(FormatType::Table);
208 bool trunc = !context.Args.GetValue<ArgType::NoTrunc>();
209
210 switch (format)
211 {
212 case FormatType::Json:
213 {
214 for (const auto& image : images)
215 {
216 context.Terminal.Output(L"{}\n", ToJsonW(ToImageOutput(image, trunc), c_jsonCompactIndent));
217 }
218
219 break;
220 }
221 case FormatType::Table:
222 {
223 using enum ColumnOverflow;
224
225 // Create table — only IMAGE ID uses fixed width; other columns shrink to fit the console.
226 // When --no-trunc is passed, IMAGE ID also shows full length via TruncateId().
227 auto table =
228 trunc
229 ? wsl::windows::wslc::TableOutput<5>(
230 context.Terminal,
231 {{{L"REPOSITORY", {.Overflow = Shrink}},
232 {L"TAG", {.Overflow = Shrink}},
233 {L"IMAGE ID", {.MinWidth = 12, .MaxWidth = 12, .Overflow = Shrink}},
234 {L"CREATED", {.Overflow = Shrink}},
235 {L"SIZE", {.Overflow = Shrink}}}},
236 images.size())
237 : wsl::windows::wslc::TableOutput<5>(context.Terminal, {L"REPOSITORY", L"TAG", L"IMAGE ID", L"CREATED", L"SIZE"});
238
239 for (const auto& image : images)
240 {
241 const auto entry = ToImageOutput(image, trunc);
242 table.WriteRow({
243 MultiByteToWide(entry.Repository),
244 MultiByteToWide(entry.Tag),
245 MultiByteToWide(entry.ID),
246 MultiByteToWide(entry.CreatedSince),
247 MultiByteToWide(entry.Size),
248 });
249 }
250
251 table.Complete();
252 break;
253 }
254 default:
255 THROW_HR(E_UNEXPECTED);
256 }
257 }
258
259 void PullImage(CLIExecutionContext& context)
260 {
261 WI_ASSERT(context.Data.Contains(Data::Session));
262 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
263 auto& session = context.Data.Get<Data::Session>();
264 const auto image = WideToMultiByte(context.Args.GetValue<ArgType::ImageId>());
265 const bool quiet = context.Args.GetValue<ArgType::Quiet>();
266
267 // Match `docker pull`: for a name-only reference (no tag or digest) the tag defaults to "latest". Unless quiet,
268 // the client reports this on stdout before contacting the registry.
269 const auto reference = ImageReference::Parse(image);
270 if (!quiet && reference.Format == EnumReferenceFormatNone)
271 {
272 context.Terminal.Output(L"{}\n", Localization::WSLCCLI_PullUsingDefaultTag(L"latest"));
273 }
274
275 // Match `docker pull`: in quiet mode, suppress progress output by passing no progress callback. Warnings are
276 // unaffected because the warning callback is built internally by ImageService::Pull from the Terminal.
277 std::optional<ImageProgressCallback> callback;
278 if (!quiet)
279 {
280 callback.emplace(context.Terminal, Terminal::Level::Output);
281 }
282
283 IProgressCallback* progress = callback ? &*callback : nullptr;
284 services::ImageService::Pull(context.Terminal, session, image, progress);
285
286 // Match `docker pull`: always print the resolved canonical image reference as the final line.
287 context.Terminal.Output(L"{}\n", MultiByteToWide(reference.GetCanonical()));
288 }
289
290 void PushImage(CLIExecutionContext& context)
291 {
292 WI_ASSERT(context.Data.Contains(Data::Session));
293 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
294 auto& session = context.Data.Get<Data::Session>();
295 auto& imageId = context.Args.GetValue<ArgType::ImageId>();
296
297 ImageProgressCallback callback(context.Terminal, Terminal::Level::Output);
298 services::ImageService::Push(context.Terminal, session, WideToMultiByte(imageId), &callback);
299 }
300
301 void DeleteImage(CLIExecutionContext& context)
302 {
303 WI_ASSERT(context.Data.Contains(Data::Session));
304 auto& session = context.Data.Get<Data::Session>();
305 auto imageIds = context.Args.GetAllValues<ArgType::ImageId>();
306 bool force = context.Args.GetValue<ArgType::ImageForce>();
307 bool noPrune = context.Args.GetValue<ArgType::NoPrune>();
308 for (const auto& id : imageIds)
309 {
310 const auto deleted = services::ImageService::Delete(session, WideToMultiByte(id), force, noPrune);
311 for (const auto& entry : deleted)
312 {
313 context.Terminal.Output(
314 L"{}\n",
315 entry.Deleted ? Localization::WSLCCLI_ImageDeleteDeleted(entry.Image)
316 : Localization::WSLCCLI_ImageDeleteUntagged(entry.Image));
317 }
318 }
319 }
320
321 void LoadImage(CLIExecutionContext& context)
322 {
323 WI_ASSERT(context.Data.Contains(Data::Session));
324 auto& session = context.Data.Get<Data::Session>();
325
326 if (context.Args.Contains(ArgType::Input))
327 {
328 auto& input = context.Args.GetValue<ArgType::Input>();
329 auto callback = wil::MakeOrThrow<WSLCImageLoadCallback>(context.Terminal);
330 services::ImageService::Load(context.Terminal, session, input, callback.Get());
331 return;
332 }
333
334 // TODO Read from stdin if no input argument is provided.
335 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_ImageLoadNoInputError());
336 }
337
338 void ImportImage(CLIExecutionContext& context)
339 {
340 WI_ASSERT(context.Data.Contains(Data::Session));
341 WI_ASSERT(context.Args.Contains(ArgType::ImportFile));
342 auto& session = context.Data.Get<Data::Session>();
343
344 std::string imageName;
345 if (context.Args.Contains(ArgType::ImageId))
346 {
347 imageName = WideToMultiByte(context.Args.GetValue<ArgType::ImageId>());
348 }
349
350 auto& input = context.Args.GetValue<ArgType::ImportFile>();
351 auto imageId = services::ImageService::Import(context.Terminal, session, input, imageName);
352 if (!imageId.empty())
353 {
354 bool trunc = !context.Args.GetValue<ArgType::NoTrunc>();
355 context.Terminal.Output(L"{}\n", MultiByteToWide(TruncateId(imageId, trunc)));
356 }
357 }
358
359 void InspectImages(CLIExecutionContext& context)
360 {
361 WI_ASSERT(context.Data.Contains(Data::Session));
362 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
363 auto& session = context.Data.Get<Data::Session>();
364 auto imageIds = context.Args.GetAllValues<ArgType::ImageId>();
365
366 std::vector<wsl::windows::common::wslc_schema::InspectImage> result;
367 for (const auto& id : imageIds)
368 {
369 std::optional<wslc_schema::InspectImage> inspectData;
370 if (TryInspectImage(context.Terminal, session, WideToMultiByte(id), inspectData))
371 {
372 result.push_back(*inspectData);
373 }
374 else
375 {
376 context.ExitCode = 1;
377 }
378 }
379
380 auto json = ToJson(result, context.Args.GetValue<ArgType::InspectFormat>(c_jsonPrettyPrintIndent));
381 context.Terminal.Output(L"{}\n", MultiByteToWide(json));
382 }
383
384 void SaveImage(CLIExecutionContext& context)
385 {
386 WI_ASSERT(context.Data.Contains(Data::Session));
387 WI_ASSERT(context.Args.Contains(ArgType::ImageId));
388 auto& session = context.Data.Get<Data::Session>();
389 auto imageIds = context.Args.GetAllValues<ArgType::ImageId>();
390
391 std::vector<std::string> images;
392 images.reserve(imageIds.size());
393 for (const auto& id : imageIds)
394 {
395 images.push_back(WideToMultiByte(id));
396 }
397
398 if (context.Args.Contains(ArgType::Output))
399 {
400 auto& output = context.Args.GetValue<ArgType::Output>();
401 services::ImageService::Save(session, images, output, context.CreateCancelEvent());
402 }
403 else
404 {
405 auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
406 if (wsl::windows::common::wslutil::IsConsoleHandle(stdoutHandle))
407 {
408 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_ImageSaveStdoutIsTerminalError());
409 }
410
411 services::ImageService::Save(session, images, stdoutHandle, context.CreateCancelEvent());
412 }
413 }
414
415 void TagImage(CLIExecutionContext& context)
416 {
417 WI_ASSERT(context.Data.Contains(Data::Session));
418 auto& session = context.Data.Get<Data::Session>();
419 auto& source = context.Args.GetValue<ArgType::Source>();
420 auto& target = context.Args.GetValue<ArgType::Target>();
421 services::ImageService::Tag(session, WideToMultiByte(source), WideToMultiByte(target));
422 }
423
424 void PruneImages(CLIExecutionContext& context)
425 {
426 WI_ASSERT(context.Data.Contains(Data::Session));
427 auto& session = context.Data.Get<Data::Session>();
428
429 bool all = context.Args.GetValue<ArgType::All>();
430
431 // Filter values are parsed and cached during argument validation.
432 auto filters = context.Args.GetAllValues<ArgType::Filter>();
433
434 auto result = ImageService::Prune(session, all, filters);
435
436 if (!result.UntaggedImages.empty() || !result.DeletedImages.empty())
437 {
438 context.Terminal.Output(L"{}\n", Localization::WSLCCLI_ImagePruneDeletedHeader());
439
440 for (const auto& image : result.UntaggedImages)
441 {
442 context.Terminal.Output(L"{}\n", Localization::WSLCCLI_ImagePruneUntagged(image));
443 }
444
445 for (const auto& image : result.DeletedImages)
446 {
447 context.Terminal.Output(L"{}\n", Localization::WSLCCLI_ImagePruneDeleted(image));
448 }
449
450 context.Terminal.Output(L"\n");
451 }
452
453 context.Terminal.Output(
454 L"{}\n", Localization::WSLCCLI_ImagePruneSpaceReclaimedBytes(FormatHumanReadableSize(result.SpaceReclaimed, c_reclaimedSpacePrecision)));
455 }
456 } // namespace wsl::windows::wslc::task