master
cpp 306 lines 8.78 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 ContainerModel.cpp
8
9 Abstract:
10
11 This file contains the ContainerModel implementation
12 --*/
13
14 #include "precomp.h"
15 #include "ContainerModel.h"
16 #include <unordered_set>
17
18 namespace wsl::windows::wslc::models {
19
20 using namespace wsl::shared;
21 using namespace wsl::shared::string;
22
23 PublishPort::PortRange PublishPort::PortRange::ParsePortPart(const std::string& portPart)
24 {
25 static auto parsePort = [](const std::string& value, const std::string& errorMessage) -> uint16_t {
26 try
27 {
28 // Ensure the value is not empty and contains only digits before parsing
29 if (value.empty() || !std::all_of(value.begin(), value.end(), ::isdigit))
30 {
31 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
32 }
33
34 // Parse the port number and validate the range
35 auto port = std::stoul(value, nullptr, 10);
36 if (!PublishPort::IsValidPort(port))
37 {
38 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
39 }
40 return static_cast<uint16_t>(port);
41 }
42 catch (const std::exception&)
43 {
44 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
45 }
46 };
47
48 // Find optional port range separator
49 auto dashPos = portPart.find('-');
50 if (dashPos != std::string::npos)
51 {
52 // Port range specified
53 auto startPortStr = portPart.substr(0, dashPos);
54 auto endPortStr = portPart.substr(dashPos + 1);
55 auto startPort = parsePort(startPortStr, std::format("Invalid port range specified in port mapping: '{}'.", portPart));
56 auto endPort = parsePort(endPortStr, std::format("Invalid port range specified in port mapping: '{}'.", portPart));
57 return {startPort, endPort};
58 }
59
60 // Single port specified
61 auto port = parsePort(portPart, std::format("Invalid port specified in port mapping: '{}'.", portPart));
62 return {port, port};
63 }
64
65 PublishPort PublishPort::Parse(const std::string& value)
66 {
67 PublishPort result{};
68 result.m_original = value;
69
70 // 1. Strip optional protocol suffix
71 std::string portPart = value;
72 auto slashPos = value.find('/');
73 if (slashPos != std::string::npos)
74 {
75 portPart = value.substr(0, slashPos);
76 auto protocolPart = value.substr(slashPos + 1);
77 if (protocolPart == "tcp")
78 {
79 result.m_protocol = PublishPort::Protocol::TCP;
80 }
81 else if (protocolPart == "udp")
82 {
83 result.m_protocol = PublishPort::Protocol::UDP;
84 }
85 else
86 {
87 THROW_HR_WITH_USER_ERROR(
88 E_INVALIDARG, "Invalid protocol specified in port mapping. Only 'tcp' and 'udp' are supported.");
89 }
90 }
91
92 // 2. Split off the container port from the right
93 auto colonPos = portPart.rfind(':');
94 std::optional<std::string> hostPortPart;
95 if (colonPos != std::string::npos)
96 {
97 result.m_containerPort = PublishPort::PortRange::ParsePortPart(portPart.substr(colonPos + 1));
98 hostPortPart = portPart.substr(0, colonPos);
99 }
100 else
101 {
102 result.m_containerPort = PublishPort::PortRange::ParsePortPart(portPart);
103 }
104
105 // 3. Parse the host port
106 if (hostPortPart.has_value())
107 {
108 auto colonPos = hostPortPart->rfind(':');
109 if (colonPos != std::string::npos)
110 {
111 result.m_hostIP = PublishPort::IPAddress(hostPortPart->substr(0, colonPos));
112 auto hostPort = hostPortPart->substr(colonPos + 1);
113 if (!hostPort.empty())
114 {
115 result.m_hostPort = PublishPort::PortRange::ParsePortPart(hostPort);
116 }
117 }
118 else
119 {
120 result.m_hostPort = PublishPort::PortRange::ParsePortPart(*hostPortPart);
121 }
122 }
123
124 result.Validate();
125 return result;
126 }
127
128 void PublishPort::Validate() const
129 {
130 if (m_containerPort.Count() == 0)
131 {
132 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, "Container port must specify at least one port.");
133 }
134
135 if (!m_containerPort.IsValid())
136 {
137 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, "Container port must be a valid port number (1-65535).");
138 }
139
140 if (!m_hostPort.IsEphemeral())
141 {
142 if (!m_hostPort.IsValid())
143 {
144 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, "Host port must be a valid port number (1-65535).");
145 }
146
147 if (m_hostPort.Count() != m_containerPort.Count())
148 {
149 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, "Host port range must match the container port range.");
150 }
151 }
152 }
153
154 // Returns true if the given string is a valid Docker named volume name.
155 // Based on Docker's named volume validation: ^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,}$
156 // Source: https://github.com/moby/moby/blob/master/volume/validate.go
157 bool VolumeMount::IsValidNamedVolumeName(const std::wstring& name)
158 {
159 return mount::IsValidNamedVolumeName(name);
160 }
161
162 VolumeMount VolumeMount::Parse(const std::wstring& value)
163 {
164 mount::Spec mountSpec;
165 try
166 {
167 mountSpec = mount::ParseDockerVolumeString(value);
168 mount::ValidateMountSpec(mountSpec);
169 }
170 catch (const mount::MountException& ex)
171 {
172 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, ex.Reason());
173 }
174
175 VolumeMount volume;
176 volume.m_host = std::move(mountSpec.Source);
177 volume.m_containerPath = std::move(mountSpec.Target);
178 volume.m_isReadOnlyMode = mountSpec.ReadOnly;
179 volume.m_isNamedVolume = mountSpec.MountType == WSLCMountTypeVolume;
180 return volume;
181 }
182
183 std::optional<std::wstring> EnvironmentVariable::Parse(const std::wstring& entry)
184 {
185 if (entry.empty() || std::all_of(entry.begin(), entry.end(), std::iswspace))
186 {
187 return std::nullopt;
188 }
189
190 std::wstring key;
191 std::optional<std::wstring> value;
192
193 auto delimiterPos = entry.find('=');
194 if (delimiterPos == std::wstring::npos)
195 {
196 key = entry;
197 }
198 else
199 {
200 key = entry.substr(0, delimiterPos);
201 value = entry.substr(delimiterPos + 1);
202 }
203
204 if (key.empty())
205 {
206 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_EnvKeyEmptyError());
207 }
208
209 if (std::any_of(key.begin(), key.end(), std::iswspace))
210 {
211 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_EnvKeyWhitespaceError(key));
212 }
213
214 if (!value.has_value())
215 {
216 std::wstring envValue;
217 auto hr = wil::GetEnvironmentVariableW(key.c_str(), envValue);
218 if (FAILED(hr))
219 {
220 return std::nullopt;
221 }
222
223 value = envValue;
224 }
225
226 return std::format(L"{}={}", key, value.value());
227 }
228
229 std::vector<std::wstring> EnvironmentVariable::ParseFile(const std::wstring& filePath)
230 {
231 std::ifstream file(filePath);
232 if (!file.is_open() || !file.good())
233 {
234 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, std::format(L"Environment file '{}' cannot be opened for reading", filePath));
235 }
236
237 // Read the file line by line
238 std::vector<std::wstring> envVars;
239 std::string line;
240 while (std::getline(file, line))
241 {
242 // Remove leading whitespace
243 line.erase(line.begin(), std::find_if(line.begin(), line.end(), [](unsigned char ch) { return !std::isspace(ch); }));
244
245 // Skip empty lines and comments
246 if (line.empty() || line[0] == '#')
247 {
248 continue;
249 }
250
251 auto envVar = Parse(wsl::shared::string::MultiByteToWide(line));
252 if (envVar.has_value())
253 {
254 envVars.push_back(std::move(envVar.value()));
255 }
256 }
257
258 return envVars;
259 }
260
261 CidFile::CidFile(const std::optional<std::wstring>& path)
262 {
263 if (!path.has_value())
264 {
265 return;
266 }
267
268 m_path = *path;
269 auto [file, openError] = wil::try_create_new_file(std::filesystem::path(*m_path).c_str(), GENERIC_WRITE);
270 if (!file.is_valid())
271 {
272 if (openError == ERROR_FILE_EXISTS || openError == ERROR_ALREADY_EXISTS)
273 {
274 THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(openError), Localization::WSLCCLI_CIDFileAlreadyExistsError(*m_path));
275 }
276
277 const auto errorMessage = wsl::windows::common::wslutil::GetSystemErrorString(HRESULT_FROM_WIN32(openError));
278 THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(openError), Localization::MessageWslcFailedToOpenFile(*m_path, errorMessage));
279 }
280
281 m_file = std::move(file);
282 }
283
284 CidFile::~CidFile()
285 {
286 if (m_committed || !m_path.has_value())
287 {
288 return;
289 }
290
291 m_file.reset();
292 std::error_code ec;
293 std::filesystem::remove(std::filesystem::path(*m_path), ec);
294 }
295
296 void CidFile::Commit(const std::string& containerId)
297 {
298 if (m_file)
299 {
300 DWORD bytesWritten{};
301 THROW_IF_WIN32_BOOL_FALSE(::WriteFile(m_file.get(), containerId.data(), static_cast<DWORD>(containerId.size()), &bytesWritten, nullptr));
302 }
303
304 m_committed = true;
305 }
306 } // namespace wsl::windows::wslc::models