Merge ~ines-almeida/launchpad:fetch-service-update-end-session into launchpad: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)
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

To post a comment you must log in.
Revision history for this message
Ines Almeida (ines-almeida) wrote :

This has not been cleaned up (and mypy issues not fixed). Opening it up for an early review.

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
diff --git a/lib/lp/buildmaster/builderproxy.py b/lib/lp/buildmaster/builderproxy.py
index 1fc0d9b..a52ce06 100644
--- a/lib/lp/buildmaster/builderproxy.py
+++ b/lib/lp/buildmaster/builderproxy.py
@@ -17,8 +17,9 @@ __all__ = [
1717
18import base6418import base64
19import os19import os
20import re
20import time21import time
21from typing import Dict, Generator, Type22from typing import Optional
2223
23from twisted.internet import defer24from twisted.internet import defer
2425
@@ -37,6 +38,10 @@ from lp.services.config import config
37BUILD_METADATA_FILENAME_FORMAT = "{build_id}_metadata.json"38BUILD_METADATA_FILENAME_FORMAT = "{build_id}_metadata.json"
3839
3940
41class ProxyServiceException(Exception):
42 pass
43
44
40def _get_proxy_config(name):45def _get_proxy_config(name):
41 """Get a config item from builddmaster (current) or snappy (deprecated)."""46 """Get a config item from builddmaster (current) or snappy (deprecated)."""
42 return getattr(config.builddmaster, name) or getattr(config.snappy, name)47 return getattr(config.builddmaster, name) or getattr(config.snappy, name)
@@ -56,28 +61,23 @@ class BuilderProxyMixin:
56 build: BuildFarmJob61 build: BuildFarmJob
57 _worker: BuilderWorker62 _worker: BuilderWorker
5863
59 def __init__(self, *args, **kwargs):
60 super().__init__(*args, **kwargs)
61 self._use_fetch_service = False
62 self._proxy_service = None
63
64 @defer.inlineCallbacks64 @defer.inlineCallbacks
65 def startProxySession(65 def startProxySession(
66 self,66 self,
67 args: BuildArgs,67 args: BuildArgs,
68 allow_internet: bool = True,68 allow_internet: bool = True,
69 use_fetch_service: bool = False,69 use_fetch_service: bool = False,
70 ) -> Generator[None, Dict[str, str], None]:70 ):
71
72 self._use_fetch_service = use_fetch_service
7371
74 if not allow_internet:72 if not allow_internet:
75 return73 return
7674
75 proxy_service: IProxyService
76
77 if not use_fetch_service and _get_proxy_config("builder_proxy_host"):77 if not use_fetch_service and _get_proxy_config("builder_proxy_host"):
78 proxy_service: Type[IProxyService] = BuilderProxy78 proxy_service = BuilderProxy(worker=self._worker)
79 elif use_fetch_service and _get_proxy_config("fetch_service_host"):79 elif use_fetch_service and _get_proxy_config("fetch_service_host"):
80 proxy_service = FetchService80 proxy_service = FetchService(worker=self._worker)
8181
82 # Append the fetch-service certificate to BuildArgs secrets.82 # Append the fetch-service certificate to BuildArgs secrets.
83 if "secrets" not in args:83 if "secrets" not in args:
@@ -91,10 +91,9 @@ class BuilderProxyMixin:
91 # non-production environments.91 # non-production environments.
92 return92 return
9393
94 self._proxy_service = proxy_service(94 session_data = yield proxy_service.startSession(
95 build_id=self.build.build_cookie, worker=self._worker95 build_id=self.build.build_cookie
96 )96 )
97 session_data = yield self._proxy_service.startSession()
9897
99 args["proxy_url"] = session_data["proxy_url"]98 args["proxy_url"] = session_data["proxy_url"]
100 args["revocation_endpoint"] = session_data["revocation_endpoint"]99 args["revocation_endpoint"] = session_data["revocation_endpoint"]
@@ -115,34 +114,55 @@ class BuilderProxyMixin:
115 Sessions will be closed automatically within the Fetch Service after114 Sessions will be closed automatically within the Fetch Service after
116 a certain amount of time configured by its charm (default 6 hours).115 a certain amount of time configured by its charm (default 6 hours).
117 """116 """
118 if not self._use_fetch_service:
119 # No cleanup needed for the builder proxy
120 return
121117
122 if self._proxy_service is None:118 proxy_info = yield self._worker.proxy_info()
123 # A session was never started. This can happen if the proxy configs119 use_fetch_service = proxy_info.get("use_fetch_service")
124 # are not set (see `startProxySession`)120
121 if not use_fetch_service:
122 # No cleanup needed when not using the fetch service
123 # This is true both when we use the builder proxy and when we don't
124 # allow internet access to the builds.
125 return125 return
126126
127 proxy_service = FetchService(worker=self._worker)
128
129 # XXX ines-almeida 2024-04-30: Getting the session_id from the
130 # revocation_endpoint feels a little like going back and forwards
131 # given the revocation_endpoint is created on `startProxySession()`.
132 # Ideally, we would update `buildd` and `buildd-manager` to senf and
133 # retrieve the session ID directly (instead of having to parse it).
134 revocation_endpoint = proxy_info.get("revocation_endpoint")
135 session_id = proxy_service.extractSessionIDFromRevocationEndpoint(
136 revocation_endpoint
137 )
138
139 if session_id is None:
140 raise ProxyServiceException(
141 "Could not parse the revocation endpoint fetched from buildd "
142 f"('{revocation_endpoint}') to get the fetch service "
143 "`session_id` used within the build."
144 )
145
127 metadata_file_name = BUILD_METADATA_FILENAME_FORMAT.format(146 metadata_file_name = BUILD_METADATA_FILENAME_FORMAT.format(
128 build_id=self.build.build_cookie147 build_id=self.build.build_cookie
129 )148 )
130 file_path = os.path.join(upload_path, metadata_file_name)149 file_path = os.path.join(upload_path, metadata_file_name)
131 yield self._proxy_service.retrieveMetadataFromSession(150 yield proxy_service.retrieveMetadataFromSession(
132 save_content_to=file_path151 session_id=session_id,
152 save_content_to=file_path,
133 )153 )
134154
135 yield self._proxy_service.endSession()155 yield proxy_service.endSession(session_id=session_id)
136156
137157
138class IProxyService:158class IProxyService:
139 """Interface for Proxy Services - either FetchService or BuilderProxy."""159 """Interface for Proxy Services - either FetchService or BuilderProxy."""
140160
141 def __init__(self, build_id: str, worker: BuilderWorker):161 def __init__(self, worker: BuilderWorker):
142 pass162 pass
143163
144 @defer.inlineCallbacks164 @defer.inlineCallbacks
145 def startSession(self):165 def startSession(self, build_id: str):
146 """Start a proxy session and request required166 """Start a proxy session and request required
147167
148 :returns: dictionary with an authenticated `proxy_url` for the builder168 :returns: dictionary with an authenticated `proxy_url` for the builder
@@ -159,7 +179,7 @@ class BuilderProxy(IProxyService):
159 making API requests directly to the builder proxy control endpoint.179 making API requests directly to the builder proxy control endpoint.
160 """180 """
161181
162 def __init__(self, build_id: str, worker: BuilderWorker):182 def __init__(self, worker: BuilderWorker):
163 self.control_endpoint = _get_value_from_config(183 self.control_endpoint = _get_value_from_config(
164 "builder_proxy_auth_api_endpoint"184 "builder_proxy_auth_api_endpoint"
165 )185 )
@@ -168,8 +188,6 @@ class BuilderProxy(IProxyService):
168 port=_get_value_from_config("builder_proxy_port"),188 port=_get_value_from_config("builder_proxy_port"),
169 )189 )
170 self.auth_header = self._getAuthHeader()190 self.auth_header = self._getAuthHeader()
171
172 self.build_id = build_id
173 self.worker = worker191 self.worker = worker
174192
175 @staticmethod193 @staticmethod
@@ -187,14 +205,14 @@ class BuilderProxy(IProxyService):
187 return b"Basic " + base64.b64encode(auth_string.encode("ASCII"))205 return b"Basic " + base64.b64encode(auth_string.encode("ASCII"))
188206
189 @defer.inlineCallbacks207 @defer.inlineCallbacks
190 def startSession(self):208 def startSession(self, build_id: str):
191 """Request a token from the builder proxy to be used by the builders.209 """Request a token from the builder proxy to be used by the builders.
192210
193 See IProxyService.211 See IProxyService.
194 """212 """
195 timestamp = int(time.time())213 timestamp = int(time.time())
196 proxy_username = "{build_id}-{timestamp}".format(214 proxy_username = "{build_id}-{timestamp}".format(
197 build_id=self.build_id, timestamp=timestamp215 build_id=build_id, timestamp=timestamp
198 )216 )
199217
200 token = yield self.worker.process_pool.doWork(218 token = yield self.worker.process_pool.doWork(
@@ -233,7 +251,7 @@ class FetchService(IProxyService):
233 RETRIEVE_METADATA_ENDPOINT = "{control_endpoint}/session/{session_id}"251 RETRIEVE_METADATA_ENDPOINT = "{control_endpoint}/session/{session_id}"
234 END_SESSION_ENDPOINT = "{control_endpoint}/session/{session_id}"252 END_SESSION_ENDPOINT = "{control_endpoint}/session/{session_id}"
235253
236 def __init__(self, build_id: str, worker: BuilderWorker):254 def __init__(self, worker: BuilderWorker):
237 self.control_endpoint = _get_value_from_config(255 self.control_endpoint = _get_value_from_config(
238 "fetch_service_control_endpoint"256 "fetch_service_control_endpoint"
239 )257 )
@@ -242,10 +260,7 @@ class FetchService(IProxyService):
242 port=_get_value_from_config("fetch_service_port"),260 port=_get_value_from_config("fetch_service_port"),
243 )261 )
244 self.auth_header = self._getAuthHeader()262 self.auth_header = self._getAuthHeader()
245
246 self.build_id = build_id
247 self.worker = worker263 self.worker = worker
248 self.session_id = None
249264
250 @staticmethod265 @staticmethod
251 def _getAuthHeader():266 def _getAuthHeader():
@@ -259,7 +274,7 @@ class FetchService(IProxyService):
259 return b"Basic " + base64.b64encode(auth_string.encode("ASCII"))274 return b"Basic " + base64.b64encode(auth_string.encode("ASCII"))
260275
261 @defer.inlineCallbacks276 @defer.inlineCallbacks
262 def startSession(self):277 def startSession(self, build_id: str):
263 """Requests a fetch service session and returns session information.278 """Requests a fetch service session and returns session information.
264279
265 See IProxyService.280 See IProxyService.
@@ -290,8 +305,28 @@ class FetchService(IProxyService):
290 "revocation_endpoint": revocation_endpoint,305 "revocation_endpoint": revocation_endpoint,
291 }306 }
292307
308 def extractSessionIDFromRevocationEndpoint(
309 self, revocation_endpoint: str
310 ) -> Optional[str]:
311 """Helper method to get the session_id out of the revocation
312 endpoint.
313
314 Example `revocation_endpoint`:
315 http://fetch-service.net:9999/session/913890402914ce3be9ffd4d0/token
316
317 Would return: 913890402914ce3be9ffd4d0
318 """
319 re_pattern = self.TOKEN_REVOCATION.format(
320 control_endpoint=self.control_endpoint,
321 session_id="(?P<session_id>.*)",
322 )
323 match = re.match(re_pattern, revocation_endpoint)
324 return match["session_id"] if match else None
325
293 @defer.inlineCallbacks326 @defer.inlineCallbacks
294 def retrieveMetadataFromSession(self, save_content_to: str):327 def retrieveMetadataFromSession(
328 self, session_id: str, save_content_to: str
329 ):
295 """Make request to retrieve metadata from the current session.330 """Make request to retrieve metadata from the current session.
296331
297 Data is stored directly into a file whose path is `save_content_to`332 Data is stored directly into a file whose path is `save_content_to`
@@ -300,7 +335,7 @@ class FetchService(IProxyService):
300 """335 """
301 url = self.RETRIEVE_METADATA_ENDPOINT.format(336 url = self.RETRIEVE_METADATA_ENDPOINT.format(
302 control_endpoint=self.control_endpoint,337 control_endpoint=self.control_endpoint,
303 session_id=self.session_id,338 session_id=session_id,
304 )339 )
305 yield self.worker.process_pool.doWork(340 yield self.worker.process_pool.doWork(
306 RetrieveFetchServiceSessionCommand,341 RetrieveFetchServiceSessionCommand,
@@ -310,14 +345,14 @@ class FetchService(IProxyService):
310 )345 )
311346
312 @defer.inlineCallbacks347 @defer.inlineCallbacks
313 def endSession(self):348 def endSession(self, session_id: str):
314 """End the proxy session and do any cleanup needed.349 """End the proxy session and do any cleanup needed.
315350
316 :raises: RequestException if request to Fetch Service fails351 :raises: RequestException if request to Fetch Service fails
317 """352 """
318 url = self.END_SESSION_ENDPOINT.format(353 url = self.END_SESSION_ENDPOINT.format(
319 control_endpoint=self.control_endpoint,354 control_endpoint=self.control_endpoint,
320 session_id=self.session_id,355 session_id=session_id,
321 )356 )
322 yield self.worker.process_pool.doWork(357 yield self.worker.process_pool.doWork(
323 EndFetchServiceSessionCommand,358 EndFetchServiceSessionCommand,
diff --git a/lib/lp/buildmaster/interactor.py b/lib/lp/buildmaster/interactor.py
index dfcc86b..9bf1a71 100644
--- a/lib/lp/buildmaster/interactor.py
+++ b/lib/lp/buildmaster/interactor.py
@@ -209,6 +209,10 @@ class BuilderWorker:
209 """Echo the arguments back."""209 """Echo the arguments back."""
210 return self._with_timeout(self._server.callRemote("echo", *args))210 return self._with_timeout(self._server.callRemote("echo", *args))
211211
212 def proxy_info(self):
213 """Return the details for the proxy used by the manager."""
214 return self._with_timeout(self._server.callRemote("proxy_info"))
215
212 def info(self):216 def info(self):
213 """Return the protocol version and the builder methods supported."""217 """Return the protocol version and the builder methods supported."""
214 return self._with_timeout(self._server.callRemote("info"))218 return self._with_timeout(self._server.callRemote("info"))
diff --git a/lib/lp/buildmaster/tests/mock_workers.py b/lib/lp/buildmaster/tests/mock_workers.py
index a880a9a..5f5563b 100644
--- a/lib/lp/buildmaster/tests/mock_workers.py
+++ b/lib/lp/buildmaster/tests/mock_workers.py
@@ -151,6 +151,18 @@ class OkWorker:
151 self.call_log.append("info")151 self.call_log.append("info")
152 return defer.succeed(("1.0", self.arch_tag, "binarypackage"))152 return defer.succeed(("1.0", self.arch_tag, "binarypackage"))
153153
154 def proxy_info(self):
155 self.call_log.append("proxy_info")
156 return defer.succeed(
157 {
158 "revocation_endpoint": (
159 "http://fetch-service.test:9999/session/"
160 "9138904ce3be9ffd4d0/token"
161 ),
162 "use_fetch_service": False,
163 }
164 )
165
154 def resume(self):166 def resume(self):
155 self.call_log.append("resume")167 self.call_log.append("resume")
156 return defer.succeed(("", "", 0))168 return defer.succeed(("", "", 0))
diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
index 81d6926..cb2269b 100644
--- a/lib/lp/snappy/tests/test_snapbuildbehaviour.py
+++ b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
@@ -10,6 +10,7 @@ import time
10import uuid10import uuid
11from datetime import datetime11from datetime import datetime
12from textwrap import dedent12from textwrap import dedent
13from unittest.mock import MagicMock
13from urllib.parse import urlsplit14from urllib.parse import urlsplit
1415
15import fixtures16import fixtures
@@ -40,7 +41,6 @@ from lp.app.enums import InformationType
40from lp.archivepublisher.interfaces.archivegpgsigningkey import (41from lp.archivepublisher.interfaces.archivegpgsigningkey import (
41 IArchiveGPGSigningKey,42 IArchiveGPGSigningKey,
42)43)
43from lp.buildmaster.builderproxy import BuilderProxy
44from lp.buildmaster.enums import BuildBaseImageType, BuildStatus44from lp.buildmaster.enums import BuildBaseImageType, BuildStatus
45from lp.buildmaster.interactor import shut_down_default_process_pool45from lp.buildmaster.interactor import shut_down_default_process_pool
46from lp.buildmaster.interfaces.builder import CannotBuild46from lp.buildmaster.interfaces.builder import CannotBuild
@@ -431,6 +431,20 @@ class TestAsyncSnapBuildBehaviourFetchService(
431 snap = self.factory.makeSnap(use_fetch_service=True)431 snap = self.factory.makeSnap(use_fetch_service=True)
432 request = self.factory.makeSnapBuildRequest(snap=snap)432 request = self.factory.makeSnapBuildRequest(snap=snap)
433 job = self.makeJob(snap=snap, build_request=request)433 job = self.makeJob(snap=snap, build_request=request)
434
435 host = config.builddmaster.fetch_service_host
436 port = config.builddmaster.fetch_service_port
437 session_id = self.fetch_service_api.sessions.session_id
438 revocation_endpoint = (
439 f"http://{host}:{port}/session/{session_id}/token"
440 )
441
442 job._worker.proxy_info = MagicMock(
443 return_value={
444 "revocation_endpoint": revocation_endpoint,
445 "use_fetch_service": True,
446 }
447 )
434 yield job.extraBuildArgs()448 yield job.extraBuildArgs()
435449
436 # End the session450 # End the session
@@ -443,7 +457,6 @@ class TestAsyncSnapBuildBehaviourFetchService(
443 start_session_request = self.fetch_service_api.sessions.requests[0]457 start_session_request = self.fetch_service_api.sessions.requests[0]
444 self.assertEqual(b"POST", start_session_request["method"])458 self.assertEqual(b"POST", start_session_request["method"])
445 self.assertEqual(b"/session", start_session_request["uri"])459 self.assertEqual(b"/session", start_session_request["uri"])
446 session_id = self.fetch_service_api.sessions.responses[0]["id"]
447460
448 # Request retrieve metadata461 # Request retrieve metadata
449 retrieve_metadata_request = self.fetch_service_api.sessions.requests[1]462 retrieve_metadata_request = self.fetch_service_api.sessions.requests[1]
@@ -478,6 +491,14 @@ class TestAsyncSnapBuildBehaviourFetchService(
478 snap = self.factory.makeSnap(use_fetch_service=False)491 snap = self.factory.makeSnap(use_fetch_service=False)
479 request = self.factory.makeSnapBuildRequest(snap=snap)492 request = self.factory.makeSnapBuildRequest(snap=snap)
480 job = self.makeJob(snap=snap, build_request=request)493 job = self.makeJob(snap=snap, build_request=request)
494
495 job._worker.proxy_info = MagicMock(
496 return_value={
497 "revocation_endpoint": "https://builder-proxy.test/revoke",
498 "use_fetch_service": False,
499 }
500 )
501
481 yield job.extraBuildArgs()502 yield job.extraBuildArgs()
482 yield job.endProxySession(upload_path="test_path")503 yield job.endProxySession(upload_path="test_path")
483504
@@ -485,23 +506,33 @@ class TestAsyncSnapBuildBehaviourFetchService(
485 self.assertEqual(0, len(self.fetch_service_api.sessions.requests))506 self.assertEqual(0, len(self.fetch_service_api.sessions.requests))
486507
487 @defer.inlineCallbacks508 @defer.inlineCallbacks
488 def test_endProxySession_no_proxy_service(self):509 def test_endProxySession_fetch_Service_allow_internet_false(self):
489 """When the `fetch_service_host` is not set, the calls to the fetch510 """When `allow_internet` is False, we don't send proxy variables to the
490 service don't go through."""511 buildd, and ending the session does not make calls to the fetch
512 service."""
491 self.useFixture(513 self.useFixture(
492 FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: "on"})514 FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: "on"})
493 )515 )
494 self.useFixture(FeatureFixture({"fetch_service_host": None}))516 snap = self.factory.makeSnap(allow_internet=False)
495
496 snap = self.factory.makeSnap(use_fetch_service=True)
497 request = self.factory.makeSnapBuildRequest(snap=snap)517 request = self.factory.makeSnapBuildRequest(snap=snap)
498 job = self.makeJob(snap=snap, build_request=request)518 job = self.makeJob(snap=snap, build_request=request)
499 yield job.extraBuildArgs()519 args = yield job.extraBuildArgs()
520
521 # Scenario when we don't allow internet
522 job._worker.proxy_info = MagicMock(
523 return_value={
524 "revocation_endpoint": None,
525 "use_fetch_service": None,
526 }
527 )
528
500 yield job.endProxySession(upload_path="test_path")529 yield job.endProxySession(upload_path="test_path")
501530
531 # No proxy config sent to buildd
532 self.assertIsNone(args.get("use_fetch_service"))
533 self.assertIsNone(args.get("revocation_endpoint"))
502 # No calls go through to the fetch service534 # No calls go through to the fetch service
503 self.assertEqual(0, len(self.fetch_service_api.sessions.requests))535 self.assertEqual(0, len(self.fetch_service_api.sessions.requests))
504 self.assertEqual(None, job._proxy_service)
505536
506537
507class TestAsyncSnapBuildBehaviourBuilderProxy(538class TestAsyncSnapBuildBehaviourBuilderProxy(
@@ -1470,8 +1501,6 @@ class TestAsyncSnapBuildBehaviourBuilderProxy(
14701501
1471 # End the session1502 # End the session
1472 yield job.endProxySession(upload_path="test_path")1503 yield job.endProxySession(upload_path="test_path")
1473 self.assertFalse(job.use_fetch_service)
1474 self.assertTrue(isinstance(job.proxy_service, BuilderProxy))
14751504
1476 @defer.inlineCallbacks1505 @defer.inlineCallbacks
1477 def test_composeBuildRequest_proxy_url_set(self):1506 def test_composeBuildRequest_proxy_url_set(self):

Subscribers

People subscribed via source and target branches

to status/vote changes: