| 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 |