Split image into repo and tag arguments when relevant#14484
Split image into repo and tag arguments when relevant#14484OneBlue wants to merge 6 commits intofeature/wsl-for-appsfrom
Conversation
| size_t separator = Input.find_last_of(':'); | ||
| if (separator == std::string::npos) | ||
| { | ||
| return {Input, {}}; | ||
| } | ||
|
|
||
| THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslaInvalidImage(Input), separator >= Input.size() - 1 || separator == 0); | ||
|
|
||
| return {Input.substr(0, separator), Input.substr(separator + 1)}; |
There was a problem hiding this comment.
ParseImage splits on the last ':' unconditionally. This mis-parses valid image references that include a registry port but no explicit tag (e.g., "localhost:5000/repo"), treating "5000/repo" as the tag and preventing defaulting to "latest". Consider implementing Docker reference parsing rules (only treat ':' as a tag separator if it occurs after the last '/', and handle digests like "@sha256:") so repo/tag are derived correctly.
| size_t separator = Input.find_last_of(':'); | |
| if (separator == std::string::npos) | |
| { | |
| return {Input, {}}; | |
| } | |
| THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslaInvalidImage(Input), separator >= Input.size() - 1 || separator == 0); | |
| return {Input.substr(0, separator), Input.substr(separator + 1)}; | |
| // | |
| // Docker-like parsing rules: | |
| // - A ':' is treated as a tag separator only if it occurs after the last '/'. | |
| // - Any digest (e.g. "@sha256:...") comes after '@' and may contain ':'; these | |
| // colons must not be treated as tag separators. | |
| // | |
| const size_t digestSeparator = Input.find('@'); | |
| // Portion used to locate a potential tag (before any '@' digest). | |
| const std::string nameForTag = (digestSeparator == std::string::npos) ? Input : Input.substr(0, digestSeparator); | |
| // Portion containing any digest (including '@'), which should remain attached to the repository. | |
| const std::string digestPart = (digestSeparator == std::string::npos) ? std::string() : Input.substr(digestSeparator); | |
| const size_t lastSlash = nameForTag.rfind('/'); | |
| const size_t lastColon = nameForTag.rfind(':'); | |
| // No ':' suitable for a tag separator -> no explicit tag, return image as-is. | |
| if (lastColon == std::string::npos || (lastSlash != std::string::npos && lastColon <= lastSlash)) | |
| { | |
| return {Input, {}}; | |
| } | |
| // Validate that the tag separator is not the first character and that a tag is present. | |
| THROW_HR_WITH_USER_ERROR_IF( | |
| E_INVALIDARG, | |
| Localization::MessageWslaInvalidImage(Input), | |
| lastColon == 0 || lastColon >= nameForTag.size() - 1); | |
| const std::string repoBase = nameForTag.substr(0, lastColon); | |
| const std::string tag = nameForTag.substr(lastColon + 1); | |
| // Reattach any digest part to the repository. | |
| const std::string repo = repoBase + digestPart; | |
| return {repo, tag}; |
| // Negative: null repo inside options must fail. | ||
| { | ||
| WslcPullImageOptions opts{}; | ||
| opts.repo = "repo"; | ||
| opts.tag = nullptr; |
There was a problem hiding this comment.
This negative test comment is duplicated: the second block is validating a null tag, not a null repo. Updating the comment will avoid confusion when interpreting failures.
| @@ -496,32 +503,43 @@ class WslcSdkTests | |||
| VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize)); | |||
|
|
|||
| VERIFY_SUCCEEDED(WslcImportSessionImage( | |||
| m_defaultSession, c_handleImportedImageName, imageTarFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, nullptr)); | |||
| m_defaultSession, "my-hello-world-handle", "test", imageTarFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, nullptr)); | |||
|
|
|||
| auto output = RunContainerAndCapture(m_defaultSession, c_handleImportedImageName, {"/hello"}); | |||
| auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { | |||
| LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, "my-hello-world-handle:test", nullptr)); | |||
| }); | |||
There was a problem hiding this comment.
The import tests no longer delete the target image before attempting the import, and the cleanup scope_exit is only created after a successful import. If the image already exists from a previous run (or the import fails after partially creating it), this test can become flaky. Consider deleting the image upfront (ignore failures) and/or creating the cleanup guard before calling WslcImportSessionImage/WslcImportSessionImageFromFile so cleanup runs even when the import fails.
| RETURN_HR_IF_NULL(E_POINTER, Repo); | ||
| RETURN_HR_IF_NULL(E_POINTER, Tag); | ||
|
|
||
| auto [repo, tag] = ParseImage(ImageUri); | ||
| std::string repo{Repo}; | ||
| std::string tag{Tag}; | ||
|
|
There was a problem hiding this comment.
WSLASession::PullImage no longer parses/validates "repo:tag" and now accepts Repo/Tag separately, but it only checks for null pointers. This allows empty Tag/Repo (e.g., repo="foo", tag="") which previously would have been rejected by ParseImage. Consider adding explicit validation that Repo and Tag are non-empty (and possibly reusing the same invalid-image localization) to preserve input validation behavior and avoid passing invalid refs to dockerd.
| RETURN_HR_IF_NULL(E_POINTER, Repo); | ||
| RETURN_HR_IF_NULL(E_POINTER, Tag); | ||
| RETURN_HR_IF(E_INVALIDARG, strlen(Repo) + strlen(Tag) + 1 > WSLA_MAX_IMAGE_NAME_LENGTH); | ||
|
|
||
| THROW_HR_IF_MSG(E_INVALIDARG, !tag.has_value(), "Expected tag for image import: %hs", ImageName); | ||
| std::string repo{Repo}; | ||
| std::string tag{Tag}; |
There was a problem hiding this comment.
WSLASession::ImportImage now takes Repo/Tag separately but only validates null pointers and combined length. It should also reject empty Repo/Tag strings (e.g., tag="") to avoid importing under an invalid name and to match the prior ParseImage validation that ensured both parts were non-empty.
| HRESULT PullImage([in] LPCSTR Repo, [in] LPCSTR Tag, [in, unique] const WslaRegistryAuthInformation* RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback); | ||
| HRESULT BuildImage([in] const WSLABuildImageOptions* Options, [in, unique] IProgressCallback* ProgressCallback); | ||
| HRESULT LoadImage([in] ULONG ImageHandle, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength); | ||
| HRESULT ImportImage([in] ULONG ImageHandle, [in] LPCSTR ImageName, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength); | ||
| HRESULT ImportImage([in] ULONG ImageHandle, [in] LPCSTR Repo, [in] LPCSTR Tag, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength); |
There was a problem hiding this comment.
Changing the IWSLASession COM method signatures (PullImage/ImportImage) is ABI-breaking for any existing clients compiled against the previous IDL. With the IID left unchanged, old clients can end up calling into the new vtable slot with a mismatched stack layout. Consider introducing a new interface (e.g., IWSLASession2 with a new uuid) or adding new methods while keeping the existing ones for backward compatibility, and update callers accordingly.
| @@ -376,14 +377,20 @@ typedef struct WslcImportImageOptions | |||
|
|
|||
| STDAPI WslcImportSessionImage( | |||
| _In_ WslcSession session, | |||
| _In_z_ PCSTR imageName, | |||
| _In_z_ PCSTR repo, | |||
| _In_z_ PCSTR tag, | |||
| _In_ HANDLE imageContent, | |||
| _In_ uint64_t imageContentLength, | |||
| _In_opt_ const WslcImportImageOptions* options, | |||
| _Outptr_opt_result_z_ PWSTR* errorMessage); | |||
|
|
|||
| STDAPI WslcImportSessionImageFromFile( | |||
| _In_ WslcSession session, _In_z_ PCSTR imageName, _In_z_ PCWSTR path, _In_opt_ const WslcImportImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage); | |||
| _In_ WslcSession session, | |||
| _In_z_ PCSTR repo, | |||
| _In_z_ PCSTR tag, | |||
| _In_z_ PCWSTR path, | |||
| _In_opt_ const WslcImportImageOptions* options, | |||
| _Outptr_opt_result_z_ PWSTR* errorMessage); | |||
There was a problem hiding this comment.
WslcSDK is a C ABI surface: changing WslcPullImageOptions layout (uri -> repo/tag) and the import function signatures is a binary breaking change for existing applications built against previous headers. Consider preserving the old entrypoints/struct (e.g., add new *V2 functions/options structs) or otherwise versioning the DLL/API so older clients fail gracefully rather than misinterpreting struct fields or calling with the wrong signature.
|
|
||
| // Image management. | ||
| HRESULT PullImage([in] LPCSTR ImageUri, [in, unique] const WslaRegistryAuthInformation* RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback); | ||
| HRESULT PullImage([in] LPCSTR Repo, [in] LPCSTR Tag, [in, unique] const WslaRegistryAuthInformation* RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback); |
There was a problem hiding this comment.
I think we should keep it so PullImage accepts just an image name, which is then parsed into repo and tag. Partly this is to make it so clients don't need to do the parsing themselves, but also because we need to support pulling an image by digest (e.g. docker pull ubuntu@sha256:66460d557b25769b102175144d538d88219c077c678a49af4afca6fbfc1b5252). You could make PullImage takes a repo, tag, and digest, as an alternative.
There was a problem hiding this comment.
This comment from Copilot also touches on some of the parsing particularities. I don't know if there is a spec that defines exactly what form an image string can take, unfortunately.
There was a problem hiding this comment.
Hmm that's a good point. The main reason for this change was that today if you run wslc pull debian, we pull every single image in the debian repo, which is obviously not the behavior that we want.
I'll rethink this. Maybe we can make this work by splitting the image into repo & tag on the service side and defaulting to latest if no tag is passed.
Sadly I don't think there's a way around us having to parse repo & tags
There was a problem hiding this comment.
This looks like a good reference for an image parser: https://deepwiki.com/openshift/docker-distribution/9-image-references
Summary of the Pull Request
This change splits the single "image" string into two "repo" and "tags" arguments when pulling or importing images.
This allows us to easily default to "latest" in these operations, and have the correct behavior when doing something like:
PR Checklist
Detailed Description of the Pull Request / Additional comments
Validation Steps Performed