master
cpp 501 lines 20.2 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 ImageService.cpp
8
9 Abstract:
10
11 This file contains the ImageService implementation
12
13 --*/
14 #include "ImageService.h"
15 #include "RegistryService.h"
16 #include "SessionService.h"
17 #include "SpecParsing.h"
18 #include "WarningCallback.h"
19 #include <filesystem.hpp>
20 #include <wslutil.h>
21 #include <HandleConsoleProgressBar.h>
22 #include <relay.hpp>
23
24 using namespace wsl::shared;
25 using namespace wsl::windows::common::wslutil;
26
27 namespace {
28
29 wil::unique_hfile ResolveBuildFile(const std::filesystem::path& contextPath)
30 {
31 auto containerfilePath = contextPath / L"Containerfile";
32 auto containerfileStatus = wil::try_open_file(containerfilePath.c_str());
33
34 auto dockerfilePath = contextPath / L"Dockerfile";
35 auto dockerfileStatus = wil::try_open_file(dockerfilePath.c_str());
36
37 // Fail if both Containerfile and Dockerfile exist.
38 // Assume that both exist if one opens successfully and the other returns anything other than ERROR_FILE_NOT_FOUND to cover the case where one of them exists, but fails to open.
39 // If both exist but fail to open, the logic after this block will report the appropriate error.
40 if ((containerfileStatus.last_error != ERROR_FILE_NOT_FOUND && dockerfileStatus.file) ||
41 (dockerfileStatus.last_error != ERROR_FILE_NOT_FOUND && containerfileStatus.file))
42 {
43 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcBothDockerAndContainerFileFound());
44 }
45
46 if (containerfileStatus.last_error != ERROR_FILE_NOT_FOUND)
47 {
48 THROW_HR_WITH_USER_ERROR_IF(
49 HRESULT_FROM_WIN32(containerfileStatus.last_error),
50 Localization::MessageWslcFailedToOpenFile(
51 containerfilePath, wsl::windows::common::wslutil::GetSystemErrorString(HRESULT_FROM_WIN32(containerfileStatus.last_error))),
52 !containerfileStatus.file.is_valid());
53
54 return std::move(containerfileStatus.file);
55 }
56
57 if (dockerfileStatus.last_error != ERROR_FILE_NOT_FOUND)
58 {
59 THROW_HR_WITH_USER_ERROR_IF(
60 HRESULT_FROM_WIN32(dockerfileStatus.last_error),
61 Localization::MessageWslcFailedToOpenFile(
62 dockerfilePath, wsl::windows::common::wslutil::GetSystemErrorString(HRESULT_FROM_WIN32(dockerfileStatus.last_error))),
63 !dockerfileStatus.file.is_valid());
64
65 return std::move(dockerfileStatus.file);
66 }
67
68 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcBuildFileNotFound(contextPath));
69 }
70
71 std::string GetServerFromImage(const std::string& image)
72 {
73 return wsl::windows::common::wslutil::ImageReference::Parse(image).Repository.Server;
74 }
75
76 struct InputSource
77 {
78 InputSource(wsl::windows::common::relay::HandleWrapper&& handle, ULONGLONG contentLength) :
79 Handle(std::move(handle)), ContentLength(contentLength)
80 {
81 }
82
83 wsl::windows::common::relay::HandleWrapper Handle;
84 ULONGLONG ContentLength = 0;
85 };
86
87 wsl::windows::common::relay::HandleWrapper OpenInputHandle(const std::wstring& input)
88 {
89 if (input == L"-")
90 {
91 return wsl::windows::common::relay::HandleWrapper(GetStdHandle(STD_INPUT_HANDLE));
92 }
93
94 wil::unique_hfile file(CreateFileW(input.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
95 THROW_LAST_ERROR_IF(!file);
96
97 return wsl::windows::common::relay::HandleWrapper(std::move(file));
98 }
99
100 InputSource OpenImageInput(const std::wstring& input)
101 {
102 auto handle = OpenInputHandle(input);
103
104 LARGE_INTEGER fileSize{};
105 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcImportPipeNotSupported(), !GetFileSizeEx(handle.Get(), &fileSize));
106
107 return InputSource{std::move(handle), static_cast<ULONGLONG>(fileSize.QuadPart)};
108 }
109
110 } // namespace
111
112 namespace wsl::windows::wslc::services {
113
114 using namespace wsl::windows::wslc::models;
115 using wsl::windows::common::wslc_schema::InspectImage;
116
117 void ImageService::Build(
118 wsl::windows::wslc::models::Session& session,
119 const std::wstring& contextPath,
120 const std::vector<std::wstring>& tags,
121 const std::vector<std::wstring>& buildArgs,
122 const std::vector<std::wstring>& labels,
123 const std::vector<BuildSecret>& secrets,
124 const std::wstring& dockerfilePath,
125 const std::wstring& target,
126 const std::optional<BuildOutput>& output,
127 const std::optional<std::wstring>& iidFilePath,
128 WSLCBuildImageFlags flags,
129 IProgressCallback* callback,
130 HANDLE cancelEvent)
131 {
132 auto absolutePath = std::filesystem::absolute(contextPath);
133 THROW_HR_IF_MSG(
134 HRESULT_FROM_WIN32(ERROR_DIRECTORY),
135 !std::filesystem::is_directory(absolutePath),
136 "Path must be a directory: %ls",
137 absolutePath.c_str());
138
139 HANDLE dockerfileHandle = nullptr;
140 wil::unique_hfile dockerfile;
141 if (dockerfilePath == L"-")
142 {
143 dockerfileHandle = GetStdHandle(STD_INPUT_HANDLE);
144 }
145 else if (!dockerfilePath.empty())
146 {
147 dockerfile.reset(CreateFileW(dockerfilePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
148 THROW_LAST_ERROR_IF_MSG(!dockerfile, "Failed to open Dockerfile: %ls", dockerfilePath.c_str());
149 dockerfileHandle = dockerfile.get();
150 }
151 else
152 {
153 dockerfile = ResolveBuildFile(absolutePath);
154 dockerfileHandle = dockerfile.get();
155 }
156
157 auto toMultiByte = [](const std::vector<std::wstring>& input, std::vector<std::string>& strings, std::vector<LPCSTR>& pointers) {
158 strings.reserve(input.size());
159 for (const auto& s : input)
160 {
161 strings.push_back(wsl::windows::common::string::WideToMultiByte(s));
162 pointers.push_back(strings.back().c_str());
163 }
164 };
165
166 std::vector<std::string> tagStrings;
167 std::vector<LPCSTR> tagPointers;
168 toMultiByte(tags, tagStrings, tagPointers);
169
170 std::vector<std::string> buildArgStrings;
171 std::vector<LPCSTR> buildArgPointers;
172 toMultiByte(buildArgs, buildArgStrings, buildArgPointers);
173
174 std::vector<std::string> labelStrings;
175 std::vector<LPCSTR> labelPointers;
176 toMultiByte(labels, labelStrings, labelPointers);
177
178 // Keep narrow-encoded id strings alive for the duration of the COM call. The source path and raw
179 // secret bytes are referenced in place from the caller's BuildSecret objects (which outlive this
180 // call), so they are never copied or NUL-truncated.
181 std::vector<std::string> secretIdStrings;
182 std::vector<WSLCBuildSecret> secretEntries;
183 secretIdStrings.reserve(secrets.size());
184 secretEntries.reserve(secrets.size());
185 for (const auto& secret : secrets)
186 {
187 secretIdStrings.push_back(wsl::windows::common::string::WideToMultiByte(secret.Id));
188 secretEntries.push_back(WSLCBuildSecret{
189 .Id = secretIdStrings.back().c_str(),
190 .SourcePath = secret.SourcePath.empty() ? nullptr : secret.SourcePath.c_str(),
191 .Value = secret.Value.empty() ? nullptr : secret.Value.data(),
192 .ValueSize = static_cast<ULONG>(secret.Value.size()),
193 });
194 }
195
196 auto targetStr = wsl::windows::common::string::WideToMultiByte(target);
197
198 // Route the docker-style --output exporter. A single-file exporter with a real destination (tar/oci/
199 // docker with dest=) has the destination's parent directory mounted read-write into the VM, so buildx
200 // writes the output file straight to the destination in place. dest=- (stdout) streams the exporter
201 // tarball back out of the VM to OutputHandle. Exporters with no client destination (docker load,
202 // image, registry, cacheonly) run entirely in the VM and the spec is forwarded as-is. Directory
203 // exporters (type=local, or oci/docker with tar=false) are rejected up front by ParseOutputSpec: a
204 // Linux tree cannot be written faithfully to a Windows-backed destination.
205 std::string outputStr;
206 HANDLE outputHandle = nullptr;
207
208 // For a single-file exporter with a real destination the server writes the exporter output into a
209 // read-write virtiofs mount of the destination's parent directory rather than streaming it back:
210 // outputMountPath is that parent directory and outputMountFile the destination file's leaf name.
211 std::wstring outputMountPath;
212 std::wstring outputMountFile;
213
214 if (output.has_value())
215 {
216 const auto& spec = output.value();
217 // Route the exporter the same way `docker buildx build --output` does: some exporters produce a
218 // result the client must materialize (a file or a stdout stream), while others run entirely in
219 // the build VM. Directory exporters are already rejected by ParseOutputSpec. See
220 // OutputStreamsToClient.
221 const bool streamsBack = validation::OutputStreamsToClient(spec);
222
223 if (streamsBack)
224 {
225 if (spec.Dest == L"-")
226 {
227 // dest=- streams the exporter tarball to the client's stdout, matching docker.
228 outputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
229
230 // Refuse to dump the binary exporter stream onto an interactive console (matching docker).
231 // Otherwise ToCOMInputHandle would fail the marshal with a cryptic ERROR_NOT_SUPPORTED for
232 // the console handle. A redirected character device such as NUL is not a console, so
233 // IsConsoleHandle (which also checks GetConsoleMode) still lets those through.
234 THROW_HR_WITH_USER_ERROR_IF(
235 E_INVALIDARG,
236 Localization::MessageWslcOutputConsoleNotSupported(validation::FormatOutputSpec(spec)),
237 IsConsoleHandle(outputHandle));
238 }
239 else
240 {
241 // Single-file exporter with a real destination: mount the destination's parent directory
242 // read-write into the VM so buildx writes the exporter output file straight to the
243 // destination in place.
244 auto destPath = std::filesystem::absolute(spec.Dest);
245 auto destDir = destPath.parent_path();
246 std::filesystem::create_directories(destDir);
247
248 outputMountPath = destDir.wstring();
249 outputMountFile = destPath.filename().wstring();
250 }
251
252 // The server picks the VM-side dest, so forward the spec without the client's dest.
253 BuildOutput vmSpec = spec;
254 vmSpec.Dest.clear();
255 outputStr = wsl::windows::common::string::WideToMultiByte(validation::FormatOutputSpec(vmSpec));
256 }
257 else
258 {
259 outputStr = wsl::windows::common::string::WideToMultiByte(validation::FormatOutputSpec(spec));
260 }
261 }
262
263 auto contextPathStr = absolutePath.wstring();
264
265 // Resolve the --iidfile destination against the client's working directory; the server mounts its
266 // parent directory read-write into the VM so buildx writes the image ID straight to it.
267 std::wstring iidPathStr;
268 if (iidFilePath.has_value())
269 {
270 iidPathStr = wsl::windows::common::filesystem::GetCanonicalPath(*iidFilePath).wstring();
271 }
272
273 WSLCBuildImageOptions options{
274 .ContextPath = contextPathStr.c_str(),
275 .DockerfileHandle = ToCOMInputHandle(dockerfileHandle),
276 .Tags = {tagPointers.data(), static_cast<ULONG>(tagPointers.size())},
277 .BuildArgs = {buildArgPointers.data(), static_cast<ULONG>(buildArgPointers.size())},
278 .Target = targetStr.empty() ? nullptr : targetStr.c_str(),
279 .Flags = flags,
280 .Labels = {labelPointers.data(), static_cast<ULONG>(labelPointers.size())},
281 .Secrets = {secretEntries.data(), static_cast<ULONG>(secretEntries.size())},
282 .Output = outputStr.empty() ? nullptr : outputStr.c_str(),
283 .OutputHandle = outputHandle != nullptr ? ToCOMInputHandle(outputHandle) : WSLCHandle{.Type = WSLCHandleTypeUnknown},
284 .OutputMountPath = outputMountPath.empty() ? nullptr : outputMountPath.c_str(),
285 .OutputMountFile = outputMountFile.empty() ? nullptr : outputMountFile.c_str(),
286 .IidFilePath = iidPathStr.empty() ? nullptr : iidPathStr.c_str(),
287 };
288
289 THROW_IF_FAILED(session.Get()->BuildImage(&options, callback, cancelEvent));
290 }
291
292 std::vector<ImageInformation> ImageService::List(
293 wsl::windows::wslc::models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters, bool containerCounts)
294 {
295 std::vector<WSLCFilter> filterEntries;
296 filterEntries.reserve(filters.size());
297 for (const auto& [key, value] : filters)
298 {
299 filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()});
300 }
301
302 WSLCListImagesOptions options{};
303 options.Flags = containerCounts ? WSLCListImagesFlagsContainerCounts : WSLCListImagesFlagsNone;
304 options.Filters = filterEntries.empty() ? nullptr : filterEntries.data();
305 options.FiltersCount = static_cast<ULONG>(filterEntries.size());
306
307 wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
308 ULONG count = 0;
309 THROW_IF_FAILED(session.Get()->ListImages(&options, &images, &count));
310
311 std::vector<ImageInformation> result;
312 for (auto ptr = images.get(), end = images.get() + count; ptr != end; ++ptr)
313 {
314 const WSLCImageInformation& image = *ptr;
315 ImageInformation info{};
316
317 // Parse the image reference — dangling images have no repo/tag
318 std::string imageRef = image.Image;
319 if (imageRef != "<none>:<none>")
320 {
321 auto parsed = wsl::windows::common::wslutil::ImageReference::Parse(imageRef);
322 info.Repository = parsed.Repository.Name;
323 info.Tag = parsed.TagOrDigest();
324 }
325
326 info.Id = image.Hash;
327 info.Created = image.Created;
328 info.Size = image.Size;
329 info.Containers = image.Containers;
330 result.push_back(info);
331 }
332
333 return result;
334 }
335
336 void ImageService::Load(Terminal& terminal, wsl::windows::wslc::models::Session& session, const std::wstring& input, IImageLoadCallback* callback)
337 {
338 WarningCallback warningCallback(terminal);
339 auto source = OpenImageInput(input);
340 THROW_IF_FAILED(session.Get()->LoadImage(ToCOMInputHandle(source.Handle.Get()), source.ContentLength, &warningCallback, callback));
341 }
342
343 std::string ImageService::Import(Terminal& terminal, wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName)
344 {
345 WarningCallback warningCallback(terminal);
346 auto source = OpenImageInput(input);
347 wil::unique_cotaskmem_ansistring imageId;
348 THROW_IF_FAILED(session.Get()->ImportImage(
349 ToCOMInputHandle(source.Handle.Get()), imageName.empty() ? nullptr : imageName.c_str(), source.ContentLength, &warningCallback, &imageId));
350 return imageId.get() ? std::string(imageId.get()) : std::string();
351 }
352
353 std::vector<wsl::windows::wslc::models::DeletedImageEntry> ImageService::Delete(
354 wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune)
355 {
356 WSLCDeleteImageOptions options{};
357 options.Image = image.c_str();
358
359 if (force)
360 {
361 options.Flags |= WSLCDeleteImageFlagsForce;
362 }
363
364 if (noPrune)
365 {
366 options.Flags |= WSLCDeleteImageFlagsNoPrune;
367 }
368
369 wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
370 THROW_IF_FAILED(session.Get()->DeleteImage(&options, &deletedImages, deletedImages.size_address<ULONG>()));
371
372 std::vector<wsl::windows::wslc::models::DeletedImageEntry> result;
373 result.reserve(deletedImages.size());
374 for (const auto& entry : deletedImages)
375 {
376 result.push_back({entry.Image, entry.Type == WSLCDeletedImageTypeDeleted});
377 }
378
379 return result;
380 }
381
382 void ImageService::Pull(Terminal& terminal, wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback)
383 {
384 WarningCallback warningCallback(terminal);
385 auto server = GetServerFromImage(image);
386 auto auth = RegistryService::Get(server);
387 THROW_IF_FAILED(session.Get()->PullImage(image.c_str(), auth.c_str(), callback, &warningCallback));
388 }
389
390 void ImageService::Tag(wsl::windows::wslc::models::Session& session, const std::string& sourceImage, const std::string& targetImage)
391 {
392 auto reference = ImageReference::Parse(targetImage);
393 if (reference.Format == EnumReferenceFormatDigest)
394 {
395 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcTagImageInvalidFormat(targetImage.c_str()));
396 }
397
398 WSLCTagImageOptions options{};
399 options.Image = sourceImage.c_str();
400 options.Repo = reference.Repository.Name.c_str();
401 options.Tag = reference.Tag ? reference.Tag->c_str() : "";
402
403 THROW_IF_FAILED(session.Get()->TagImage(&options));
404 }
405
406 InspectImage ImageService::Inspect(wsl::windows::wslc::models::Session& session, const std::string& image)
407 {
408 wil::unique_cotaskmem_ansistring inspectData;
409 THROW_IF_FAILED(session.Get()->InspectImage(image.c_str(), &inspectData));
410 return wsl::shared::FromJson<InspectImage>(inspectData.get());
411 }
412
413 void ImageService::Push(Terminal& terminal, wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback)
414 {
415 WarningCallback warningCallback(terminal);
416 auto server = GetServerFromImage(image);
417 auto auth = RegistryService::Get(server);
418 THROW_IF_FAILED(session.Get()->PushImage(image.c_str(), auth.c_str(), callback, &warningCallback));
419 }
420
421 void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::vector<std::string>& images, const std::wstring& output, HANDLE cancelEvent)
422 {
423 wil::unique_hfile outputFile{
424 CreateFileW(output.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
425 THROW_LAST_ERROR_IF(!outputFile);
426
427 Save(session, images, outputFile.get(), cancelEvent);
428 }
429
430 void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::vector<std::string>& images, HANDLE outputHandle, HANDLE cancelEvent)
431 {
432 WI_ASSERT(!images.empty());
433
434 wsl::windows::common::HandleConsoleProgressBar progressBar(
435 outputHandle, Localization::MessageWslcSaveInProgress(), wsl::windows::common::HandleConsoleProgressBar::Format::FileSize);
436
437 if (images.size() == 1)
438 {
439 THROW_IF_FAILED(session.Get()->SaveImage(ToCOMInputHandle(outputHandle), images[0].c_str(), nullptr, cancelEvent));
440 }
441 else
442 {
443 std::vector<LPCSTR> imagePointers;
444 imagePointers.reserve(images.size());
445 for (const auto& image : images)
446 {
447 imagePointers.push_back(image.c_str());
448 }
449
450 WSLCStringArray imageArray{
451 .Values = imagePointers.data(),
452 .Count = static_cast<ULONG>(imagePointers.size()),
453 };
454
455 THROW_IF_FAILED(session.Get()->SaveImages(ToCOMInputHandle(outputHandle), &imageArray, nullptr, cancelEvent));
456 }
457 }
458
459 wsl::windows::wslc::models::PruneImagesResult ImageService::Prune(
460 wsl::windows::wslc::models::Session& session, bool all, const std::vector<std::pair<std::string, std::string>>& filters)
461 {
462 // The --all flag is translated into a `dangling` filter. Skip the implicit
463 // filter if the caller already supplied an explicit `dangling` filter so the
464 // user's value wins (matching docker's behavior).
465 const bool hasExplicitDangling =
466 std::any_of(filters.begin(), filters.end(), [](const auto& f) { return f.first == "dangling"; });
467
468 std::vector<WSLCFilter> filterEntries;
469 filterEntries.reserve(filters.size() + (hasExplicitDangling ? 0 : 1));
470 if (!hasExplicitDangling)
471 {
472 filterEntries.push_back({.Key = "dangling", .Value = all ? "false" : "true"});
473 }
474
475 for (const auto& [key, value] : filters)
476 {
477 filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()});
478 }
479
480 wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
481 ULONGLONG spaceReclaimed = 0;
482 THROW_IF_FAILED(session.Get()->PruneImages(
483 filterEntries.data(), static_cast<ULONG>(filterEntries.size()), &deletedImages, deletedImages.size_address<ULONG>(), &spaceReclaimed));
484
485 wsl::windows::wslc::models::PruneImagesResult result;
486 result.SpaceReclaimed = spaceReclaimed;
487 for (const auto& entry : deletedImages)
488 {
489 if (entry.Type == WSLCDeletedImageTypeDeleted)
490 {
491 result.DeletedImages.push_back(entry.Image);
492 }
493 else
494 {
495 result.UntaggedImages.push_back(entry.Image);
496 }
497 }
498
499 return result;
500 }
501 } // namespace wsl::windows::wslc::services