Merge ~ines-almeida/launchpad:fetch-service-buildd-manager-refactor into launchpad:master

Proposed by Ines Almeida
Status: Merged
Approved by: Ines Almeida
Approved revision: 9dc14ea9f83ff47c4356e4764090dd6d06151282
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~ines-almeida/launchpad:fetch-service-buildd-manager-refactor
Merge into: launchpad:master
Diff against target: 279 lines (+166/-50)
1 file modified
lib/lp/buildmaster/builderproxy.py (+166/-50)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+464303@code.launchpad.net

Commit message

Refactor buildd-manager builder proxy logic

This separates the logic for making calls to the builder proxy and the fetch service out of the BuilderProxyMixin and into their own handlers.
This will make it easier to later add the logic to end sessions or, retrieve metadata.

Description of the change

All tests in `lp.snappy.tests` have be run and passed successfully.

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

The code itself should be tested from the existing unit tests, since the functionality was maintained. But I still need to have a look at adding new tests for the new classes.

Revision history for this message
Jürgen Gmach (jugmac00) wrote :

That is a much cleaner approach - I like it a lot.

It is a pity that we do not have a coverage report, as I would be absolutely happy with no further unit tests as the functionality "should" be already 100% covered.

mypy is not happy yet

Revision history for this message
Ines Almeida (ines-almeida) wrote :

Fixed all mypy issues (interestingly, some of them were issues that already existed in the code previously, but weren't an issue before), and added comments where it was relevant.

I will refrain from adding unit tests in this MP since the functionality seems to be highly tested between the multiple tests that contain `extraBuildArgs` and all the tests within `lp.snappy.tests.test_snapbuildbehaviour`.

Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Thanks a lot for the MP!

- typo in the commit message
retreieve -> retrieve

This is a great improvement for readability and maintainability!

review: Approve
Revision history for this message
Ines Almeida (ines-almeida) wrote :

Thanks for the review! I'll update the commit when I squash them

9dc14ea... by Ines Almeida

Refactor buildd-manager builder proxy logic

This separates the logic for making calls to the builder proxy and the fetch service out of the BuilderProxyMixin and into their own handlers.
This will make it easier to later add the logic to end sessions or, retrieve metadata.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/buildmaster/builderproxy.py b/lib/lp/buildmaster/builderproxy.py
2index 5f3cf5b..a2b8f71 100644
3--- a/lib/lp/buildmaster/builderproxy.py
4+++ b/lib/lp/buildmaster/builderproxy.py
5@@ -16,7 +16,7 @@ __all__ = [
6
7 import base64
8 import time
9-from typing import Dict, Generator
10+from typing import Dict, Generator, Type
11
12 from twisted.internet import defer
13
14@@ -24,8 +24,10 @@ from lp.buildmaster.downloader import (
15 RequestFetchServiceSessionCommand,
16 RequestProxyTokenCommand,
17 )
18+from lp.buildmaster.interactor import BuilderWorker
19 from lp.buildmaster.interfaces.builder import CannotBuild
20 from lp.buildmaster.interfaces.buildfarmjobbehaviour import BuildArgs
21+from lp.buildmaster.model.buildfarmjob import BuildFarmJob
22 from lp.services.config import config
23
24
25@@ -44,43 +46,27 @@ def _get_value_from_config(key: str):
26 class BuilderProxyMixin:
27 """Methods for handling builds with the Snap Build Proxy enabled."""
28
29+ # Variables that are set up by the class that uses the Mixin
30+ build: BuildFarmJob
31+ _worker: BuilderWorker
32+
33 @defer.inlineCallbacks
34 def addProxyArgs(
35 self,
36 args: BuildArgs,
37 allow_internet: bool = True,
38- fetch_service: bool = False,
39+ use_fetch_service: bool = False,
40 ) -> Generator[None, Dict[str, str], None]:
41+
42+ self.proxy_service = None
43+
44 if not allow_internet:
45 return
46- if not fetch_service and _get_proxy_config("builder_proxy_host"):
47- token = yield self._requestProxyToken()
48- args["proxy_url"] = (
49- "http://{username}:{password}@{host}:{port}".format(
50- username=token["username"],
51- password=token["secret"],
52- host=_get_proxy_config("builder_proxy_host"),
53- port=_get_proxy_config("builder_proxy_port"),
54- )
55- )
56- args["revocation_endpoint"] = "{endpoint}/{token}".format(
57- endpoint=_get_proxy_config("builder_proxy_auth_api_endpoint"),
58- token=token["username"],
59- )
60- elif fetch_service and _get_proxy_config("fetch_service_host"):
61- session = yield self._requestFetchServiceSession()
62- args["proxy_url"] = (
63- "http://{session_id}:{token}@{host}:{port}".format(
64- session_id=session["id"],
65- token=session["token"],
66- host=_get_proxy_config("fetch_service_host"),
67- port=_get_proxy_config("fetch_service_port"),
68- )
69- )
70- args["revocation_endpoint"] = "{endpoint}/{session_id}".format(
71- endpoint=_get_proxy_config("fetch_service_control_endpoint"),
72- session_id=session["id"],
73- )
74+
75+ if not use_fetch_service and _get_proxy_config("builder_proxy_host"):
76+ proxy_service: Type[IProxyService] = BuilderProxy
77+ elif use_fetch_service and _get_proxy_config("fetch_service_host"):
78+ proxy_service = FetchService
79
80 # Append the fetch-service certificate to BuildArgs secrets.
81 if "secrets" not in args:
82@@ -88,47 +74,177 @@ class BuilderProxyMixin:
83 args["secrets"]["fetch_service_mitm_certificate"] = (
84 _get_value_from_config("fetch_service_mitm_certificate")
85 )
86+ else:
87+ # If the required config values aren't set, we skip adding proxy
88+ # args or starting a proxy session. This might be relevant for
89+ # non-production environments.
90+ return
91+
92+ self.proxy_service = proxy_service(
93+ build_id=self.build.build_cookie, worker=self._worker
94+ )
95+ new_session = yield self.proxy_service.startSession()
96+ args["proxy_url"] = new_session["proxy_url"]
97+ args["revocation_endpoint"] = new_session["revocation_endpoint"]
98+
99+
100+class IProxyService:
101+ """Interface for Proxy Services - either FetchService or BuilderProxy."""
102+
103+ def __init__(self, build_id: str, worker: BuilderWorker):
104+ pass
105
106 @defer.inlineCallbacks
107- def _requestProxyToken(self):
108+ def startSession(self):
109+ """Start a proxy session and request required
110+
111+ :returns: dictionary with an authenticated `proxy_url` for the builder
112+ to use, and a `revocation_endpoint` to revoke the token when it's no
113+ longer required.
114+ """
115+ pass
116+
117+
118+class BuilderProxy(IProxyService):
119+ """Handler for the Builder Proxy.
120+
121+ Handles the life-cycle of the proxy session used by a builder by
122+ making API requests directly to the builder proxy control endpoint.
123+ """
124+
125+ def __init__(self, build_id: str, worker: BuilderWorker):
126+ self.control_endpoint = _get_value_from_config(
127+ "builder_proxy_auth_api_endpoint"
128+ )
129+ self.proxy_endpoint = "{host}:{port}".format(
130+ host=_get_value_from_config("builder_proxy_host"),
131+ port=_get_value_from_config("builder_proxy_port"),
132+ )
133+ self.auth_header = self._getAuthHeader()
134+
135+ self.build_id = build_id
136+ self.worker = worker
137+
138+ @staticmethod
139+ def _getAuthHeader():
140+ """Helper method to generate authentication needed to call the
141+ builder proxy authentication endpoint."""
142+
143 admin_username = _get_value_from_config(
144 "builder_proxy_auth_api_admin_username"
145 )
146- secret = _get_value_from_config("builder_proxy_auth_api_admin_secret")
147- url = _get_value_from_config("builder_proxy_auth_api_endpoint")
148+ admin_secret = _get_value_from_config(
149+ "builder_proxy_auth_api_admin_secret"
150+ )
151+ auth_string = f"{admin_username}:{admin_secret}".strip()
152+ return b"Basic " + base64.b64encode(auth_string.encode("ASCII"))
153+
154+ @defer.inlineCallbacks
155+ def startSession(self):
156+ """Request a token from the builder proxy to be used by the builders.
157+
158+ See IProxyService.
159+ """
160 timestamp = int(time.time())
161 proxy_username = "{build_id}-{timestamp}".format(
162- build_id=self.build.build_cookie, timestamp=timestamp
163+ build_id=self.build_id, timestamp=timestamp
164 )
165- auth_string = f"{admin_username}:{secret}".strip()
166- auth_header = b"Basic " + base64.b64encode(auth_string.encode("ASCII"))
167
168- token = yield self._worker.process_pool.doWork(
169+ token = yield self.worker.process_pool.doWork(
170 RequestProxyTokenCommand,
171- url=url,
172- auth_header=auth_header,
173+ url=self.control_endpoint,
174+ auth_header=self.auth_header,
175 proxy_username=proxy_username,
176 )
177- return token
178
179- @defer.inlineCallbacks
180- def _requestFetchServiceSession(self):
181+ proxy_url = "http://{username}:{password}@{proxy_endpoint}".format(
182+ username=token["username"],
183+ password=token["secret"],
184+ proxy_endpoint=self.proxy_endpoint,
185+ )
186+ revocation_endpoint = "{endpoint}/{token}".format(
187+ endpoint=self.control_endpoint,
188+ token=token["username"],
189+ )
190+
191+ return {
192+ "proxy_url": proxy_url,
193+ "revocation_endpoint": revocation_endpoint,
194+ }
195+
196+
197+class FetchService(IProxyService):
198+ """Handler for the Fetch Service.
199+
200+ Handles the life-cycle of the fetch service session used by a builder by
201+ making API requests directly to the fetch service control endpoint.
202+ """
203+
204+ PROXY_URL = "http://{session_id}:{token}@{proxy_endpoint}"
205+ START_SESSION_ENDPOINT = "{control_endpoint}"
206+ TOKEN_REVOCATION = "{control_endpoint}/session/{session_id}/token"
207+ RETRIEVE_METADATA_ENDPOINT = "{control_endpoint}/session/{session_id}"
208+ END_SESSION_ENDPOINT = "{control_endpoint}/session/{session_id}"
209+
210+ def __init__(self, build_id: str, worker: BuilderWorker):
211+ self.control_endpoint = _get_value_from_config(
212+ "fetch_service_control_endpoint"
213+ )
214+ self.proxy_endpoint = "{host}:{port}".format(
215+ host=_get_value_from_config("fetch_service_host"),
216+ port=_get_value_from_config("fetch_service_port"),
217+ )
218+ self.auth_header = self._getAuthHeader()
219+
220+ self.build_id = build_id
221+ self.worker = worker
222+ self.session_id = None
223+
224+ @staticmethod
225+ def _getAuthHeader():
226+ """Helper method to generate authentication needed to call the
227+ fetch service control endpoint."""
228 admin_username = _get_value_from_config(
229 "fetch_service_control_admin_username"
230 )
231 secret = _get_value_from_config("fetch_service_control_admin_secret")
232- url = _get_value_from_config("fetch_service_control_endpoint")
233+ auth_string = f"{admin_username}:{secret}".strip()
234+ return b"Basic " + base64.b64encode(auth_string.encode("ASCII"))
235+
236+ @defer.inlineCallbacks
237+ def startSession(self):
238+ """Requests a fetch service session and returns session information.
239+
240+ See IProxyService.
241+ """
242 timestamp = int(time.time())
243 proxy_username = "{build_id}-{timestamp}".format(
244- build_id=self.build.build_cookie, timestamp=timestamp
245+ build_id=self.build_id, timestamp=timestamp
246 )
247- auth_string = f"{admin_username}:{secret}".strip()
248- auth_header = b"Basic " + base64.b64encode(auth_string.encode("ASCII"))
249
250- session = yield self._worker.process_pool.doWork(
251+ session_data = yield self.worker.process_pool.doWork(
252 RequestFetchServiceSessionCommand,
253- url=url,
254- auth_header=auth_header,
255+ url=self.START_SESSION_ENDPOINT.format(
256+ control_endpoint=self.control_endpoint
257+ ),
258+ auth_header=self.auth_header,
259 proxy_username=proxy_username,
260 )
261- return session
262+
263+ self.session_id = session_data["id"]
264+ token = session_data["token"]
265+
266+ proxy_url = self.PROXY_URL.format(
267+ session_id=self.session_id,
268+ token=token,
269+ proxy_endpoint=self.proxy_endpoint,
270+ )
271+ revocation_endpoint = self.TOKEN_REVOCATION.format(
272+ control_endpoint=self.control_endpoint,
273+ session_id=self.session_id,
274+ )
275+
276+ return {
277+ "proxy_url": proxy_url,
278+ "revocation_endpoint": revocation_endpoint,
279+ }

Subscribers

People subscribed via source and target branches

to status/vote changes: