Merge ~pelpsi/launchpad:fetch-service-session-API into launchpad:master
- Git
- lp:~pelpsi/launchpad
- fetch-service-session-API
- Merge into master
Status: | Merged |
---|---|
Approved by: | Simone Pelosi |
Approved revision: | 8862e2cafd4efe19fea09311993f419bd1994594 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pelpsi/launchpad:fetch-service-session-API |
Merge into: | launchpad:master |
Prerequisite: | ~ines-almeida/launchpad:fetch-service-option-model-update |
Diff against target: |
544 lines (+422/-5) 6 files modified
lib/lp/buildmaster/builderproxy.py (+54/-3) lib/lp/buildmaster/downloader.py (+44/-0) lib/lp/buildmaster/tests/fetchservice.py (+124/-0) lib/lp/services/config/schema-lazr.conf (+36/-0) lib/lp/snappy/model/snapbuildbehaviour.py (+3/-1) lib/lp/snappy/tests/test_snapbuildbehaviour.py (+161/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jürgen Gmach | Approve | ||
Review via email: mp+461721@code.launchpad.net |
Commit message
Add fetch service session API
Update BuilderProxyMixin class to switch between
Fetch Service and classic Builder Proxy depending on
use_fetch_service flag.
Add test suite for Fetch Service and fetchservice.py mock class
for API.
Add Fetch Service API information on schema-lazr.conf.
Description of the change
Ines Almeida (ines-almeida) wrote : | # |
Simone Pelosi (pelpsi) : | # |
Simone Pelosi (pelpsi) wrote : | # |
Thank you for the review Ines, I'm so sorry I forgot to push the commit in which I removed all the debugging stuff or useless comments :P
Ines Almeida (ines-almeida) : | # |
Simone Pelosi (pelpsi) : | # |
Jürgen Gmach (jugmac00) wrote : | # |
Thanks a lot for the MP! We are on the right track!
As we went with the approach to handle both cases inside a single mixin, we really, really want to have a good class docstring to explain that there are two proxies involved.
Also we should add a comment or a paragraph in the docstring which describes the way forward, that at one time hopefully the fetch service will replace the builder proxy.
Also, we need to be careful with naming - do not copy paste naming from the builder proxy. We should align this with what the starcraft folks are using.
Jürgen Gmach (jugmac00) wrote : | # |
Thanks! Looks good now!
I would ask you to revert the refactoring when reading the configuration, or at very least put the refactoring and the new features in two separate commits.
Also, I left again the question where "FetchServiceUR
Simone Pelosi (pelpsi) : | # |
Jürgen Gmach (jugmac00) wrote : | # |
Looks good now - please at least separate the MP into a refactoring commit and one implementing the new feature.
Simone Pelosi (pelpsi) : | # |
Preview Diff
1 | diff --git a/lib/lp/buildmaster/builderproxy.py b/lib/lp/buildmaster/builderproxy.py |
2 | index be65e29..1ac121f 100644 |
3 | --- a/lib/lp/buildmaster/builderproxy.py |
4 | +++ b/lib/lp/buildmaster/builderproxy.py |
5 | @@ -20,7 +20,10 @@ from typing import Dict, Generator |
6 | |
7 | from twisted.internet import defer |
8 | |
9 | -from lp.buildmaster.downloader import RequestProxyTokenCommand |
10 | +from lp.buildmaster.downloader import ( |
11 | + RequestFetchServiceSessionCommand, |
12 | + RequestProxyTokenCommand, |
13 | +) |
14 | from lp.buildmaster.interfaces.builder import CannotBuild |
15 | from lp.buildmaster.interfaces.buildfarmjobbehaviour import BuildArgs |
16 | from lp.services.config import config |
17 | @@ -31,14 +34,26 @@ def _get_proxy_config(name): |
18 | return getattr(config.builddmaster, name) or getattr(config.snappy, name) |
19 | |
20 | |
21 | +def _get_value_from_config(key: str): |
22 | + value = _get_proxy_config(key) |
23 | + if not value: |
24 | + raise CannotBuild(f"{key} is not configured.") |
25 | + return value |
26 | + |
27 | + |
28 | class BuilderProxyMixin: |
29 | """Methods for handling builds with the Snap Build Proxy enabled.""" |
30 | |
31 | @defer.inlineCallbacks |
32 | def addProxyArgs( |
33 | - self, args: BuildArgs, allow_internet: bool = True |
34 | + self, |
35 | + args: BuildArgs, |
36 | + allow_internet: bool = True, |
37 | + fetch_service: bool = False, |
38 | ) -> Generator[None, Dict[str, str], None]: |
39 | - if _get_proxy_config("builder_proxy_host") and allow_internet: |
40 | + if not allow_internet: |
41 | + return |
42 | + if not fetch_service and _get_proxy_config("builder_proxy_host"): |
43 | token = yield self._requestProxyToken() |
44 | args["proxy_url"] = ( |
45 | "http://{username}:{password}@{host}:{port}".format( |
46 | @@ -52,6 +67,20 @@ class BuilderProxyMixin: |
47 | endpoint=_get_proxy_config("builder_proxy_auth_api_endpoint"), |
48 | token=token["username"], |
49 | ) |
50 | + elif fetch_service and _get_proxy_config("fetch_service_host"): |
51 | + session = yield self._requestFetchServiceSession() |
52 | + args["proxy_url"] = ( |
53 | + "http://{session_id}:{token}@{host}:{port}".format( |
54 | + session_id=session["id"], |
55 | + token=session["token"], |
56 | + host=_get_proxy_config("fetch_service_host"), |
57 | + port=_get_proxy_config("fetch_service_port"), |
58 | + ) |
59 | + ) |
60 | + args["revocation_endpoint"] = "{endpoint}/session/{id}".format( |
61 | + endpoint=_get_proxy_config("fetch_service_control_endpoint"), |
62 | + id=session["id"], |
63 | + ) |
64 | |
65 | @defer.inlineCallbacks |
66 | def _requestProxyToken(self): |
67 | @@ -86,3 +115,25 @@ class BuilderProxyMixin: |
68 | proxy_username=proxy_username, |
69 | ) |
70 | return token |
71 | + |
72 | + @defer.inlineCallbacks |
73 | + def _requestFetchServiceSession(self): |
74 | + admin_username = _get_value_from_config( |
75 | + "fetch_service_control_admin_username" |
76 | + ) |
77 | + secret = _get_value_from_config("fetch_service_control_admin_secret") |
78 | + url = _get_value_from_config("fetch_service_control_endpoint") |
79 | + timestamp = int(time.time()) |
80 | + proxy_username = "{build_id}-{timestamp}".format( |
81 | + build_id=self.build.build_cookie, timestamp=timestamp |
82 | + ) |
83 | + auth_string = f"{admin_username}:{secret}".strip() |
84 | + auth_header = b"Basic " + base64.b64encode(auth_string.encode("ASCII")) |
85 | + |
86 | + session = yield self._worker.process_pool.doWork( |
87 | + RequestFetchServiceSessionCommand, |
88 | + url=url, |
89 | + auth_header=auth_header, |
90 | + proxy_username=proxy_username, |
91 | + ) |
92 | + return session |
93 | diff --git a/lib/lp/buildmaster/downloader.py b/lib/lp/buildmaster/downloader.py |
94 | index fc16852..7cf1c9b 100644 |
95 | --- a/lib/lp/buildmaster/downloader.py |
96 | +++ b/lib/lp/buildmaster/downloader.py |
97 | @@ -9,6 +9,7 @@ anything from the rest of Launchpad. |
98 | |
99 | __all__ = [ |
100 | "DownloadCommand", |
101 | + "RequestFetchServiceSessionCommand", |
102 | "RequestProcess", |
103 | "RequestProxyTokenCommand", |
104 | ] |
105 | @@ -37,6 +38,28 @@ class DownloadCommand(amp.Command): |
106 | } |
107 | |
108 | |
109 | +class RequestFetchServiceSessionCommand(amp.Command): |
110 | + """Fetch service API Command subclass |
111 | + |
112 | + It defines arguments, response values, and error conditions. |
113 | + For reference: |
114 | + https://docs.twisted.org/en/twisted-18.7.0/core/howto/amp.html |
115 | + """ |
116 | + |
117 | + arguments = [ |
118 | + (b"url", amp.Unicode()), |
119 | + (b"auth_header", amp.String()), |
120 | + (b"proxy_username", amp.Unicode()), |
121 | + ] |
122 | + response = [ |
123 | + (b"id", amp.Unicode()), |
124 | + (b"token", amp.Unicode()), |
125 | + ] |
126 | + errors = { |
127 | + RequestException: b"REQUEST_ERROR", |
128 | + } |
129 | + |
130 | + |
131 | class RequestProxyTokenCommand(amp.Command): |
132 | arguments = [ |
133 | (b"url", amp.Unicode()), |
134 | @@ -94,3 +117,24 @@ class RequestProcess(AMPChild): |
135 | ) |
136 | response.raise_for_status() |
137 | return response.json() |
138 | + |
139 | + @RequestFetchServiceSessionCommand.responder |
140 | + def requestFetchServiceSessionCommand( |
141 | + self, url, auth_header, proxy_username |
142 | + ): |
143 | + with Session() as session: |
144 | + session.trust_env = False |
145 | + # XXX pelpsi: from ST108 and from what Claudio |
146 | + # said `timeout` and `policy` are not mandatory now: |
147 | + # `timeout` will never be mandatory and we don't pass |
148 | + # it as parameter to the call. |
149 | + # `policy` could be mandatory or optional in future |
150 | + # (assuming `strict` as default), so for now it's better |
151 | + # to pass it explicitly and set it as `permissive`. |
152 | + response = session.post( |
153 | + url, |
154 | + headers={"Authorization": auth_header}, |
155 | + json={"username": proxy_username, "policy": "permissive"}, |
156 | + ) |
157 | + response.raise_for_status() |
158 | + return response.json() |
159 | diff --git a/lib/lp/buildmaster/tests/fetchservice.py b/lib/lp/buildmaster/tests/fetchservice.py |
160 | new file mode 100644 |
161 | index 0000000..ea70651 |
162 | --- /dev/null |
163 | +++ b/lib/lp/buildmaster/tests/fetchservice.py |
164 | @@ -0,0 +1,124 @@ |
165 | +# Copyright 2015-2024 Canonical Ltd. This software is licensed under the |
166 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
167 | + |
168 | +"""Fixtures for dealing with the build time Fetch Service http proxy.""" |
169 | + |
170 | +import json |
171 | +import uuid |
172 | +from textwrap import dedent |
173 | +from urllib.parse import urlsplit |
174 | + |
175 | +import fixtures |
176 | +from testtools.matchers import Equals, HasLength, MatchesStructure |
177 | +from twisted.internet import defer, endpoints, reactor |
178 | +from twisted.python.compat import nativeString |
179 | +from twisted.web import resource, server |
180 | + |
181 | +from lp.services.config import config |
182 | + |
183 | + |
184 | +class FetchServiceAuthAPITokensResource(resource.Resource): |
185 | + """A test session resource for the fetch service authentication API.""" |
186 | + |
187 | + isLeaf = True |
188 | + |
189 | + def __init__(self): |
190 | + resource.Resource.__init__(self) |
191 | + self.requests = [] |
192 | + |
193 | + def render_POST(self, request): |
194 | + content = json.loads(request.content.read().decode("UTF-8")) |
195 | + self.requests.append( |
196 | + { |
197 | + "method": request.method, |
198 | + "uri": request.uri, |
199 | + "headers": dict(request.requestHeaders.getAllRawHeaders()), |
200 | + "json": content, |
201 | + } |
202 | + ) |
203 | + return json.dumps( |
204 | + { |
205 | + "id": "1", |
206 | + "token": uuid.uuid4().hex, |
207 | + } |
208 | + ).encode("UTF-8") |
209 | + |
210 | + |
211 | +class InProcessFetchServiceAuthAPIFixture(fixtures.Fixture): |
212 | + """A fixture that mocks the Fetch Service authentication API. |
213 | + |
214 | + Users of this fixture must call the `start` method, which returns a |
215 | + `Deferred`, and arrange for that to get back to the reactor. This is |
216 | + necessary because the basic fixture API does not allow `setUp` to return |
217 | + anything. For example: |
218 | + |
219 | + class TestSomething(TestCase): |
220 | + |
221 | + run_tests_with = AsynchronousDeferredRunTest.make_factory( |
222 | + timeout=30) |
223 | + |
224 | + @defer.inlineCallbacks |
225 | + def setUp(self): |
226 | + super().setUp() |
227 | + yield self.useFixture( |
228 | + InProcessFetchServiceAuthAPIFixture() |
229 | + ).start() |
230 | + """ |
231 | + |
232 | + @defer.inlineCallbacks |
233 | + def start(self): |
234 | + root = resource.Resource() |
235 | + self.sessions = FetchServiceAuthAPITokensResource() |
236 | + root.putChild(b"sessions", self.sessions) |
237 | + endpoint = endpoints.serverFromString(reactor, nativeString("tcp:0")) |
238 | + site = server.Site(self.sessions) |
239 | + self.addCleanup(site.stopFactory) |
240 | + port = yield endpoint.listen(site) |
241 | + self.addCleanup(port.stopListening) |
242 | + config.push( |
243 | + "in-process-fetch-service-api-fixture", |
244 | + dedent( |
245 | + """ |
246 | + [builddmaster] |
247 | + fetch_service_control_admin_secret: admin-secret |
248 | + fetch_service_control_admin_username: admin-launchpad.test |
249 | + fetch_service_control_endpoint: http://{host}:{port}/session |
250 | + fetch_service_host: {host} |
251 | + fetch_service_port: {port} |
252 | + """ |
253 | + ).format(host=port.getHost().host, port=port.getHost().port), |
254 | + ) |
255 | + self.addCleanup(config.pop, "in-process-fetch-service-api-fixture") |
256 | + |
257 | + |
258 | +# This class is not used yet but it could be |
259 | +# useful for future tests |
260 | +class FetchServiceURLMatcher(MatchesStructure): |
261 | + """Check that a string is a valid url for fetch service.""" |
262 | + |
263 | + def __init__(self): |
264 | + super().__init__( |
265 | + scheme=Equals("http"), |
266 | + id=Equals(1), |
267 | + token=HasLength(32), |
268 | + hostname=Equals(config.builddmaster.fetch_service_host), |
269 | + port=Equals(config.builddmaster.fetch_service_port), |
270 | + path=Equals(""), |
271 | + ) |
272 | + |
273 | + def match(self, matchee): |
274 | + super().match(urlsplit(matchee)) |
275 | + |
276 | + |
277 | +class RevocationEndpointMatcher(Equals): |
278 | + """Check that a string is a valid endpoint for fetch |
279 | + service session revocation. |
280 | + """ |
281 | + |
282 | + def __init__(self, session_id): |
283 | + super().__init__( |
284 | + "{endpoint}/session/{session_id}".format( |
285 | + endpoint=config.builddmaster.fetch_service_control_endpoint, |
286 | + session_id=session_id, |
287 | + ) |
288 | + ) |
289 | diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf |
290 | index ac68bd4..bbfe8b8 100644 |
291 | --- a/lib/lp/services/config/schema-lazr.conf |
292 | +++ b/lib/lp/services/config/schema-lazr.conf |
293 | @@ -167,6 +167,24 @@ builder_proxy_host: none |
294 | # Builder http proxy port |
295 | builder_proxy_port: none |
296 | |
297 | +# Admin secret for requesting sessions from the fetch service. |
298 | +# For a mojo deployed proxy service, the secret is generated and will reside in |
299 | +# /srv/mojo/${MOJO_PROJECT}/${SERIES}/${MOJO_WORKSPACE}/local/admin-api-secret |
300 | +fetch_service_control_admin_secret: none |
301 | + |
302 | +# Admin username for fetch service. |
303 | +fetch_service_control_admin_username: none |
304 | + |
305 | +# Endpoint for fetch service authentication service |
306 | +fetch_service_control_endpoint: none |
307 | + |
308 | +# Fetch service host, it could be either a single instance |
309 | +# or a load balancer in front |
310 | +fetch_service_host: none |
311 | + |
312 | +# Fetch service port |
313 | +fetch_service_port: none |
314 | + |
315 | [canonical] |
316 | # datatype: boolean |
317 | show_tracebacks: False |
318 | @@ -1854,6 +1872,24 @@ builder_proxy_host: none |
319 | # Deprecated in favour of the same key in the builddmaster section. |
320 | builder_proxy_port: none |
321 | |
322 | +# Admin secret for requesting sessions from the fetch service. |
323 | +# For a mojo deployed proxy service, the secret is generated and will reside in |
324 | +# /srv/mojo/${MOJO_PROJECT}/${SERIES}/${MOJO_WORKSPACE}/local/admin-api-secret |
325 | +fetch_service_control_admin_secret: none |
326 | + |
327 | +# Admin username for fetch service. |
328 | +fetch_service_control_admin_username: none |
329 | + |
330 | +# Endpoint for fetch service authentication service |
331 | +fetch_service_control_endpoint: none |
332 | + |
333 | +# Fetch service host, it could be either a single instance |
334 | +# or a load balancer in front |
335 | +fetch_service_host: none |
336 | + |
337 | +# Fetch service port |
338 | +fetch_service_port: none |
339 | + |
340 | # Optional sources.list entry to send to build workers when doing snap |
341 | # package builds. Use this form so that the series is set: |
342 | # deb http://foo %(series)s main |
343 | diff --git a/lib/lp/snappy/model/snapbuildbehaviour.py b/lib/lp/snappy/model/snapbuildbehaviour.py |
344 | index 66ae7cc..58fdd5f 100644 |
345 | --- a/lib/lp/snappy/model/snapbuildbehaviour.py |
346 | +++ b/lib/lp/snappy/model/snapbuildbehaviour.py |
347 | @@ -116,7 +116,9 @@ class SnapBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase): |
348 | """ |
349 | build: ISnapBuild = self.build |
350 | args: BuildArgs = yield super().extraBuildArgs(logger=logger) |
351 | - yield self.addProxyArgs(args, build.snap.allow_internet) |
352 | + yield self.addProxyArgs( |
353 | + args, build.snap.allow_internet, build.snap.use_fetch_service |
354 | + ) |
355 | args["name"] = build.snap.store_name or build.snap.name |
356 | channels = build.channels or {} |
357 | if "snapcraft" not in channels: |
358 | diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py |
359 | index 5ec5804..2ccc18d 100644 |
360 | --- a/lib/lp/snappy/tests/test_snapbuildbehaviour.py |
361 | +++ b/lib/lp/snappy/tests/test_snapbuildbehaviour.py |
362 | @@ -51,6 +51,9 @@ from lp.buildmaster.tests.builderproxy import ( |
363 | ProxyURLMatcher, |
364 | RevocationEndpointMatcher, |
365 | ) |
366 | +from lp.buildmaster.tests.fetchservice import ( |
367 | + InProcessFetchServiceAuthAPIFixture, |
368 | +) |
369 | from lp.buildmaster.tests.mock_workers import ( |
370 | MockBuilder, |
371 | OkWorker, |
372 | @@ -72,6 +75,7 @@ from lp.services.statsd.tests import StatsMixin |
373 | from lp.services.webapp import canonical_url |
374 | from lp.snappy.interfaces.snap import ( |
375 | SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG, |
376 | + SNAP_USE_FETCH_SERVICE_FEATURE_FLAG, |
377 | SnapBuildArchiveOwnerMismatch, |
378 | ) |
379 | from lp.snappy.model.snapbuildbehaviour import ( |
380 | @@ -235,7 +239,163 @@ class TestSnapBuildBehaviour(TestSnapBuildBehaviourBase): |
381 | self.assertIn("Missing chroot", str(e)) |
382 | |
383 | |
384 | -class TestAsyncSnapBuildBehaviour(StatsMixin, TestSnapBuildBehaviourBase): |
385 | +class TestAsyncSnapBuildBehaviourFetchService( |
386 | + StatsMixin, TestSnapBuildBehaviourBase |
387 | +): |
388 | + run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory( |
389 | + timeout=30 |
390 | + ) |
391 | + |
392 | + @defer.inlineCallbacks |
393 | + def setUp(self): |
394 | + super().setUp() |
395 | + self.session = { |
396 | + "id": "1", |
397 | + "token": uuid.uuid4().hex, |
398 | + } |
399 | + self.fetch_service_url = ( |
400 | + "http://{session_id}:{token}@{host}:{port}".format( |
401 | + session_id=self.session["id"], |
402 | + token=self.session["token"], |
403 | + host=config.builddmaster.fetch_service_host, |
404 | + port=config.builddmaster.fetch_service_port, |
405 | + ) |
406 | + ) |
407 | + self.fetch_service_api = self.useFixture( |
408 | + InProcessFetchServiceAuthAPIFixture() |
409 | + ) |
410 | + yield self.fetch_service_api.start() |
411 | + self.now = time.time() |
412 | + self.useFixture(fixtures.MockPatch("time.time", return_value=self.now)) |
413 | + self.addCleanup(shut_down_default_process_pool) |
414 | + self.setUpStats() |
415 | + |
416 | + def makeJob(self, **kwargs): |
417 | + # We need a builder worker in these tests, in order that requesting |
418 | + # a proxy token can piggyback on its reactor and pool. |
419 | + job = super().makeJob(**kwargs) |
420 | + builder = MockBuilder() |
421 | + builder.processor = job.build.processor |
422 | + worker = self.useFixture(WorkerTestHelpers()).getClientWorker() |
423 | + job.setBuilder(builder, worker) |
424 | + self.addCleanup(worker.pool.closeCachedConnections) |
425 | + return job |
426 | + |
427 | + @defer.inlineCallbacks |
428 | + def test_requestFetchServiceSession_unconfigured(self): |
429 | + """Create a snap build request with an incomplete fetch service |
430 | + configuration. |
431 | + |
432 | + If `fetch_service_host` is not provided the function will return |
433 | + without populating `proxy_url` and `revocation_endpoint`. |
434 | + """ |
435 | + self.pushConfig("builddmaster", fetch_service_host=None) |
436 | + self.useFixture( |
437 | + FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: "on"}) |
438 | + ) |
439 | + snap = self.factory.makeSnap(use_fetch_service=True) |
440 | + request = self.factory.makeSnapBuildRequest(snap=snap) |
441 | + job = self.makeJob(snap=snap, build_request=request) |
442 | + with dbuser(config.builddmaster.dbuser): |
443 | + args = yield job.extraBuildArgs() |
444 | + self.assertEqual([], self.fetch_service_api.sessions.requests) |
445 | + self.assertNotIn("proxy_url", args) |
446 | + self.assertNotIn("revocation_endpoint", args) |
447 | + |
448 | + @defer.inlineCallbacks |
449 | + def test_requestFetchServiceSession_no_secret(self): |
450 | + """Create a snap build request with an incomplete fetch service |
451 | + configuration. |
452 | + |
453 | + If `fetch_service_control_admin_secret` is not provided |
454 | + the function raises a `CannotBuild` error. |
455 | + """ |
456 | + self.pushConfig( |
457 | + "builddmaster", fetch_service_control_admin_secret=None |
458 | + ) |
459 | + self.useFixture( |
460 | + FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: "on"}) |
461 | + ) |
462 | + snap = self.factory.makeSnap(use_fetch_service=True) |
463 | + request = self.factory.makeSnapBuildRequest(snap=snap) |
464 | + job = self.makeJob(snap=snap, build_request=request) |
465 | + expected_exception_msg = ( |
466 | + "fetch_service_control_admin_secret is not configured." |
467 | + ) |
468 | + with ExpectedException(CannotBuild, expected_exception_msg): |
469 | + yield job.extraBuildArgs() |
470 | + |
471 | + @defer.inlineCallbacks |
472 | + def test_requestFetchServiceSession(self): |
473 | + """Create a snap build request with a successful fetch service |
474 | + configuration. |
475 | + |
476 | + `proxy_url` and `revocation_endpoint` are correctly populated. |
477 | + """ |
478 | + self.useFixture( |
479 | + FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: "on"}) |
480 | + ) |
481 | + snap = self.factory.makeSnap(use_fetch_service=True) |
482 | + request = self.factory.makeSnapBuildRequest(snap=snap) |
483 | + job = self.makeJob(snap=snap, build_request=request) |
484 | + args = yield job.extraBuildArgs() |
485 | + expected_uri = urlsplit( |
486 | + config.builddmaster.fetch_service_control_endpoint |
487 | + ).path.encode("UTF-8") |
488 | + request_matcher = MatchesDict( |
489 | + { |
490 | + "method": Equals(b"POST"), |
491 | + "uri": Equals(expected_uri), |
492 | + "headers": ContainsDict( |
493 | + { |
494 | + b"Authorization": MatchesListwise( |
495 | + [ |
496 | + Equals( |
497 | + b"Basic " |
498 | + + base64.b64encode( |
499 | + b"admin-launchpad.test:admin-secret" |
500 | + ) |
501 | + ) |
502 | + ] |
503 | + ), |
504 | + b"Content-Type": MatchesListwise( |
505 | + [ |
506 | + Equals(b"application/json"), |
507 | + ] |
508 | + ), |
509 | + } |
510 | + ), |
511 | + "json": MatchesDict( |
512 | + { |
513 | + "username": StartsWith(job.build.build_cookie + "-"), |
514 | + "policy": Equals("permissive"), |
515 | + } |
516 | + ), |
517 | + } |
518 | + ) |
519 | + self.assertThat( |
520 | + self.fetch_service_api.sessions.requests, |
521 | + MatchesListwise([request_matcher]), |
522 | + ) |
523 | + self.assertIn("proxy_url", args) |
524 | + self.assertIn("revocation_endpoint", args) |
525 | + |
526 | + @defer.inlineCallbacks |
527 | + def test_requestFetchServiceSession_flag_off(self): |
528 | + """Create a snap build request turning off the feature flag.""" |
529 | + self.useFixture( |
530 | + FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: None}) |
531 | + ) |
532 | + snap = self.factory.makeSnap(use_fetch_service=True) |
533 | + request = self.factory.makeSnapBuildRequest(snap=snap) |
534 | + job = self.makeJob(snap=snap, build_request=request) |
535 | + yield job.extraBuildArgs() |
536 | + self.assertEqual([], self.fetch_service_api.sessions.requests) |
537 | + |
538 | + |
539 | +class TestAsyncSnapBuildBehaviourBuilderProxy( |
540 | + StatsMixin, TestSnapBuildBehaviourBase |
541 | +): |
542 | run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory( |
543 | timeout=30 |
544 | ) |
Good work - left a few comments!
I'll leave the approval to Jürgen who is more into the details of the whole project.