Merge ~ines-almeida/launchpad:fetch-service-update-end-session into launchpad:master
- Git
- lp:~ines-almeida/launchpad
- fetch-service-update-end-session
- Merge into master
Proposed by
Ines Almeida
Status: | Merged |
---|---|
Approved by: | Ines Almeida |
Approved revision: | c55da58c037c6cb89c77787ae1c8b59c60eb3b68 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~ines-almeida/launchpad:fetch-service-update-end-session |
Merge into: | launchpad:master |
Diff against target: |
413 lines (+131/-51) 4 files modified
lib/lp/buildmaster/builderproxy.py (+74/-39) lib/lp/buildmaster/interactor.py (+4/-0) lib/lp/buildmaster/tests/mock_workers.py (+12/-0) lib/lp/snappy/tests/test_snapbuildbehaviour.py (+41/-12) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jürgen Gmach | Approve | ||
Review via email: mp+465152@code.launchpad.net |
Commit message
Update how to keep track of session_id within a builder session
Description of the change
To post a comment you must log in.
Revision history for this message
Ines Almeida (ines-almeida) wrote : | # |
Revision history for this message
Jürgen Gmach (jugmac00) : | # |
Revision history for this message
Jürgen Gmach (jugmac00) : | # |
Revision history for this message
Ines Almeida (ines-almeida) wrote : | # |
Fixed the mypy issues and updated the logic to use the `use_fetch_service` flag directly from the builder proxy info. I still need to re-work a test, but this is now ready for review.
Revision history for this message
Jürgen Gmach (jugmac00) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/buildmaster/builderproxy.py b/lib/lp/buildmaster/builderproxy.py |
2 | index 1fc0d9b..a52ce06 100644 |
3 | --- a/lib/lp/buildmaster/builderproxy.py |
4 | +++ b/lib/lp/buildmaster/builderproxy.py |
5 | @@ -17,8 +17,9 @@ __all__ = [ |
6 | |
7 | import base64 |
8 | import os |
9 | +import re |
10 | import time |
11 | -from typing import Dict, Generator, Type |
12 | +from typing import Optional |
13 | |
14 | from twisted.internet import defer |
15 | |
16 | @@ -37,6 +38,10 @@ from lp.services.config import config |
17 | BUILD_METADATA_FILENAME_FORMAT = "{build_id}_metadata.json" |
18 | |
19 | |
20 | +class ProxyServiceException(Exception): |
21 | + pass |
22 | + |
23 | + |
24 | def _get_proxy_config(name): |
25 | """Get a config item from builddmaster (current) or snappy (deprecated).""" |
26 | return getattr(config.builddmaster, name) or getattr(config.snappy, name) |
27 | @@ -56,28 +61,23 @@ class BuilderProxyMixin: |
28 | build: BuildFarmJob |
29 | _worker: BuilderWorker |
30 | |
31 | - def __init__(self, *args, **kwargs): |
32 | - super().__init__(*args, **kwargs) |
33 | - self._use_fetch_service = False |
34 | - self._proxy_service = None |
35 | - |
36 | @defer.inlineCallbacks |
37 | def startProxySession( |
38 | self, |
39 | args: BuildArgs, |
40 | allow_internet: bool = True, |
41 | use_fetch_service: bool = False, |
42 | - ) -> Generator[None, Dict[str, str], None]: |
43 | - |
44 | - self._use_fetch_service = use_fetch_service |
45 | + ): |
46 | |
47 | if not allow_internet: |
48 | return |
49 | |
50 | + proxy_service: IProxyService |
51 | + |
52 | if not use_fetch_service and _get_proxy_config("builder_proxy_host"): |
53 | - proxy_service: Type[IProxyService] = BuilderProxy |
54 | + proxy_service = BuilderProxy(worker=self._worker) |
55 | elif use_fetch_service and _get_proxy_config("fetch_service_host"): |
56 | - proxy_service = FetchService |
57 | + proxy_service = FetchService(worker=self._worker) |
58 | |
59 | # Append the fetch-service certificate to BuildArgs secrets. |
60 | if "secrets" not in args: |
61 | @@ -91,10 +91,9 @@ class BuilderProxyMixin: |
62 | # non-production environments. |
63 | return |
64 | |
65 | - self._proxy_service = proxy_service( |
66 | - build_id=self.build.build_cookie, worker=self._worker |
67 | + session_data = yield proxy_service.startSession( |
68 | + build_id=self.build.build_cookie |
69 | ) |
70 | - session_data = yield self._proxy_service.startSession() |
71 | |
72 | args["proxy_url"] = session_data["proxy_url"] |
73 | args["revocation_endpoint"] = session_data["revocation_endpoint"] |
74 | @@ -115,34 +114,55 @@ class BuilderProxyMixin: |
75 | Sessions will be closed automatically within the Fetch Service after |
76 | a certain amount of time configured by its charm (default 6 hours). |
77 | """ |
78 | - if not self._use_fetch_service: |
79 | - # No cleanup needed for the builder proxy |
80 | - return |
81 | |
82 | - if self._proxy_service is None: |
83 | - # A session was never started. This can happen if the proxy configs |
84 | - # are not set (see `startProxySession`) |
85 | + proxy_info = yield self._worker.proxy_info() |
86 | + use_fetch_service = proxy_info.get("use_fetch_service") |
87 | + |
88 | + if not use_fetch_service: |
89 | + # No cleanup needed when not using the fetch service |
90 | + # This is true both when we use the builder proxy and when we don't |
91 | + # allow internet access to the builds. |
92 | return |
93 | |
94 | + proxy_service = FetchService(worker=self._worker) |
95 | + |
96 | + # XXX ines-almeida 2024-04-30: Getting the session_id from the |
97 | + # revocation_endpoint feels a little like going back and forwards |
98 | + # given the revocation_endpoint is created on `startProxySession()`. |
99 | + # Ideally, we would update `buildd` and `buildd-manager` to senf and |
100 | + # retrieve the session ID directly (instead of having to parse it). |
101 | + revocation_endpoint = proxy_info.get("revocation_endpoint") |
102 | + session_id = proxy_service.extractSessionIDFromRevocationEndpoint( |
103 | + revocation_endpoint |
104 | + ) |
105 | + |
106 | + if session_id is None: |
107 | + raise ProxyServiceException( |
108 | + "Could not parse the revocation endpoint fetched from buildd " |
109 | + f"('{revocation_endpoint}') to get the fetch service " |
110 | + "`session_id` used within the build." |
111 | + ) |
112 | + |
113 | metadata_file_name = BUILD_METADATA_FILENAME_FORMAT.format( |
114 | build_id=self.build.build_cookie |
115 | ) |
116 | file_path = os.path.join(upload_path, metadata_file_name) |
117 | - yield self._proxy_service.retrieveMetadataFromSession( |
118 | - save_content_to=file_path |
119 | + yield proxy_service.retrieveMetadataFromSession( |
120 | + session_id=session_id, |
121 | + save_content_to=file_path, |
122 | ) |
123 | |
124 | - yield self._proxy_service.endSession() |
125 | + yield proxy_service.endSession(session_id=session_id) |
126 | |
127 | |
128 | class IProxyService: |
129 | """Interface for Proxy Services - either FetchService or BuilderProxy.""" |
130 | |
131 | - def __init__(self, build_id: str, worker: BuilderWorker): |
132 | + def __init__(self, worker: BuilderWorker): |
133 | pass |
134 | |
135 | @defer.inlineCallbacks |
136 | - def startSession(self): |
137 | + def startSession(self, build_id: str): |
138 | """Start a proxy session and request required |
139 | |
140 | :returns: dictionary with an authenticated `proxy_url` for the builder |
141 | @@ -159,7 +179,7 @@ class BuilderProxy(IProxyService): |
142 | making API requests directly to the builder proxy control endpoint. |
143 | """ |
144 | |
145 | - def __init__(self, build_id: str, worker: BuilderWorker): |
146 | + def __init__(self, worker: BuilderWorker): |
147 | self.control_endpoint = _get_value_from_config( |
148 | "builder_proxy_auth_api_endpoint" |
149 | ) |
150 | @@ -168,8 +188,6 @@ class BuilderProxy(IProxyService): |
151 | port=_get_value_from_config("builder_proxy_port"), |
152 | ) |
153 | self.auth_header = self._getAuthHeader() |
154 | - |
155 | - self.build_id = build_id |
156 | self.worker = worker |
157 | |
158 | @staticmethod |
159 | @@ -187,14 +205,14 @@ class BuilderProxy(IProxyService): |
160 | return b"Basic " + base64.b64encode(auth_string.encode("ASCII")) |
161 | |
162 | @defer.inlineCallbacks |
163 | - def startSession(self): |
164 | + def startSession(self, build_id: str): |
165 | """Request a token from the builder proxy to be used by the builders. |
166 | |
167 | See IProxyService. |
168 | """ |
169 | timestamp = int(time.time()) |
170 | proxy_username = "{build_id}-{timestamp}".format( |
171 | - build_id=self.build_id, timestamp=timestamp |
172 | + build_id=build_id, timestamp=timestamp |
173 | ) |
174 | |
175 | token = yield self.worker.process_pool.doWork( |
176 | @@ -233,7 +251,7 @@ class FetchService(IProxyService): |
177 | RETRIEVE_METADATA_ENDPOINT = "{control_endpoint}/session/{session_id}" |
178 | END_SESSION_ENDPOINT = "{control_endpoint}/session/{session_id}" |
179 | |
180 | - def __init__(self, build_id: str, worker: BuilderWorker): |
181 | + def __init__(self, worker: BuilderWorker): |
182 | self.control_endpoint = _get_value_from_config( |
183 | "fetch_service_control_endpoint" |
184 | ) |
185 | @@ -242,10 +260,7 @@ class FetchService(IProxyService): |
186 | port=_get_value_from_config("fetch_service_port"), |
187 | ) |
188 | self.auth_header = self._getAuthHeader() |
189 | - |
190 | - self.build_id = build_id |
191 | self.worker = worker |
192 | - self.session_id = None |
193 | |
194 | @staticmethod |
195 | def _getAuthHeader(): |
196 | @@ -259,7 +274,7 @@ class FetchService(IProxyService): |
197 | return b"Basic " + base64.b64encode(auth_string.encode("ASCII")) |
198 | |
199 | @defer.inlineCallbacks |
200 | - def startSession(self): |
201 | + def startSession(self, build_id: str): |
202 | """Requests a fetch service session and returns session information. |
203 | |
204 | See IProxyService. |
205 | @@ -290,8 +305,28 @@ class FetchService(IProxyService): |
206 | "revocation_endpoint": revocation_endpoint, |
207 | } |
208 | |
209 | + def extractSessionIDFromRevocationEndpoint( |
210 | + self, revocation_endpoint: str |
211 | + ) -> Optional[str]: |
212 | + """Helper method to get the session_id out of the revocation |
213 | + endpoint. |
214 | + |
215 | + Example `revocation_endpoint`: |
216 | + http://fetch-service.net:9999/session/913890402914ce3be9ffd4d0/token |
217 | + |
218 | + Would return: 913890402914ce3be9ffd4d0 |
219 | + """ |
220 | + re_pattern = self.TOKEN_REVOCATION.format( |
221 | + control_endpoint=self.control_endpoint, |
222 | + session_id="(?P<session_id>.*)", |
223 | + ) |
224 | + match = re.match(re_pattern, revocation_endpoint) |
225 | + return match["session_id"] if match else None |
226 | + |
227 | @defer.inlineCallbacks |
228 | - def retrieveMetadataFromSession(self, save_content_to: str): |
229 | + def retrieveMetadataFromSession( |
230 | + self, session_id: str, save_content_to: str |
231 | + ): |
232 | """Make request to retrieve metadata from the current session. |
233 | |
234 | Data is stored directly into a file whose path is `save_content_to` |
235 | @@ -300,7 +335,7 @@ class FetchService(IProxyService): |
236 | """ |
237 | url = self.RETRIEVE_METADATA_ENDPOINT.format( |
238 | control_endpoint=self.control_endpoint, |
239 | - session_id=self.session_id, |
240 | + session_id=session_id, |
241 | ) |
242 | yield self.worker.process_pool.doWork( |
243 | RetrieveFetchServiceSessionCommand, |
244 | @@ -310,14 +345,14 @@ class FetchService(IProxyService): |
245 | ) |
246 | |
247 | @defer.inlineCallbacks |
248 | - def endSession(self): |
249 | + def endSession(self, session_id: str): |
250 | """End the proxy session and do any cleanup needed. |
251 | |
252 | :raises: RequestException if request to Fetch Service fails |
253 | """ |
254 | url = self.END_SESSION_ENDPOINT.format( |
255 | control_endpoint=self.control_endpoint, |
256 | - session_id=self.session_id, |
257 | + session_id=session_id, |
258 | ) |
259 | yield self.worker.process_pool.doWork( |
260 | EndFetchServiceSessionCommand, |
261 | diff --git a/lib/lp/buildmaster/interactor.py b/lib/lp/buildmaster/interactor.py |
262 | index dfcc86b..9bf1a71 100644 |
263 | --- a/lib/lp/buildmaster/interactor.py |
264 | +++ b/lib/lp/buildmaster/interactor.py |
265 | @@ -209,6 +209,10 @@ class BuilderWorker: |
266 | """Echo the arguments back.""" |
267 | return self._with_timeout(self._server.callRemote("echo", *args)) |
268 | |
269 | + def proxy_info(self): |
270 | + """Return the details for the proxy used by the manager.""" |
271 | + return self._with_timeout(self._server.callRemote("proxy_info")) |
272 | + |
273 | def info(self): |
274 | """Return the protocol version and the builder methods supported.""" |
275 | return self._with_timeout(self._server.callRemote("info")) |
276 | diff --git a/lib/lp/buildmaster/tests/mock_workers.py b/lib/lp/buildmaster/tests/mock_workers.py |
277 | index a880a9a..5f5563b 100644 |
278 | --- a/lib/lp/buildmaster/tests/mock_workers.py |
279 | +++ b/lib/lp/buildmaster/tests/mock_workers.py |
280 | @@ -151,6 +151,18 @@ class OkWorker: |
281 | self.call_log.append("info") |
282 | return defer.succeed(("1.0", self.arch_tag, "binarypackage")) |
283 | |
284 | + def proxy_info(self): |
285 | + self.call_log.append("proxy_info") |
286 | + return defer.succeed( |
287 | + { |
288 | + "revocation_endpoint": ( |
289 | + "http://fetch-service.test:9999/session/" |
290 | + "9138904ce3be9ffd4d0/token" |
291 | + ), |
292 | + "use_fetch_service": False, |
293 | + } |
294 | + ) |
295 | + |
296 | def resume(self): |
297 | self.call_log.append("resume") |
298 | return defer.succeed(("", "", 0)) |
299 | diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py |
300 | index 81d6926..cb2269b 100644 |
301 | --- a/lib/lp/snappy/tests/test_snapbuildbehaviour.py |
302 | +++ b/lib/lp/snappy/tests/test_snapbuildbehaviour.py |
303 | @@ -10,6 +10,7 @@ import time |
304 | import uuid |
305 | from datetime import datetime |
306 | from textwrap import dedent |
307 | +from unittest.mock import MagicMock |
308 | from urllib.parse import urlsplit |
309 | |
310 | import fixtures |
311 | @@ -40,7 +41,6 @@ from lp.app.enums import InformationType |
312 | from lp.archivepublisher.interfaces.archivegpgsigningkey import ( |
313 | IArchiveGPGSigningKey, |
314 | ) |
315 | -from lp.buildmaster.builderproxy import BuilderProxy |
316 | from lp.buildmaster.enums import BuildBaseImageType, BuildStatus |
317 | from lp.buildmaster.interactor import shut_down_default_process_pool |
318 | from lp.buildmaster.interfaces.builder import CannotBuild |
319 | @@ -431,6 +431,20 @@ class TestAsyncSnapBuildBehaviourFetchService( |
320 | snap = self.factory.makeSnap(use_fetch_service=True) |
321 | request = self.factory.makeSnapBuildRequest(snap=snap) |
322 | job = self.makeJob(snap=snap, build_request=request) |
323 | + |
324 | + host = config.builddmaster.fetch_service_host |
325 | + port = config.builddmaster.fetch_service_port |
326 | + session_id = self.fetch_service_api.sessions.session_id |
327 | + revocation_endpoint = ( |
328 | + f"http://{host}:{port}/session/{session_id}/token" |
329 | + ) |
330 | + |
331 | + job._worker.proxy_info = MagicMock( |
332 | + return_value={ |
333 | + "revocation_endpoint": revocation_endpoint, |
334 | + "use_fetch_service": True, |
335 | + } |
336 | + ) |
337 | yield job.extraBuildArgs() |
338 | |
339 | # End the session |
340 | @@ -443,7 +457,6 @@ class TestAsyncSnapBuildBehaviourFetchService( |
341 | start_session_request = self.fetch_service_api.sessions.requests[0] |
342 | self.assertEqual(b"POST", start_session_request["method"]) |
343 | self.assertEqual(b"/session", start_session_request["uri"]) |
344 | - session_id = self.fetch_service_api.sessions.responses[0]["id"] |
345 | |
346 | # Request retrieve metadata |
347 | retrieve_metadata_request = self.fetch_service_api.sessions.requests[1] |
348 | @@ -478,6 +491,14 @@ class TestAsyncSnapBuildBehaviourFetchService( |
349 | snap = self.factory.makeSnap(use_fetch_service=False) |
350 | request = self.factory.makeSnapBuildRequest(snap=snap) |
351 | job = self.makeJob(snap=snap, build_request=request) |
352 | + |
353 | + job._worker.proxy_info = MagicMock( |
354 | + return_value={ |
355 | + "revocation_endpoint": "https://builder-proxy.test/revoke", |
356 | + "use_fetch_service": False, |
357 | + } |
358 | + ) |
359 | + |
360 | yield job.extraBuildArgs() |
361 | yield job.endProxySession(upload_path="test_path") |
362 | |
363 | @@ -485,23 +506,33 @@ class TestAsyncSnapBuildBehaviourFetchService( |
364 | self.assertEqual(0, len(self.fetch_service_api.sessions.requests)) |
365 | |
366 | @defer.inlineCallbacks |
367 | - def test_endProxySession_no_proxy_service(self): |
368 | - """When the `fetch_service_host` is not set, the calls to the fetch |
369 | - service don't go through.""" |
370 | + def test_endProxySession_fetch_Service_allow_internet_false(self): |
371 | + """When `allow_internet` is False, we don't send proxy variables to the |
372 | + buildd, and ending the session does not make calls to the fetch |
373 | + service.""" |
374 | self.useFixture( |
375 | FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: "on"}) |
376 | ) |
377 | - self.useFixture(FeatureFixture({"fetch_service_host": None})) |
378 | - |
379 | - snap = self.factory.makeSnap(use_fetch_service=True) |
380 | + snap = self.factory.makeSnap(allow_internet=False) |
381 | request = self.factory.makeSnapBuildRequest(snap=snap) |
382 | job = self.makeJob(snap=snap, build_request=request) |
383 | - yield job.extraBuildArgs() |
384 | + args = yield job.extraBuildArgs() |
385 | + |
386 | + # Scenario when we don't allow internet |
387 | + job._worker.proxy_info = MagicMock( |
388 | + return_value={ |
389 | + "revocation_endpoint": None, |
390 | + "use_fetch_service": None, |
391 | + } |
392 | + ) |
393 | + |
394 | yield job.endProxySession(upload_path="test_path") |
395 | |
396 | + # No proxy config sent to buildd |
397 | + self.assertIsNone(args.get("use_fetch_service")) |
398 | + self.assertIsNone(args.get("revocation_endpoint")) |
399 | # No calls go through to the fetch service |
400 | self.assertEqual(0, len(self.fetch_service_api.sessions.requests)) |
401 | - self.assertEqual(None, job._proxy_service) |
402 | |
403 | |
404 | class TestAsyncSnapBuildBehaviourBuilderProxy( |
405 | @@ -1470,8 +1501,6 @@ class TestAsyncSnapBuildBehaviourBuilderProxy( |
406 | |
407 | # End the session |
408 | yield job.endProxySession(upload_path="test_path") |
409 | - self.assertFalse(job.use_fetch_service) |
410 | - self.assertTrue(isinstance(job.proxy_service, BuilderProxy)) |
411 | |
412 | @defer.inlineCallbacks |
413 | def test_composeBuildRequest_proxy_url_set(self): |
This has not been cleaned up (and mypy issues not fixed). Opening it up for an early review.