Merge ~twom/launchpad:oci-buildbehaviour into launchpad:master
- Git
- lp:~twom/launchpad
- oci-buildbehaviour
- Merge into master
Proposed by
Tom Wardill
Status: | Merged |
---|---|
Approved by: | Tom Wardill |
Approved revision: | 47145664aa7899c4393aff34ccbba9536ac7775b |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~twom/launchpad:oci-buildbehaviour |
Merge into: | launchpad:master |
Diff against target: |
1557 lines (+1067/-166) 15 files modified
database/schema/security.cfg (+1/-0) lib/lp/buildmaster/model/buildfarmjobbehaviour.py (+17/-13) lib/lp/buildmaster/tests/mock_slaves.py (+9/-3) lib/lp/buildmaster/tests/snapbuildproxy.py (+123/-0) lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py (+1/-1) lib/lp/oci/configure.zcml (+6/-0) lib/lp/oci/interfaces/ocirecipebuild.py (+6/-0) lib/lp/oci/model/ocirecipebuild.py (+25/-4) lib/lp/oci/model/ocirecipebuildbehaviour.py (+182/-0) lib/lp/oci/tests/test_ocirecipebuild.py (+11/-1) lib/lp/oci/tests/test_ocirecipebuildbehaviour.py (+604/-0) lib/lp/snappy/model/snapbuildbehaviour.py (+52/-43) lib/lp/snappy/tests/test_snapbuildbehaviour.py (+18/-96) lib/lp/testing/factory.py (+11/-4) lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py (+1/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+379201@code.launchpad.net |
This proposal supersedes a proposal from 2019-12-12.
Commit message
Add OCIRecipeBuildB
Description of the change
Add a buildbehaviour to drive OCI image builds.
Use mixin tests where sensible, override for places where the OCI methods differ.
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : Posted in a previous version of this proposal | # |
review:
Needs Fixing
Revision history for this message
Tom Wardill (twom) : Posted in a previous version of this proposal | # |
Revision history for this message
Colin Watson (cjwatson) : Posted in a previous version of this proposal | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Needs Fixing
Revision history for this message
Colin Watson (cjwatson) : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Needs Fixing
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/database/schema/security.cfg b/database/schema/security.cfg | |||
2 | index 306d206..d0832c1 100644 | |||
3 | --- a/database/schema/security.cfg | |||
4 | +++ b/database/schema/security.cfg | |||
5 | @@ -995,6 +995,7 @@ public.livefsbuild = SELECT, UPDATE | |||
6 | 995 | public.livefsfile = SELECT | 995 | public.livefsfile = SELECT |
7 | 996 | public.ocifile = SELECT | 996 | public.ocifile = SELECT |
8 | 997 | public.ociproject = SELECT | 997 | public.ociproject = SELECT |
9 | 998 | public.ociprojectname = SELECT | ||
10 | 998 | public.ocirecipe = SELECT | 999 | public.ocirecipe = SELECT |
11 | 999 | public.ocirecipebuild = SELECT, UPDATE | 1000 | public.ocirecipebuild = SELECT, UPDATE |
12 | 1000 | public.openididentifier = SELECT | 1001 | public.openididentifier = SELECT |
13 | diff --git a/lib/lp/buildmaster/model/buildfarmjobbehaviour.py b/lib/lp/buildmaster/model/buildfarmjobbehaviour.py | |||
14 | index 2f3515a..4b28693 100644 | |||
15 | --- a/lib/lp/buildmaster/model/buildfarmjobbehaviour.py | |||
16 | +++ b/lib/lp/buildmaster/model/buildfarmjobbehaviour.py | |||
17 | @@ -301,6 +301,22 @@ class BuildFarmJobBehaviourBase: | |||
18 | 301 | transaction.commit() | 301 | transaction.commit() |
19 | 302 | 302 | ||
20 | 303 | @defer.inlineCallbacks | 303 | @defer.inlineCallbacks |
21 | 304 | def _downloadFiles(self, filemap, upload_path, logger): | ||
22 | 305 | filenames_to_download = [] | ||
23 | 306 | for filename, sha1 in filemap.items(): | ||
24 | 307 | logger.info("Grabbing file: %s (%s)" % ( | ||
25 | 308 | filename, self._slave.getURL(sha1))) | ||
26 | 309 | out_file_name = os.path.join(upload_path, filename) | ||
27 | 310 | # If the evaluated output file name is not within our | ||
28 | 311 | # upload path, then we don't try to copy this or any | ||
29 | 312 | # subsequent files. | ||
30 | 313 | if not os.path.realpath(out_file_name).startswith(upload_path): | ||
31 | 314 | raise BuildDaemonError( | ||
32 | 315 | "Build returned a file named %r." % filename) | ||
33 | 316 | filenames_to_download.append((sha1, out_file_name)) | ||
34 | 317 | yield self._slave.getFiles(filenames_to_download, logger=logger) | ||
35 | 318 | |||
36 | 319 | @defer.inlineCallbacks | ||
37 | 304 | def handleSuccess(self, slave_status, logger): | 320 | def handleSuccess(self, slave_status, logger): |
38 | 305 | """Handle a package that built successfully. | 321 | """Handle a package that built successfully. |
39 | 306 | 322 | ||
40 | @@ -337,19 +353,7 @@ class BuildFarmJobBehaviourBase: | |||
41 | 337 | grab_dir, str(build.archive.id), build.distribution.name) | 353 | grab_dir, str(build.archive.id), build.distribution.name) |
42 | 338 | os.makedirs(upload_path) | 354 | os.makedirs(upload_path) |
43 | 339 | 355 | ||
57 | 340 | filenames_to_download = [] | 356 | yield self._downloadFiles(filemap, upload_path, logger) |
45 | 341 | for filename, sha1 in filemap.items(): | ||
46 | 342 | logger.info("Grabbing file: %s (%s)" % ( | ||
47 | 343 | filename, self._slave.getURL(sha1))) | ||
48 | 344 | out_file_name = os.path.join(upload_path, filename) | ||
49 | 345 | # If the evaluated output file name is not within our | ||
50 | 346 | # upload path, then we don't try to copy this or any | ||
51 | 347 | # subsequent files. | ||
52 | 348 | if not os.path.realpath(out_file_name).startswith(upload_path): | ||
53 | 349 | raise BuildDaemonError( | ||
54 | 350 | "Build returned a file named %r." % filename) | ||
55 | 351 | filenames_to_download.append((sha1, out_file_name)) | ||
56 | 352 | yield self._slave.getFiles(filenames_to_download, logger=logger) | ||
58 | 353 | 357 | ||
59 | 354 | transaction.commit() | 358 | transaction.commit() |
60 | 355 | 359 | ||
61 | diff --git a/lib/lp/buildmaster/tests/mock_slaves.py b/lib/lp/buildmaster/tests/mock_slaves.py | |||
62 | index a19a85d..32633ea 100644 | |||
63 | --- a/lib/lp/buildmaster/tests/mock_slaves.py | |||
64 | +++ b/lib/lp/buildmaster/tests/mock_slaves.py | |||
65 | @@ -194,7 +194,8 @@ class WaitingSlave(OkSlave): | |||
66 | 194 | 194 | ||
67 | 195 | # By default, the slave only has a buildlog, but callsites | 195 | # By default, the slave only has a buildlog, but callsites |
68 | 196 | # can update this list as needed. | 196 | # can update this list as needed. |
70 | 197 | self.valid_file_hashes = ['buildlog'] | 197 | self.valid_files = {'buildlog': ''} |
71 | 198 | self._got_file_record = [] | ||
72 | 198 | 199 | ||
73 | 199 | def status(self): | 200 | def status(self): |
74 | 200 | self.call_log.append('status') | 201 | self.call_log.append('status') |
75 | @@ -208,12 +209,17 @@ class WaitingSlave(OkSlave): | |||
76 | 208 | 209 | ||
77 | 209 | def getFile(self, hash, file_to_write): | 210 | def getFile(self, hash, file_to_write): |
78 | 210 | self.call_log.append('getFile') | 211 | self.call_log.append('getFile') |
81 | 211 | if hash in self.valid_file_hashes: | 212 | if hash in self.valid_files: |
80 | 212 | content = "This is a %s" % hash | ||
82 | 213 | if isinstance(file_to_write, types.StringTypes): | 213 | if isinstance(file_to_write, types.StringTypes): |
83 | 214 | file_to_write = open(file_to_write, 'wb') | 214 | file_to_write = open(file_to_write, 'wb') |
84 | 215 | if not self.valid_files[hash]: | ||
85 | 216 | content = b"This is a %s" % hash | ||
86 | 217 | else: | ||
87 | 218 | with open(self.valid_files[hash], 'rb') as source: | ||
88 | 219 | content = source.read() | ||
89 | 215 | file_to_write.write(content) | 220 | file_to_write.write(content) |
90 | 216 | file_to_write.close() | 221 | file_to_write.close() |
91 | 222 | self._got_file_record.append(hash) | ||
92 | 217 | return defer.succeed(None) | 223 | return defer.succeed(None) |
93 | 218 | 224 | ||
94 | 219 | 225 | ||
95 | diff --git a/lib/lp/buildmaster/tests/snapbuildproxy.py b/lib/lp/buildmaster/tests/snapbuildproxy.py | |||
96 | 220 | new file mode 100644 | 226 | new file mode 100644 |
97 | index 0000000..21d973e | |||
98 | --- /dev/null | |||
99 | +++ b/lib/lp/buildmaster/tests/snapbuildproxy.py | |||
100 | @@ -0,0 +1,123 @@ | |||
101 | 1 | # Copyright 2015-2019 Canonical Ltd. This software is licensed under the | ||
102 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
103 | 3 | |||
104 | 4 | """Fixtures for dealing with the build time 'snap' HTTP proxy.""" | ||
105 | 5 | |||
106 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
107 | 7 | |||
108 | 8 | __metaclass__ = type | ||
109 | 9 | |||
110 | 10 | from datetime import datetime | ||
111 | 11 | import json | ||
112 | 12 | from textwrap import dedent | ||
113 | 13 | import uuid | ||
114 | 14 | |||
115 | 15 | import fixtures | ||
116 | 16 | from six.moves.urllib_parse import urlsplit | ||
117 | 17 | from testtools.matchers import ( | ||
118 | 18 | Equals, | ||
119 | 19 | HasLength, | ||
120 | 20 | MatchesStructure, | ||
121 | 21 | ) | ||
122 | 22 | from twisted.internet import ( | ||
123 | 23 | defer, | ||
124 | 24 | endpoints, | ||
125 | 25 | reactor, | ||
126 | 26 | ) | ||
127 | 27 | from twisted.python.compat import nativeString | ||
128 | 28 | from twisted.web import ( | ||
129 | 29 | resource, | ||
130 | 30 | server, | ||
131 | 31 | ) | ||
132 | 32 | |||
133 | 33 | from lp.services.config import config | ||
134 | 34 | |||
135 | 35 | |||
136 | 36 | class ProxyAuthAPITokensResource(resource.Resource): | ||
137 | 37 | """A test tokens resource for the proxy authentication API.""" | ||
138 | 38 | |||
139 | 39 | isLeaf = True | ||
140 | 40 | |||
141 | 41 | def __init__(self): | ||
142 | 42 | resource.Resource.__init__(self) | ||
143 | 43 | self.requests = [] | ||
144 | 44 | |||
145 | 45 | def render_POST(self, request): | ||
146 | 46 | content = request.content.read() | ||
147 | 47 | self.requests.append({ | ||
148 | 48 | "method": request.method, | ||
149 | 49 | "uri": request.uri, | ||
150 | 50 | "headers": dict(request.requestHeaders.getAllRawHeaders()), | ||
151 | 51 | "content": content, | ||
152 | 52 | }) | ||
153 | 53 | username = json.loads(content)["username"] | ||
154 | 54 | return json.dumps({ | ||
155 | 55 | "username": username, | ||
156 | 56 | "secret": uuid.uuid4().hex, | ||
157 | 57 | "timestamp": datetime.utcnow().isoformat(), | ||
158 | 58 | }) | ||
159 | 59 | |||
160 | 60 | |||
161 | 61 | class InProcessProxyAuthAPIFixture(fixtures.Fixture): | ||
162 | 62 | """A fixture that pretends to be the proxy authentication API. | ||
163 | 63 | |||
164 | 64 | Users of this fixture must call the `start` method, which returns a | ||
165 | 65 | `Deferred`, and arrange for that to get back to the reactor. This is | ||
166 | 66 | necessary because the basic fixture API does not allow `setUp` to return | ||
167 | 67 | anything. For example: | ||
168 | 68 | |||
169 | 69 | class TestSomething(TestCase): | ||
170 | 70 | |||
171 | 71 | run_tests_with = AsynchronousDeferredRunTest.make_factory( | ||
172 | 72 | timeout=10) | ||
173 | 73 | |||
174 | 74 | @defer.inlineCallbacks | ||
175 | 75 | def setUp(self): | ||
176 | 76 | super(TestSomething, self).setUp() | ||
177 | 77 | yield self.useFixture(InProcessProxyAuthAPIFixture()).start() | ||
178 | 78 | """ | ||
179 | 79 | |||
180 | 80 | @defer.inlineCallbacks | ||
181 | 81 | def start(self): | ||
182 | 82 | root = resource.Resource() | ||
183 | 83 | self.tokens = ProxyAuthAPITokensResource() | ||
184 | 84 | root.putChild("tokens", self.tokens) | ||
185 | 85 | endpoint = endpoints.serverFromString(reactor, nativeString("tcp:0")) | ||
186 | 86 | site = server.Site(self.tokens) | ||
187 | 87 | self.addCleanup(site.stopFactory) | ||
188 | 88 | port = yield endpoint.listen(site) | ||
189 | 89 | self.addCleanup(port.stopListening) | ||
190 | 90 | config.push("in-process-proxy-auth-api-fixture", dedent(""" | ||
191 | 91 | [snappy] | ||
192 | 92 | builder_proxy_auth_api_admin_secret: admin-secret | ||
193 | 93 | builder_proxy_auth_api_endpoint: http://%s:%s/tokens | ||
194 | 94 | """) % | ||
195 | 95 | (port.getHost().host, port.getHost().port)) | ||
196 | 96 | self.addCleanup(config.pop, "in-process-proxy-auth-api-fixture") | ||
197 | 97 | |||
198 | 98 | |||
199 | 99 | class ProxyURLMatcher(MatchesStructure): | ||
200 | 100 | """Check that a string is a valid url for a snap build proxy.""" | ||
201 | 101 | |||
202 | 102 | def __init__(self, job, now): | ||
203 | 103 | super(ProxyURLMatcher, self).__init__( | ||
204 | 104 | scheme=Equals("http"), | ||
205 | 105 | username=Equals("{}-{}".format( | ||
206 | 106 | job.build.build_cookie, int(now))), | ||
207 | 107 | password=HasLength(32), | ||
208 | 108 | hostname=Equals(config.snappy.builder_proxy_host), | ||
209 | 109 | port=Equals(config.snappy.builder_proxy_port), | ||
210 | 110 | path=Equals("")) | ||
211 | 111 | |||
212 | 112 | def match(self, matchee): | ||
213 | 113 | super(ProxyURLMatcher, self).match(urlsplit(matchee)) | ||
214 | 114 | |||
215 | 115 | |||
216 | 116 | class RevocationEndpointMatcher(Equals): | ||
217 | 117 | """Check that a string is a valid endpoint for proxy token revocation.""" | ||
218 | 118 | |||
219 | 119 | def __init__(self, job, now): | ||
220 | 120 | super(RevocationEndpointMatcher, self).__init__( | ||
221 | 121 | "{}/{}-{}".format( | ||
222 | 122 | config.snappy.builder_proxy_auth_api_endpoint, | ||
223 | 123 | job.build.build_cookie, int(now))) | ||
224 | diff --git a/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py b/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py | |||
225 | index 697ff52..5046d8c 100644 | |||
226 | --- a/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py | |||
227 | +++ b/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py | |||
228 | @@ -337,7 +337,7 @@ class TestHandleStatusMixin: | |||
229 | 337 | self.builder = self.factory.makeBuilder() | 337 | self.builder = self.factory.makeBuilder() |
230 | 338 | self.build.buildqueue_record.markAsBuilding(self.builder) | 338 | self.build.buildqueue_record.markAsBuilding(self.builder) |
231 | 339 | self.slave = WaitingSlave('BuildStatus.OK') | 339 | self.slave = WaitingSlave('BuildStatus.OK') |
233 | 340 | self.slave.valid_file_hashes.append('test_file_hash') | 340 | self.slave.valid_files['test_file_hash'] = '' |
234 | 341 | self.interactor = BuilderInteractor() | 341 | self.interactor = BuilderInteractor() |
235 | 342 | self.behaviour = self.interactor.getBuildBehaviour( | 342 | self.behaviour = self.interactor.getBuildBehaviour( |
236 | 343 | self.build.buildqueue_record, self.builder, self.slave) | 343 | self.build.buildqueue_record, self.builder, self.slave) |
237 | diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml | |||
238 | index b86afcb..a01178a 100644 | |||
239 | --- a/lib/lp/oci/configure.zcml | |||
240 | +++ b/lib/lp/oci/configure.zcml | |||
241 | @@ -63,4 +63,10 @@ | |||
242 | 63 | <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" /> | 63 | <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" /> |
243 | 64 | </securedutility> | 64 | </securedutility> |
244 | 65 | 65 | ||
245 | 66 | <adapter | ||
246 | 67 | for="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuild" | ||
247 | 68 | provides="lp.buildmaster.interfaces.buildfarmjobbehaviour.IBuildFarmJobBehaviour" | ||
248 | 69 | factory="lp.oci.model.ocirecipebuildbehaviour.OCIRecipeBuildBehaviour" | ||
249 | 70 | permission="zope.Public" /> | ||
250 | 71 | |||
251 | 66 | </configure> | 72 | </configure> |
252 | diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py | |||
253 | index 30c3835..a41e2c8 100644 | |||
254 | --- a/lib/lp/oci/interfaces/ocirecipebuild.py | |||
255 | +++ b/lib/lp/oci/interfaces/ocirecipebuild.py | |||
256 | @@ -27,6 +27,7 @@ from lp.oci.interfaces.ocirecipe import IOCIRecipe | |||
257 | 27 | from lp.services.database.constants import DEFAULT | 27 | from lp.services.database.constants import DEFAULT |
258 | 28 | from lp.services.fields import PublicPersonChoice | 28 | from lp.services.fields import PublicPersonChoice |
259 | 29 | from lp.services.librarian.interfaces import ILibraryFileAlias | 29 | from lp.services.librarian.interfaces import ILibraryFileAlias |
260 | 30 | from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries | ||
261 | 30 | 31 | ||
262 | 31 | 32 | ||
263 | 32 | class IOCIRecipeBuildEdit(Interface): | 33 | class IOCIRecipeBuildEdit(Interface): |
264 | @@ -82,6 +83,11 @@ class IOCIRecipeBuildView(IPackageBuild): | |||
265 | 82 | :return: The corresponding `ILibraryFileAlias`. | 83 | :return: The corresponding `ILibraryFileAlias`. |
266 | 83 | """ | 84 | """ |
267 | 84 | 85 | ||
268 | 86 | distro_arch_series = Reference( | ||
269 | 87 | IDistroArchSeries, | ||
270 | 88 | title=_("The series and architecture for which to build."), | ||
271 | 89 | required=True, readonly=True) | ||
272 | 90 | |||
273 | 85 | 91 | ||
274 | 86 | class IOCIRecipeBuildAdmin(Interface): | 92 | class IOCIRecipeBuildAdmin(Interface): |
275 | 87 | # XXX twom 2020-02-10 This will probably need rescore() implementing | 93 | # XXX twom 2020-02-10 This will probably need rescore() implementing |
276 | diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py | |||
277 | index c010817..12688cd 100644 | |||
278 | --- a/lib/lp/oci/model/ocirecipebuild.py | |||
279 | +++ b/lib/lp/oci/model/ocirecipebuild.py | |||
280 | @@ -44,7 +44,9 @@ from lp.oci.interfaces.ocirecipebuild import ( | |||
281 | 44 | IOCIRecipeBuild, | 44 | IOCIRecipeBuild, |
282 | 45 | IOCIRecipeBuildSet, | 45 | IOCIRecipeBuildSet, |
283 | 46 | ) | 46 | ) |
284 | 47 | from lp.registry.interfaces.pocket import PackagePublishingPocket | ||
285 | 47 | from lp.registry.model.person import Person | 48 | from lp.registry.model.person import Person |
286 | 49 | from lp.services.config import config | ||
287 | 48 | from lp.services.database.bulk import load_related | 50 | from lp.services.database.bulk import load_related |
288 | 49 | from lp.services.database.constants import DEFAULT | 51 | from lp.services.database.constants import DEFAULT |
289 | 50 | from lp.services.database.decoratedresultset import DecoratedResultSet | 52 | from lp.services.database.decoratedresultset import DecoratedResultSet |
290 | @@ -128,10 +130,16 @@ class OCIRecipeBuild(PackageBuildMixin, Storm): | |||
291 | 128 | build_farm_job_id = Int(name='build_farm_job', allow_none=False) | 130 | build_farm_job_id = Int(name='build_farm_job', allow_none=False) |
292 | 129 | build_farm_job = Reference(build_farm_job_id, 'BuildFarmJob.id') | 131 | build_farm_job = Reference(build_farm_job_id, 'BuildFarmJob.id') |
293 | 130 | 132 | ||
298 | 131 | # Stub attributes to match the IPackageBuild interface that we | 133 | # We only care about the pocket from a building environment POV, |
299 | 132 | # are not using in this implementation at this time. | 134 | # it is not a target, nor referenced in the final build. |
300 | 133 | pocket = None | 135 | pocket = PackagePublishingPocket.UPDATES |
301 | 134 | distro_series = None | 136 | |
302 | 137 | @property | ||
303 | 138 | def distro_series(self): | ||
304 | 139 | # XXX twom 2020-02-14 - This really needs to be set elsewhere, | ||
305 | 140 | # as this may not be an LTS release and ties the OCI target to | ||
306 | 141 | # a completely unrelated process. | ||
307 | 142 | return self.distribution.currentseries | ||
308 | 135 | 143 | ||
309 | 136 | def __init__(self, build_farm_job, requester, recipe, | 144 | def __init__(self, build_farm_job, requester, recipe, |
310 | 137 | processor, virtualized, date_created): | 145 | processor, virtualized, date_created): |
311 | @@ -257,6 +265,19 @@ class OCIRecipeBuild(PackageBuildMixin, Storm): | |||
312 | 257 | # pillar isn't just a distribution | 265 | # pillar isn't just a distribution |
313 | 258 | return self.recipe.oci_project.distribution | 266 | return self.recipe.oci_project.distribution |
314 | 259 | 267 | ||
315 | 268 | @property | ||
316 | 269 | def distro_arch_series(self): | ||
317 | 270 | return self.distribution.currentseries.getDistroArchSeriesByProcessor( | ||
318 | 271 | self.processor) | ||
319 | 272 | |||
320 | 273 | def notify(self, extra_info=None): | ||
321 | 274 | """See `IPackageBuild`.""" | ||
322 | 275 | if not config.builddmaster.send_build_notification: | ||
323 | 276 | return | ||
324 | 277 | if self.status == BuildStatus.FULLYBUILT: | ||
325 | 278 | return | ||
326 | 279 | # XXX twom 2019-12-11 This should send mail | ||
327 | 280 | |||
328 | 260 | 281 | ||
329 | 261 | @implementer(IOCIRecipeBuildSet) | 282 | @implementer(IOCIRecipeBuildSet) |
330 | 262 | class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin): | 283 | class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin): |
331 | diff --git a/lib/lp/oci/model/ocirecipebuildbehaviour.py b/lib/lp/oci/model/ocirecipebuildbehaviour.py | |||
332 | 263 | new file mode 100644 | 284 | new file mode 100644 |
333 | index 0000000..f0debfa | |||
334 | --- /dev/null | |||
335 | +++ b/lib/lp/oci/model/ocirecipebuildbehaviour.py | |||
336 | @@ -0,0 +1,182 @@ | |||
337 | 1 | # Copyright 2019 Canonical Ltd. This software is licensed under the | ||
338 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
339 | 3 | |||
340 | 4 | """An `IBuildFarmJobBehaviour` for `OCIRecipeBuild`. | ||
341 | 5 | |||
342 | 6 | Dispatches OCI image build jobs to build-farm slaves. | ||
343 | 7 | """ | ||
344 | 8 | |||
345 | 9 | from __future__ import absolute_import, print_function, unicode_literals | ||
346 | 10 | |||
347 | 11 | __metaclass__ = type | ||
348 | 12 | __all__ = [ | ||
349 | 13 | 'OCIRecipeBuildBehaviour', | ||
350 | 14 | ] | ||
351 | 15 | |||
352 | 16 | |||
353 | 17 | import json | ||
354 | 18 | import os | ||
355 | 19 | |||
356 | 20 | from twisted.internet import defer | ||
357 | 21 | from zope.interface import implementer | ||
358 | 22 | |||
359 | 23 | from lp.app.errors import NotFoundError | ||
360 | 24 | from lp.buildmaster.enums import BuildBaseImageType | ||
361 | 25 | from lp.buildmaster.interfaces.builder import ( | ||
362 | 26 | BuildDaemonError, | ||
363 | 27 | CannotBuild, | ||
364 | 28 | ) | ||
365 | 29 | from lp.buildmaster.interfaces.buildfarmjobbehaviour import ( | ||
366 | 30 | IBuildFarmJobBehaviour, | ||
367 | 31 | ) | ||
368 | 32 | from lp.buildmaster.model.buildfarmjobbehaviour import ( | ||
369 | 33 | BuildFarmJobBehaviourBase, | ||
370 | 34 | ) | ||
371 | 35 | from lp.registry.interfaces.series import SeriesStatus | ||
372 | 36 | from lp.services.librarian.utils import copy_and_close | ||
373 | 37 | from lp.snappy.model.snapbuildbehaviour import SnapProxyMixin | ||
374 | 38 | from lp.soyuz.adapters.archivedependencies import ( | ||
375 | 39 | get_sources_list_for_building, | ||
376 | 40 | ) | ||
377 | 41 | |||
378 | 42 | |||
379 | 43 | @implementer(IBuildFarmJobBehaviour) | ||
380 | 44 | class OCIRecipeBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase): | ||
381 | 45 | |||
382 | 46 | builder_type = "oci" | ||
383 | 47 | image_types = [BuildBaseImageType.LXD, BuildBaseImageType.CHROOT] | ||
384 | 48 | |||
385 | 49 | def getLogFileName(self): | ||
386 | 50 | series = self.build.distro_series | ||
387 | 51 | |||
388 | 52 | # Examples: | ||
389 | 53 | # buildlog_oci_ubuntu_wily_amd64_name_FULLYBUILT.txt | ||
390 | 54 | return 'buildlog_oci_%s_%s_%s_%s_%s' % ( | ||
391 | 55 | series.distribution.name, series.name, | ||
392 | 56 | self.build.processor.name, self.build.recipe.name, | ||
393 | 57 | self.build.status.name) | ||
394 | 58 | |||
395 | 59 | def verifyBuildRequest(self, logger): | ||
396 | 60 | """Assert some pre-build checks. | ||
397 | 61 | |||
398 | 62 | The build request is checked: | ||
399 | 63 | * Virtualized builds can't build on a non-virtual builder | ||
400 | 64 | * Ensure that we have a chroot | ||
401 | 65 | """ | ||
402 | 66 | build = self.build | ||
403 | 67 | if build.virtualized and not self._builder.virtualized: | ||
404 | 68 | raise AssertionError( | ||
405 | 69 | "Attempt to build virtual item on a non-virtual builder.") | ||
406 | 70 | |||
407 | 71 | chroot = build.distro_arch_series.getChroot(pocket=build.pocket) | ||
408 | 72 | if chroot is None: | ||
409 | 73 | raise CannotBuild( | ||
410 | 74 | "Missing chroot for %s" % build.distro_arch_series.displayname) | ||
411 | 75 | |||
412 | 76 | @defer.inlineCallbacks | ||
413 | 77 | def extraBuildArgs(self, logger=None): | ||
414 | 78 | """ | ||
415 | 79 | Return the extra arguments required by the slave for the given build. | ||
416 | 80 | """ | ||
417 | 81 | build = self.build | ||
418 | 82 | args = yield super(OCIRecipeBuildBehaviour, self).extraBuildArgs( | ||
419 | 83 | logger=logger) | ||
420 | 84 | yield self.addProxyArgs(args) | ||
421 | 85 | # XXX twom 2020-02-17 This may need to be more complex, and involve | ||
422 | 86 | # distribution name. | ||
423 | 87 | args["name"] = build.recipe.name | ||
424 | 88 | args["archives"], args["trusted_keys"] = ( | ||
425 | 89 | yield get_sources_list_for_building( | ||
426 | 90 | build, build.distro_arch_series, None, | ||
427 | 91 | tools_source=None, tools_fingerprint=None, | ||
428 | 92 | logger=logger)) | ||
429 | 93 | |||
430 | 94 | args['build_file'] = build.recipe.build_file | ||
431 | 95 | |||
432 | 96 | if build.recipe.git_ref is not None: | ||
433 | 97 | args["git_repository"] = ( | ||
434 | 98 | build.recipe.git_repository.git_https_url) | ||
435 | 99 | else: | ||
436 | 100 | raise CannotBuild( | ||
437 | 101 | "Source repository for ~%s/%s has been deleted." % | ||
438 | 102 | (build.recipe.owner.name, build.recipe.name)) | ||
439 | 103 | |||
440 | 104 | if build.recipe.git_path != "HEAD": | ||
441 | 105 | args["git_path"] = build.recipe.git_ref.name | ||
442 | 106 | |||
443 | 107 | defer.returnValue(args) | ||
444 | 108 | |||
445 | 109 | def _ensureFilePath(self, file_name, file_path, upload_path): | ||
446 | 110 | # If the evaluated output file name is not within our | ||
447 | 111 | # upload path, then we don't try to copy this or any | ||
448 | 112 | # subsequent files. | ||
449 | 113 | if not os.path.normpath(file_path).startswith(upload_path + '/'): | ||
450 | 114 | raise BuildDaemonError( | ||
451 | 115 | "Build returned a file named '%s'." % file_name) | ||
452 | 116 | |||
453 | 117 | @defer.inlineCallbacks | ||
454 | 118 | def _fetchIntermediaryFile(self, name, filemap, upload_path): | ||
455 | 119 | file_hash = filemap[name] | ||
456 | 120 | file_path = os.path.join(upload_path, name) | ||
457 | 121 | self._ensureFilePath(name, file_path, upload_path) | ||
458 | 122 | yield self._slave.getFile(file_hash, file_path) | ||
459 | 123 | |||
460 | 124 | with open(file_path, 'r') as file_fp: | ||
461 | 125 | contents = json.load(file_fp) | ||
462 | 126 | defer.returnValue(contents) | ||
463 | 127 | |||
464 | 128 | def _extractLayerFiles(self, upload_path, section, config, digests, files): | ||
465 | 129 | # These are different sets of ids, in the same order | ||
466 | 130 | # layer_id is the filename, diff_id is the internal (docker) id | ||
467 | 131 | for diff_id in config['rootfs']['diff_ids']: | ||
468 | 132 | for digests_section in digests: | ||
469 | 133 | layer_id = digests_section[diff_id]['layer_id'] | ||
470 | 134 | # This is in the form '<id>/layer.tar', we only need the first | ||
471 | 135 | layer_filename = "{}.tar.gz".format(layer_id.split('/')[0]) | ||
472 | 136 | digest = digests_section[diff_id]['digest'] | ||
473 | 137 | try: | ||
474 | 138 | _, librarian_file, _ = self.build.getLayerFileByDigest( | ||
475 | 139 | digest) | ||
476 | 140 | except NotFoundError: | ||
477 | 141 | files.add(layer_filename) | ||
478 | 142 | continue | ||
479 | 143 | layer_path = os.path.join(upload_path, layer_filename) | ||
480 | 144 | librarian_file.open() | ||
481 | 145 | copy_and_close(librarian_file, open(layer_path, 'wb')) | ||
482 | 146 | |||
483 | 147 | def _convertToRetrievableFile(self, upload_path, file_name, filemap): | ||
484 | 148 | file_path = os.path.join(upload_path, file_name) | ||
485 | 149 | self._ensureFilePath(file_name, file_path, upload_path) | ||
486 | 150 | return (filemap[file_name], file_path) | ||
487 | 151 | |||
488 | 152 | @defer.inlineCallbacks | ||
489 | 153 | def _downloadFiles(self, filemap, upload_path, logger): | ||
490 | 154 | """Download required artifact files.""" | ||
491 | 155 | # We don't want to download all of the files that have been created, | ||
492 | 156 | # just the ones that are mentioned in the manifest and config. | ||
493 | 157 | |||
494 | 158 | manifest = yield self._fetchIntermediaryFile( | ||
495 | 159 | 'manifest.json', filemap, upload_path) | ||
496 | 160 | digests = yield self._fetchIntermediaryFile( | ||
497 | 161 | 'digests.json', filemap, upload_path) | ||
498 | 162 | |||
499 | 163 | files = set() | ||
500 | 164 | for section in manifest: | ||
501 | 165 | config = yield self._fetchIntermediaryFile( | ||
502 | 166 | section['Config'], filemap, upload_path) | ||
503 | 167 | self._extractLayerFiles( | ||
504 | 168 | upload_path, section, config, digests, files) | ||
505 | 169 | |||
506 | 170 | files_to_download = [ | ||
507 | 171 | self._convertToRetrievableFile(upload_path, filename, filemap) | ||
508 | 172 | for filename in files] | ||
509 | 173 | yield self._slave.getFiles(files_to_download, logger=logger) | ||
510 | 174 | |||
511 | 175 | def verifySuccessfulBuild(self): | ||
512 | 176 | """See `IBuildFarmJobBehaviour`.""" | ||
513 | 177 | # The implementation in BuildFarmJobBehaviourBase checks whether the | ||
514 | 178 | # target suite is modifiable in the target archive. However, an | ||
515 | 179 | # `OCIRecipeBuild` does not use an archive in this manner. | ||
516 | 180 | # We do, however, refuse to build for | ||
517 | 181 | # obsolete series. | ||
518 | 182 | assert self.build.distro_series.status != SeriesStatus.OBSOLETE | ||
519 | diff --git a/lib/lp/oci/tests/test_ocirecipebuild.py b/lib/lp/oci/tests/test_ocirecipebuild.py | |||
520 | index b768e1d..60aaf93 100644 | |||
521 | --- a/lib/lp/oci/tests/test_ocirecipebuild.py | |||
522 | +++ b/lib/lp/oci/tests/test_ocirecipebuild.py | |||
523 | @@ -16,11 +16,13 @@ from lp.app.errors import NotFoundError | |||
524 | 16 | from lp.buildmaster.enums import BuildStatus | 16 | from lp.buildmaster.enums import BuildStatus |
525 | 17 | from lp.buildmaster.interfaces.buildqueue import IBuildQueue | 17 | from lp.buildmaster.interfaces.buildqueue import IBuildQueue |
526 | 18 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild | 18 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
527 | 19 | from lp.buildmaster.interfaces.processor import IProcessorSet | ||
528 | 19 | from lp.oci.interfaces.ocirecipebuild import ( | 20 | from lp.oci.interfaces.ocirecipebuild import ( |
529 | 20 | IOCIRecipeBuild, | 21 | IOCIRecipeBuild, |
530 | 21 | IOCIRecipeBuildSet, | 22 | IOCIRecipeBuildSet, |
531 | 22 | ) | 23 | ) |
532 | 23 | from lp.oci.model.ocirecipebuild import OCIRecipeBuildSet | 24 | from lp.oci.model.ocirecipebuild import OCIRecipeBuildSet |
533 | 25 | from lp.registry.interfaces.series import SeriesStatus | ||
534 | 24 | from lp.services.propertycache import clear_property_cache | 26 | from lp.services.propertycache import clear_property_cache |
535 | 25 | from lp.testing import ( | 27 | from lp.testing import ( |
536 | 26 | admin_logged_in, | 28 | admin_logged_in, |
537 | @@ -148,8 +150,16 @@ class TestOCIRecipeBuildSet(TestCaseWithFactory): | |||
538 | 148 | 150 | ||
539 | 149 | def test_new(self): | 151 | def test_new(self): |
540 | 150 | requester = self.factory.makePerson() | 152 | requester = self.factory.makePerson() |
542 | 151 | recipe = self.factory.makeOCIRecipe() | 153 | distribution = self.factory.makeDistribution() |
543 | 154 | distroseries = self.factory.makeDistroSeries( | ||
544 | 155 | distribution=distribution, status=SeriesStatus.CURRENT) | ||
545 | 156 | processor = getUtility(IProcessorSet).getByName("386") | ||
546 | 157 | distro_arch_series = self.factory.makeDistroArchSeries( | ||
547 | 158 | distroseries=distroseries, architecturetag="i386", | ||
548 | 159 | processor=processor) | ||
549 | 152 | distro_arch_series = self.factory.makeDistroArchSeries() | 160 | distro_arch_series = self.factory.makeDistroArchSeries() |
550 | 161 | oci_project = self.factory.makeOCIProject(pillar=distribution) | ||
551 | 162 | recipe = self.factory.makeOCIRecipe(oci_project=oci_project) | ||
552 | 153 | target = getUtility(IOCIRecipeBuildSet).new( | 163 | target = getUtility(IOCIRecipeBuildSet).new( |
553 | 154 | requester, recipe, distro_arch_series) | 164 | requester, recipe, distro_arch_series) |
554 | 155 | with admin_logged_in(): | 165 | with admin_logged_in(): |
555 | diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py | |||
556 | 156 | new file mode 100644 | 166 | new file mode 100644 |
557 | index 0000000..e9ac224 | |||
558 | --- /dev/null | |||
559 | +++ b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py | |||
560 | @@ -0,0 +1,604 @@ | |||
561 | 1 | # Copyright 2015-2019 Canonical Ltd. This software is licensed under the | ||
562 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
563 | 3 | |||
564 | 4 | """Tests for `OCIRecipeBuildBehaviour`.""" | ||
565 | 5 | |||
566 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
567 | 7 | |||
568 | 8 | __metaclass__ = type | ||
569 | 9 | |||
570 | 10 | import base64 | ||
571 | 11 | from datetime import datetime | ||
572 | 12 | import json | ||
573 | 13 | import os | ||
574 | 14 | import shutil | ||
575 | 15 | import tempfile | ||
576 | 16 | import time | ||
577 | 17 | import uuid | ||
578 | 18 | |||
579 | 19 | import fixtures | ||
580 | 20 | from six.moves.urllib_parse import urlsplit | ||
581 | 21 | from testtools import ExpectedException | ||
582 | 22 | from testtools.matchers import ( | ||
583 | 23 | AfterPreprocessing, | ||
584 | 24 | ContainsDict, | ||
585 | 25 | Equals, | ||
586 | 26 | Is, | ||
587 | 27 | IsInstance, | ||
588 | 28 | MatchesDict, | ||
589 | 29 | MatchesListwise, | ||
590 | 30 | StartsWith, | ||
591 | 31 | ) | ||
592 | 32 | from testtools.twistedsupport import ( | ||
593 | 33 | AsynchronousDeferredRunTestForBrokenTwisted, | ||
594 | 34 | ) | ||
595 | 35 | from twisted.internet import defer | ||
596 | 36 | from twisted.trial.unittest import TestCase as TrialTestCase | ||
597 | 37 | from zope.component import getUtility | ||
598 | 38 | from zope.security.proxy import removeSecurityProxy | ||
599 | 39 | |||
600 | 40 | from lp.buildmaster.enums import ( | ||
601 | 41 | BuildBaseImageType, | ||
602 | 42 | BuildStatus, | ||
603 | 43 | ) | ||
604 | 44 | from lp.buildmaster.interactor import BuilderInteractor | ||
605 | 45 | from lp.buildmaster.interfaces.builder import ( | ||
606 | 46 | BuildDaemonError, | ||
607 | 47 | CannotBuild, | ||
608 | 48 | ) | ||
609 | 49 | from lp.buildmaster.interfaces.buildfarmjobbehaviour import ( | ||
610 | 50 | IBuildFarmJobBehaviour, | ||
611 | 51 | ) | ||
612 | 52 | from lp.buildmaster.interfaces.processor import IProcessorSet | ||
613 | 53 | from lp.buildmaster.tests.mock_slaves import ( | ||
614 | 54 | MockBuilder, | ||
615 | 55 | OkSlave, | ||
616 | 56 | SlaveTestHelpers, | ||
617 | 57 | WaitingSlave, | ||
618 | 58 | ) | ||
619 | 59 | from lp.buildmaster.tests.snapbuildproxy import ( | ||
620 | 60 | InProcessProxyAuthAPIFixture, | ||
621 | 61 | ProxyURLMatcher, | ||
622 | 62 | RevocationEndpointMatcher, | ||
623 | 63 | ) | ||
624 | 64 | from lp.buildmaster.tests.test_buildfarmjobbehaviour import ( | ||
625 | 65 | TestGetUploadMethodsMixin, | ||
626 | 66 | ) | ||
627 | 67 | from lp.oci.model.ocirecipebuildbehaviour import OCIRecipeBuildBehaviour | ||
628 | 68 | from lp.registry.interfaces.series import SeriesStatus | ||
629 | 69 | from lp.services.config import config | ||
630 | 70 | from lp.services.log.logger import DevNullLogger | ||
631 | 71 | from lp.services.webapp import canonical_url | ||
632 | 72 | from lp.soyuz.adapters.archivedependencies import ( | ||
633 | 73 | get_sources_list_for_building, | ||
634 | 74 | ) | ||
635 | 75 | from lp.testing import TestCaseWithFactory | ||
636 | 76 | from lp.testing.dbuser import dbuser | ||
637 | 77 | from lp.testing.factory import LaunchpadObjectFactory | ||
638 | 78 | from lp.testing.fakemethod import FakeMethod | ||
639 | 79 | from lp.testing.layers import LaunchpadZopelessLayer | ||
640 | 80 | from lp.testing.mail_helpers import pop_notifications | ||
641 | 81 | |||
642 | 82 | |||
643 | 83 | class MakeOCIBuildMixin: | ||
644 | 84 | |||
645 | 85 | def makeBuild(self): | ||
646 | 86 | build = self.factory.makeOCIRecipeBuild() | ||
647 | 87 | self.factory.makeDistroSeries( | ||
648 | 88 | distribution=build.recipe.oci_project.distribution, | ||
649 | 89 | status=SeriesStatus.CURRENT) | ||
650 | 90 | build.queueBuild() | ||
651 | 91 | return build | ||
652 | 92 | |||
653 | 93 | def makeUnmodifiableBuild(self): | ||
654 | 94 | build = self.factory.makeOCIRecipeBuild() | ||
655 | 95 | build.distro_arch_series = 'failed' | ||
656 | 96 | build.queueBuild() | ||
657 | 97 | return build | ||
658 | 98 | |||
659 | 99 | def makeJob(self, git_ref, recipe=None): | ||
660 | 100 | """Create a sample `IOCIRecipeBuildBehaviour`.""" | ||
661 | 101 | if recipe is None: | ||
662 | 102 | build = self.factory.makeOCIRecipeBuild() | ||
663 | 103 | else: | ||
664 | 104 | build = self.factory.makeOCIRecipeBuild(recipe=recipe) | ||
665 | 105 | build.recipe.git_ref = git_ref | ||
666 | 106 | |||
667 | 107 | job = IBuildFarmJobBehaviour(build) | ||
668 | 108 | builder = MockBuilder() | ||
669 | 109 | builder.processor = job.build.processor | ||
670 | 110 | slave = self.useFixture(SlaveTestHelpers()).getClientSlave() | ||
671 | 111 | job.setBuilder(builder, slave) | ||
672 | 112 | self.addCleanup(slave.pool.closeCachedConnections) | ||
673 | 113 | |||
674 | 114 | # Taken from test_archivedependencies.py | ||
675 | 115 | for component_name in ("main", "universe"): | ||
676 | 116 | self.factory.makeComponentSelection( | ||
677 | 117 | build.distro_arch_series.distroseries, component_name) | ||
678 | 118 | |||
679 | 119 | return job | ||
680 | 120 | |||
681 | 121 | |||
682 | 122 | class TestOCIBuildBehaviour(TestCaseWithFactory): | ||
683 | 123 | |||
684 | 124 | layer = LaunchpadZopelessLayer | ||
685 | 125 | |||
686 | 126 | def test_provides_interface(self): | ||
687 | 127 | # OCIRecipeBuildBehaviour provides IBuildFarmJobBehaviour. | ||
688 | 128 | job = OCIRecipeBuildBehaviour(self.factory.makeOCIRecipeBuild()) | ||
689 | 129 | self.assertProvides(job, IBuildFarmJobBehaviour) | ||
690 | 130 | |||
691 | 131 | def test_adapts_IOCIRecipeBuild(self): | ||
692 | 132 | # IBuildFarmJobBehaviour adapts an IOCIRecipeBuild. | ||
693 | 133 | build = self.factory.makeOCIRecipeBuild() | ||
694 | 134 | job = IBuildFarmJobBehaviour(build) | ||
695 | 135 | self.assertProvides(job, IBuildFarmJobBehaviour) | ||
696 | 136 | |||
697 | 137 | |||
698 | 138 | class TestAsyncOCIRecipeBuildBehaviour(MakeOCIBuildMixin, TestCaseWithFactory): | ||
699 | 139 | |||
700 | 140 | run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory( | ||
701 | 141 | timeout=10) | ||
702 | 142 | layer = LaunchpadZopelessLayer | ||
703 | 143 | |||
704 | 144 | @defer.inlineCallbacks | ||
705 | 145 | def setUp(self): | ||
706 | 146 | super(TestAsyncOCIRecipeBuildBehaviour, self).setUp() | ||
707 | 147 | build_username = 'OCIBUILD-1' | ||
708 | 148 | self.token = {'secret': uuid.uuid4().get_hex(), | ||
709 | 149 | 'username': build_username, | ||
710 | 150 | 'timestamp': datetime.utcnow().isoformat()} | ||
711 | 151 | self.proxy_url = ("http://{username}:{password}" | ||
712 | 152 | "@{host}:{port}".format( | ||
713 | 153 | username=self.token['username'], | ||
714 | 154 | password=self.token['secret'], | ||
715 | 155 | host=config.snappy.builder_proxy_host, | ||
716 | 156 | port=config.snappy.builder_proxy_port)) | ||
717 | 157 | self.proxy_api = self.useFixture(InProcessProxyAuthAPIFixture()) | ||
718 | 158 | yield self.proxy_api.start() | ||
719 | 159 | self.now = time.time() | ||
720 | 160 | self.useFixture(fixtures.MockPatch( | ||
721 | 161 | "time.time", return_value=self.now)) | ||
722 | 162 | |||
723 | 163 | @defer.inlineCallbacks | ||
724 | 164 | def test_composeBuildRequest(self): | ||
725 | 165 | [ref] = self.factory.makeGitRefs() | ||
726 | 166 | job = self.makeJob(git_ref=ref) | ||
727 | 167 | lfa = self.factory.makeLibraryFileAlias(db_only=True) | ||
728 | 168 | job.build.distro_arch_series.addOrUpdateChroot(lfa) | ||
729 | 169 | build_request = yield job.composeBuildRequest(None) | ||
730 | 170 | self.assertThat(build_request, MatchesListwise([ | ||
731 | 171 | Equals('oci'), | ||
732 | 172 | Equals(job.build.distro_arch_series), | ||
733 | 173 | Equals(job.build.pocket), | ||
734 | 174 | Equals({}), | ||
735 | 175 | IsInstance(dict), | ||
736 | 176 | ])) | ||
737 | 177 | |||
738 | 178 | @defer.inlineCallbacks | ||
739 | 179 | def test_requestProxyToken_unconfigured(self): | ||
740 | 180 | self.pushConfig("snappy", builder_proxy_auth_api_admin_secret=None) | ||
741 | 181 | [ref] = self.factory.makeGitRefs() | ||
742 | 182 | job = self.makeJob(git_ref=ref) | ||
743 | 183 | expected_exception_msg = ( | ||
744 | 184 | "builder_proxy_auth_api_admin_secret is not configured.") | ||
745 | 185 | with ExpectedException(CannotBuild, expected_exception_msg): | ||
746 | 186 | yield job.extraBuildArgs() | ||
747 | 187 | |||
748 | 188 | @defer.inlineCallbacks | ||
749 | 189 | def test_requestProxyToken(self): | ||
750 | 190 | [ref] = self.factory.makeGitRefs() | ||
751 | 191 | job = self.makeJob(git_ref=ref) | ||
752 | 192 | yield job.extraBuildArgs() | ||
753 | 193 | self.assertThat(self.proxy_api.tokens.requests, MatchesListwise([ | ||
754 | 194 | MatchesDict({ | ||
755 | 195 | "method": Equals("POST"), | ||
756 | 196 | "uri": Equals(urlsplit( | ||
757 | 197 | config.snappy.builder_proxy_auth_api_endpoint).path), | ||
758 | 198 | "headers": ContainsDict({ | ||
759 | 199 | b"Authorization": MatchesListwise([ | ||
760 | 200 | Equals(b"Basic " + base64.b64encode( | ||
761 | 201 | b"admin-launchpad.test:admin-secret"))]), | ||
762 | 202 | b"Content-Type": MatchesListwise([ | ||
763 | 203 | Equals(b"application/json; charset=UTF-8"), | ||
764 | 204 | ]), | ||
765 | 205 | }), | ||
766 | 206 | "content": AfterPreprocessing(json.loads, MatchesDict({ | ||
767 | 207 | "username": StartsWith(job.build.build_cookie + "-"), | ||
768 | 208 | })), | ||
769 | 209 | }), | ||
770 | 210 | ])) | ||
771 | 211 | |||
772 | 212 | @defer.inlineCallbacks | ||
773 | 213 | def test_extraBuildArgs_git(self): | ||
774 | 214 | # extraBuildArgs returns appropriate arguments if asked to build a | ||
775 | 215 | # job for a Git branch. | ||
776 | 216 | [ref] = self.factory.makeGitRefs() | ||
777 | 217 | job = self.makeJob(git_ref=ref) | ||
778 | 218 | expected_archives, expected_trusted_keys = ( | ||
779 | 219 | yield get_sources_list_for_building( | ||
780 | 220 | job.build, job.build.distro_arch_series, None)) | ||
781 | 221 | for archive_line in expected_archives: | ||
782 | 222 | self.assertIn('universe', archive_line) | ||
783 | 223 | with dbuser(config.builddmaster.dbuser): | ||
784 | 224 | args = yield job.extraBuildArgs() | ||
785 | 225 | self.assertThat(args, MatchesDict({ | ||
786 | 226 | "archive_private": Is(False), | ||
787 | 227 | "archives": Equals(expected_archives), | ||
788 | 228 | "arch_tag": Equals("i386"), | ||
789 | 229 | "build_file": Equals(job.build.recipe.build_file), | ||
790 | 230 | "build_url": Equals(canonical_url(job.build)), | ||
791 | 231 | "fast_cleanup": Is(True), | ||
792 | 232 | "git_repository": Equals(ref.repository.git_https_url), | ||
793 | 233 | "git_path": Equals(ref.name), | ||
794 | 234 | "name": Equals(job.build.recipe.name), | ||
795 | 235 | "proxy_url": ProxyURLMatcher(job, self.now), | ||
796 | 236 | "revocation_endpoint": RevocationEndpointMatcher(job, self.now), | ||
797 | 237 | "series": Equals(job.build.distro_arch_series.distroseries.name), | ||
798 | 238 | "trusted_keys": Equals(expected_trusted_keys), | ||
799 | 239 | })) | ||
800 | 240 | |||
801 | 241 | @defer.inlineCallbacks | ||
802 | 242 | def test_extraBuildArgs_git_HEAD(self): | ||
803 | 243 | # extraBuildArgs returns appropriate arguments if asked to build a | ||
804 | 244 | # job for the default branch in a Launchpad-hosted Git repository. | ||
805 | 245 | [ref] = self.factory.makeGitRefs() | ||
806 | 246 | removeSecurityProxy(ref.repository)._default_branch = ref.path | ||
807 | 247 | job = self.makeJob(git_ref=ref.repository.getRefByPath("HEAD")) | ||
808 | 248 | expected_archives, expected_trusted_keys = ( | ||
809 | 249 | yield get_sources_list_for_building( | ||
810 | 250 | job.build, job.build.distro_arch_series, None)) | ||
811 | 251 | for archive_line in expected_archives: | ||
812 | 252 | self.assertIn('universe', archive_line) | ||
813 | 253 | with dbuser(config.builddmaster.dbuser): | ||
814 | 254 | args = yield job.extraBuildArgs() | ||
815 | 255 | self.assertThat(args, MatchesDict({ | ||
816 | 256 | "archive_private": Is(False), | ||
817 | 257 | "archives": Equals(expected_archives), | ||
818 | 258 | "arch_tag": Equals("i386"), | ||
819 | 259 | "build_file": Equals(job.build.recipe.build_file), | ||
820 | 260 | "build_url": Equals(canonical_url(job.build)), | ||
821 | 261 | "fast_cleanup": Is(True), | ||
822 | 262 | "git_repository": Equals(ref.repository.git_https_url), | ||
823 | 263 | "name": Equals(job.build.recipe.name), | ||
824 | 264 | "proxy_url": ProxyURLMatcher(job, self.now), | ||
825 | 265 | "revocation_endpoint": RevocationEndpointMatcher(job, self.now), | ||
826 | 266 | "series": Equals(job.build.distro_arch_series.distroseries.name), | ||
827 | 267 | "trusted_keys": Equals(expected_trusted_keys), | ||
828 | 268 | })) | ||
829 | 269 | |||
830 | 270 | @defer.inlineCallbacks | ||
831 | 271 | def test_composeBuildRequest_proxy_url_set(self): | ||
832 | 272 | [ref] = self.factory.makeGitRefs() | ||
833 | 273 | job = self.makeJob(git_ref=ref) | ||
834 | 274 | build_request = yield job.composeBuildRequest(None) | ||
835 | 275 | self.assertThat( | ||
836 | 276 | build_request[4]["proxy_url"], ProxyURLMatcher(job, self.now)) | ||
837 | 277 | |||
838 | 278 | @defer.inlineCallbacks | ||
839 | 279 | def test_composeBuildRequest_git_ref_deleted(self): | ||
840 | 280 | # If the source Git reference has been deleted, composeBuildRequest | ||
841 | 281 | # raises CannotBuild. | ||
842 | 282 | repository = self.factory.makeGitRepository() | ||
843 | 283 | [ref] = self.factory.makeGitRefs(repository=repository) | ||
844 | 284 | owner = self.factory.makePerson(name="oci-owner") | ||
845 | 285 | |||
846 | 286 | distribution = self.factory.makeDistribution() | ||
847 | 287 | distroseries = self.factory.makeDistroSeries( | ||
848 | 288 | distribution=distribution, status=SeriesStatus.CURRENT) | ||
849 | 289 | processor = getUtility(IProcessorSet).getByName("386") | ||
850 | 290 | self.factory.makeDistroArchSeries( | ||
851 | 291 | distroseries=distroseries, architecturetag="i386", | ||
852 | 292 | processor=processor) | ||
853 | 293 | |||
854 | 294 | oci_project = self.factory.makeOCIProject( | ||
855 | 295 | pillar=distribution, registrant=owner) | ||
856 | 296 | recipe = self.factory.makeOCIRecipe( | ||
857 | 297 | oci_project=oci_project, registrant=owner, owner=owner, | ||
858 | 298 | git_ref=ref) | ||
859 | 299 | job = self.makeJob(ref, recipe=recipe) | ||
860 | 300 | repository.removeRefs([ref.path]) | ||
861 | 301 | self.assertIsNone(job.build.recipe.git_ref) | ||
862 | 302 | expected_exception_msg = ("Source repository for " | ||
863 | 303 | "~oci-owner/{} has been deleted.".format( | ||
864 | 304 | recipe.name)) | ||
865 | 305 | with ExpectedException(CannotBuild, expected_exception_msg): | ||
866 | 306 | yield job.composeBuildRequest(None) | ||
867 | 307 | |||
868 | 308 | @defer.inlineCallbacks | ||
869 | 309 | def test_dispatchBuildToSlave_prefers_lxd(self): | ||
870 | 310 | self.pushConfig("snappy", builder_proxy_host=None) | ||
871 | 311 | [ref] = self.factory.makeGitRefs() | ||
872 | 312 | job = self.makeJob(git_ref=ref) | ||
873 | 313 | builder = MockBuilder() | ||
874 | 314 | builder.processor = job.build.processor | ||
875 | 315 | slave = OkSlave() | ||
876 | 316 | job.setBuilder(builder, slave) | ||
877 | 317 | chroot_lfa = self.factory.makeLibraryFileAlias(db_only=True) | ||
878 | 318 | job.build.distro_arch_series.addOrUpdateChroot( | ||
879 | 319 | chroot_lfa, image_type=BuildBaseImageType.CHROOT) | ||
880 | 320 | lxd_lfa = self.factory.makeLibraryFileAlias(db_only=True) | ||
881 | 321 | job.build.distro_arch_series.addOrUpdateChroot( | ||
882 | 322 | lxd_lfa, image_type=BuildBaseImageType.LXD) | ||
883 | 323 | yield job.dispatchBuildToSlave(DevNullLogger()) | ||
884 | 324 | self.assertEqual( | ||
885 | 325 | ('ensurepresent', lxd_lfa.http_url, '', ''), slave.call_log[0]) | ||
886 | 326 | |||
887 | 327 | @defer.inlineCallbacks | ||
888 | 328 | def test_dispatchBuildToSlave_falls_back_to_chroot(self): | ||
889 | 329 | self.pushConfig("snappy", builder_proxy_host=None) | ||
890 | 330 | [ref] = self.factory.makeGitRefs() | ||
891 | 331 | job = self.makeJob(git_ref=ref) | ||
892 | 332 | builder = MockBuilder() | ||
893 | 333 | builder.processor = job.build.processor | ||
894 | 334 | slave = OkSlave() | ||
895 | 335 | job.setBuilder(builder, slave) | ||
896 | 336 | chroot_lfa = self.factory.makeLibraryFileAlias(db_only=True) | ||
897 | 337 | job.build.distro_arch_series.addOrUpdateChroot( | ||
898 | 338 | chroot_lfa, image_type=BuildBaseImageType.CHROOT) | ||
899 | 339 | yield job.dispatchBuildToSlave(DevNullLogger()) | ||
900 | 340 | self.assertEqual( | ||
901 | 341 | ('ensurepresent', chroot_lfa.http_url, '', ''), slave.call_log[0]) | ||
902 | 342 | |||
903 | 343 | |||
904 | 344 | class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin, TrialTestCase, | ||
905 | 345 | fixtures.TestWithFixtures): | ||
906 | 346 | # This is mostly copied from TestHandleStatusMixin, however | ||
907 | 347 | # we can't use all of those tests, due to the way OCIRecipeBuildBehaviour | ||
908 | 348 | # parses the file contents, rather than just retrieving all that are | ||
909 | 349 | # available. There's also some differences in the filemap handling, as | ||
910 | 350 | # we need a much more complex filemap here. | ||
911 | 351 | |||
912 | 352 | layer = LaunchpadZopelessLayer | ||
913 | 353 | |||
914 | 354 | def pushConfig(self, section, **kwargs): | ||
915 | 355 | """Push some key-value pairs into a section of the config. | ||
916 | 356 | |||
917 | 357 | The config values will be restored during test tearDown. | ||
918 | 358 | """ | ||
919 | 359 | # Taken from lp/testing.py as we're using TrialTestCase, | ||
920 | 360 | # not lp.testing.TestCase, as we need to handle the deferred | ||
921 | 361 | # correctly. | ||
922 | 362 | name = self.factory.getUniqueString() | ||
923 | 363 | body = '\n'.join("%s: %s" % (k, v) for k, v in kwargs.iteritems()) | ||
924 | 364 | config.push(name, "\n[%s]\n%s\n" % (section, body)) | ||
925 | 365 | self.addCleanup(config.pop, name) | ||
926 | 366 | |||
927 | 367 | def _createTestFile(self, name, content, hash): | ||
928 | 368 | path = os.path.join(self.test_files_dir, name) | ||
929 | 369 | with open(path, 'wb') as fp: | ||
930 | 370 | fp.write(content) | ||
931 | 371 | self.slave.valid_files[hash] = path | ||
932 | 372 | |||
933 | 373 | def setUp(self): | ||
934 | 374 | super(TestHandleStatusForOCIRecipeBuild, self).setUp() | ||
935 | 375 | self.factory = LaunchpadObjectFactory() | ||
936 | 376 | self.build = self.makeBuild() | ||
937 | 377 | # For the moment, we require a builder for the build so that | ||
938 | 378 | # handleStatus_OK can get a reference to the slave. | ||
939 | 379 | self.builder = self.factory.makeBuilder() | ||
940 | 380 | self.build.buildqueue_record.markAsBuilding(self.builder) | ||
941 | 381 | self.slave = WaitingSlave('BuildStatus.OK') | ||
942 | 382 | self.slave.valid_files['test_file_hash'] = '' | ||
943 | 383 | self.interactor = BuilderInteractor() | ||
944 | 384 | self.behaviour = self.interactor.getBuildBehaviour( | ||
945 | 385 | self.build.buildqueue_record, self.builder, self.slave) | ||
946 | 386 | |||
947 | 387 | # We overwrite the buildmaster root to use a temp directory. | ||
948 | 388 | tempdir = tempfile.mkdtemp() | ||
949 | 389 | self.addCleanup(shutil.rmtree, tempdir) | ||
950 | 390 | self.upload_root = tempdir | ||
951 | 391 | self.pushConfig('builddmaster', root=self.upload_root) | ||
952 | 392 | |||
953 | 393 | # We stub out our build's getUploaderCommand() method so | ||
954 | 394 | # we can check whether it was called as well as | ||
955 | 395 | # verifySuccessfulUpload(). | ||
956 | 396 | removeSecurityProxy(self.build).verifySuccessfulUpload = FakeMethod( | ||
957 | 397 | result=True) | ||
958 | 398 | |||
959 | 399 | digests = [{ | ||
960 | 400 | "diff_id_1": { | ||
961 | 401 | "digest": "digest_1", | ||
962 | 402 | "source": "test/base_1", | ||
963 | 403 | "layer_id": "layer_1" | ||
964 | 404 | }, | ||
965 | 405 | "diff_id_2": { | ||
966 | 406 | "digest": "digest_2", | ||
967 | 407 | "source": "", | ||
968 | 408 | "layer_id": "layer_2" | ||
969 | 409 | } | ||
970 | 410 | }] | ||
971 | 411 | |||
972 | 412 | self.test_files_dir = tempfile.mkdtemp() | ||
973 | 413 | self._createTestFile('buildlog', '', 'buildlog') | ||
974 | 414 | self._createTestFile( | ||
975 | 415 | 'manifest.json', | ||
976 | 416 | '[{"Config": "config_file_1.json", ' | ||
977 | 417 | '"Layers": ["layer_1/layer.tar", "layer_2/layer.tar"]}]', | ||
978 | 418 | 'manifest_hash') | ||
979 | 419 | self._createTestFile( | ||
980 | 420 | 'digests.json', | ||
981 | 421 | json.dumps(digests), | ||
982 | 422 | 'digests_hash') | ||
983 | 423 | self._createTestFile( | ||
984 | 424 | 'config_file_1.json', | ||
985 | 425 | '{"rootfs": {"diff_ids": ["diff_id_1", "diff_id_2"]}}', | ||
986 | 426 | 'config_1_hash') | ||
987 | 427 | self._createTestFile( | ||
988 | 428 | 'layer_2.tar.gz', | ||
989 | 429 | '', | ||
990 | 430 | 'layer_2_hash' | ||
991 | 431 | ) | ||
992 | 432 | |||
993 | 433 | self.filemap = { | ||
994 | 434 | 'manifest.json': 'manifest_hash', | ||
995 | 435 | 'digests.json': 'digests_hash', | ||
996 | 436 | 'config_file_1.json': 'config_1_hash', | ||
997 | 437 | 'layer_1.tar.gz': 'layer_1_hash', | ||
998 | 438 | 'layer_2.tar.gz': 'layer_2_hash' | ||
999 | 439 | } | ||
1000 | 440 | self.factory.makeOCIFile( | ||
1001 | 441 | build=self.build, layer_file_digest=u'digest_1', | ||
1002 | 442 | content="retrieved from librarian") | ||
1003 | 443 | |||
1004 | 444 | def assertResultCount(self, count, result): | ||
1005 | 445 | self.assertEqual( | ||
1006 | 446 | 1, len(os.listdir(os.path.join(self.upload_root, result)))) | ||
1007 | 447 | |||
1008 | 448 | @defer.inlineCallbacks | ||
1009 | 449 | def test_handleStatus_OK_normal_image(self): | ||
1010 | 450 | with dbuser(config.builddmaster.dbuser): | ||
1011 | 451 | yield self.behaviour.handleStatus( | ||
1012 | 452 | self.build.buildqueue_record, 'OK', | ||
1013 | 453 | {'filemap': self.filemap}) | ||
1014 | 454 | self.assertEqual( | ||
1015 | 455 | ['buildlog', 'manifest_hash', 'digests_hash', 'config_1_hash', | ||
1016 | 456 | 'layer_2_hash'], | ||
1017 | 457 | self.slave._got_file_record) | ||
1018 | 458 | # This hash should not appear as it is already in the librarian | ||
1019 | 459 | self.assertNotIn('layer_1_hash', self.slave._got_file_record) | ||
1020 | 460 | self.assertEqual(BuildStatus.UPLOADING, self.build.status) | ||
1021 | 461 | self.assertResultCount(1, "incoming") | ||
1022 | 462 | |||
1023 | 463 | # layer_1 should have been retrieved from the librarian | ||
1024 | 464 | layer_1_path = os.path.join( | ||
1025 | 465 | self.upload_root, | ||
1026 | 466 | "incoming", | ||
1027 | 467 | self.behaviour.getUploadDirLeaf(self.build.build_cookie), | ||
1028 | 468 | str(self.build.archive.id), | ||
1029 | 469 | self.build.distribution.name, | ||
1030 | 470 | "layer_1.tar.gz" | ||
1031 | 471 | ) | ||
1032 | 472 | with open(layer_1_path, 'rb') as layer_1_fp: | ||
1033 | 473 | contents = layer_1_fp.read() | ||
1034 | 474 | self.assertEqual(contents, b'retrieved from librarian') | ||
1035 | 475 | |||
1036 | 476 | @defer.inlineCallbacks | ||
1037 | 477 | def test_handleStatus_OK_absolute_filepath(self): | ||
1038 | 478 | |||
1039 | 479 | self._createTestFile( | ||
1040 | 480 | 'manifest.json', | ||
1041 | 481 | '[{"Config": "/notvalid/config_file_1.json", ' | ||
1042 | 482 | '"Layers": ["layer_1/layer.tar", "layer_2/layer.tar"]}]', | ||
1043 | 483 | 'manifest_hash') | ||
1044 | 484 | |||
1045 | 485 | self.filemap['/notvalid/config_file_1.json'] = 'config_1_hash' | ||
1046 | 486 | |||
1047 | 487 | # A filemap that tries to write to files outside of the upload | ||
1048 | 488 | # directory will not be collected. | ||
1049 | 489 | with ExpectedException( | ||
1050 | 490 | BuildDaemonError, | ||
1051 | 491 | "Build returned a file named " | ||
1052 | 492 | "'/notvalid/config_file_1.json'."): | ||
1053 | 493 | with dbuser(config.builddmaster.dbuser): | ||
1054 | 494 | yield self.behaviour.handleStatus( | ||
1055 | 495 | self.build.buildqueue_record, 'OK', | ||
1056 | 496 | {'filemap': self.filemap}) | ||
1057 | 497 | |||
1058 | 498 | @defer.inlineCallbacks | ||
1059 | 499 | def test_handleStatus_OK_relative_filepath(self): | ||
1060 | 500 | |||
1061 | 501 | self._createTestFile( | ||
1062 | 502 | 'manifest.json', | ||
1063 | 503 | '[{"Config": "../config_file_1.json", ' | ||
1064 | 504 | '"Layers": ["layer_1/layer.tar", "layer_2/layer.tar"]}]', | ||
1065 | 505 | 'manifest_hash') | ||
1066 | 506 | |||
1067 | 507 | self.filemap['../config_file_1.json'] = 'config_1_hash' | ||
1068 | 508 | # A filemap that tries to write to files outside of | ||
1069 | 509 | # the upload directory will not be collected. | ||
1070 | 510 | with ExpectedException( | ||
1071 | 511 | BuildDaemonError, | ||
1072 | 512 | "Build returned a file named '../config_file_1.json'."): | ||
1073 | 513 | with dbuser(config.builddmaster.dbuser): | ||
1074 | 514 | yield self.behaviour.handleStatus( | ||
1075 | 515 | self.build.buildqueue_record, 'OK', | ||
1076 | 516 | {'filemap': self.filemap}) | ||
1077 | 517 | |||
1078 | 518 | @defer.inlineCallbacks | ||
1079 | 519 | def test_handleStatus_OK_sets_build_log(self): | ||
1080 | 520 | # The build log is set during handleStatus. | ||
1081 | 521 | self.assertEqual(None, self.build.log) | ||
1082 | 522 | with dbuser(config.builddmaster.dbuser): | ||
1083 | 523 | yield self.behaviour.handleStatus( | ||
1084 | 524 | self.build.buildqueue_record, 'OK', | ||
1085 | 525 | {'filemap': self.filemap}) | ||
1086 | 526 | self.assertNotEqual(None, self.build.log) | ||
1087 | 527 | |||
1088 | 528 | @defer.inlineCallbacks | ||
1089 | 529 | def test_handleStatus_ABORTED_cancels_cancelling(self): | ||
1090 | 530 | with dbuser(config.builddmaster.dbuser): | ||
1091 | 531 | self.build.updateStatus(BuildStatus.CANCELLING) | ||
1092 | 532 | yield self.behaviour.handleStatus( | ||
1093 | 533 | self.build.buildqueue_record, "ABORTED", {}) | ||
1094 | 534 | self.assertEqual(0, len(pop_notifications()), "Notifications received") | ||
1095 | 535 | self.assertEqual(BuildStatus.CANCELLED, self.build.status) | ||
1096 | 536 | |||
1097 | 537 | @defer.inlineCallbacks | ||
1098 | 538 | def test_handleStatus_ABORTED_illegal_when_building(self): | ||
1099 | 539 | self.builder.vm_host = "fake_vm_host" | ||
1100 | 540 | self.behaviour = self.interactor.getBuildBehaviour( | ||
1101 | 541 | self.build.buildqueue_record, self.builder, self.slave) | ||
1102 | 542 | with dbuser(config.builddmaster.dbuser): | ||
1103 | 543 | self.build.updateStatus(BuildStatus.BUILDING) | ||
1104 | 544 | with ExpectedException( | ||
1105 | 545 | BuildDaemonError, | ||
1106 | 546 | "Build returned unexpected status: u'ABORTED'"): | ||
1107 | 547 | yield self.behaviour.handleStatus( | ||
1108 | 548 | self.build.buildqueue_record, "ABORTED", {}) | ||
1109 | 549 | |||
1110 | 550 | @defer.inlineCallbacks | ||
1111 | 551 | def test_handleStatus_ABORTED_cancelling_sets_build_log(self): | ||
1112 | 552 | # If a build is intentionally cancelled, the build log is set. | ||
1113 | 553 | self.assertEqual(None, self.build.log) | ||
1114 | 554 | with dbuser(config.builddmaster.dbuser): | ||
1115 | 555 | self.build.updateStatus(BuildStatus.CANCELLING) | ||
1116 | 556 | yield self.behaviour.handleStatus( | ||
1117 | 557 | self.build.buildqueue_record, "ABORTED", {}) | ||
1118 | 558 | self.assertNotEqual(None, self.build.log) | ||
1119 | 559 | |||
1120 | 560 | @defer.inlineCallbacks | ||
1121 | 561 | def test_date_finished_set(self): | ||
1122 | 562 | # The date finished is updated during handleStatus_OK. | ||
1123 | 563 | self.assertEqual(None, self.build.date_finished) | ||
1124 | 564 | with dbuser(config.builddmaster.dbuser): | ||
1125 | 565 | yield self.behaviour.handleStatus( | ||
1126 | 566 | self.build.buildqueue_record, 'OK', | ||
1127 | 567 | {'filemap': self.filemap}) | ||
1128 | 568 | self.assertNotEqual(None, self.build.date_finished) | ||
1129 | 569 | |||
1130 | 570 | @defer.inlineCallbacks | ||
1131 | 571 | def test_givenback_collection(self): | ||
1132 | 572 | with ExpectedException( | ||
1133 | 573 | BuildDaemonError, | ||
1134 | 574 | "Build returned unexpected status: u'GIVENBACK'"): | ||
1135 | 575 | with dbuser(config.builddmaster.dbuser): | ||
1136 | 576 | yield self.behaviour.handleStatus( | ||
1137 | 577 | self.build.buildqueue_record, "GIVENBACK", {}) | ||
1138 | 578 | |||
1139 | 579 | @defer.inlineCallbacks | ||
1140 | 580 | def test_builderfail_collection(self): | ||
1141 | 581 | with ExpectedException( | ||
1142 | 582 | BuildDaemonError, | ||
1143 | 583 | "Build returned unexpected status: u'BUILDERFAIL'"): | ||
1144 | 584 | with dbuser(config.builddmaster.dbuser): | ||
1145 | 585 | yield self.behaviour.handleStatus( | ||
1146 | 586 | self.build.buildqueue_record, "BUILDERFAIL", {}) | ||
1147 | 587 | |||
1148 | 588 | @defer.inlineCallbacks | ||
1149 | 589 | def test_invalid_status_collection(self): | ||
1150 | 590 | with ExpectedException( | ||
1151 | 591 | BuildDaemonError, | ||
1152 | 592 | "Build returned unexpected status: u'BORKED'"): | ||
1153 | 593 | with dbuser(config.builddmaster.dbuser): | ||
1154 | 594 | yield self.behaviour.handleStatus( | ||
1155 | 595 | self.build.buildqueue_record, "BORKED", {}) | ||
1156 | 596 | |||
1157 | 597 | |||
1158 | 598 | class TestGetUploadMethodsForOCIRecipeBuild( | ||
1159 | 599 | MakeOCIBuildMixin, TestGetUploadMethodsMixin, TrialTestCase): | ||
1160 | 600 | """IPackageBuild.getUpload-related methods work with OCI recipe builds.""" | ||
1161 | 601 | |||
1162 | 602 | def setUp(self): | ||
1163 | 603 | super(TestGetUploadMethodsForOCIRecipeBuild, self).__init__(self) | ||
1164 | 604 | self.factory = LaunchpadObjectFactory() | ||
1165 | diff --git a/lib/lp/snappy/model/snapbuildbehaviour.py b/lib/lp/snappy/model/snapbuildbehaviour.py | |||
1166 | index cda3cff..cbcba1d 100644 | |||
1167 | --- a/lib/lp/snappy/model/snapbuildbehaviour.py | |||
1168 | +++ b/lib/lp/snappy/model/snapbuildbehaviour.py | |||
1169 | @@ -11,6 +11,7 @@ from __future__ import absolute_import, print_function, unicode_literals | |||
1170 | 11 | __metaclass__ = type | 11 | __metaclass__ = type |
1171 | 12 | __all__ = [ | 12 | __all__ = [ |
1172 | 13 | 'SnapBuildBehaviour', | 13 | 'SnapBuildBehaviour', |
1173 | 14 | 'SnapProxyMixin', | ||
1174 | 14 | ] | 15 | ] |
1175 | 15 | 16 | ||
1176 | 16 | import base64 | 17 | import base64 |
1177 | @@ -61,9 +62,58 @@ def format_as_rfc3339(timestamp): | |||
1178 | 61 | return timestamp.replace(microsecond=0, tzinfo=None).isoformat() + 'Z' | 62 | return timestamp.replace(microsecond=0, tzinfo=None).isoformat() + 'Z' |
1179 | 62 | 63 | ||
1180 | 63 | 64 | ||
1181 | 65 | class SnapProxyMixin: | ||
1182 | 66 | """Methods for handling builds with the Snap Build Proxy enabled.""" | ||
1183 | 67 | |||
1184 | 68 | @defer.inlineCallbacks | ||
1185 | 69 | def addProxyArgs(self, args, allow_internet=True): | ||
1186 | 70 | if config.snappy.builder_proxy_host and allow_internet: | ||
1187 | 71 | token = yield self._requestProxyToken() | ||
1188 | 72 | args["proxy_url"] = ( | ||
1189 | 73 | "http://{username}:{password}@{host}:{port}".format( | ||
1190 | 74 | username=token['username'], | ||
1191 | 75 | password=token['secret'], | ||
1192 | 76 | host=config.snappy.builder_proxy_host, | ||
1193 | 77 | port=config.snappy.builder_proxy_port)) | ||
1194 | 78 | args["revocation_endpoint"] = ( | ||
1195 | 79 | "{endpoint}/{token}".format( | ||
1196 | 80 | endpoint=config.snappy.builder_proxy_auth_api_endpoint, | ||
1197 | 81 | token=token['username'])) | ||
1198 | 82 | |||
1199 | 83 | @defer.inlineCallbacks | ||
1200 | 84 | def _requestProxyToken(self): | ||
1201 | 85 | admin_username = config.snappy.builder_proxy_auth_api_admin_username | ||
1202 | 86 | if not admin_username: | ||
1203 | 87 | raise CannotBuild( | ||
1204 | 88 | "builder_proxy_auth_api_admin_username is not configured.") | ||
1205 | 89 | secret = config.snappy.builder_proxy_auth_api_admin_secret | ||
1206 | 90 | if not secret: | ||
1207 | 91 | raise CannotBuild( | ||
1208 | 92 | "builder_proxy_auth_api_admin_secret is not configured.") | ||
1209 | 93 | url = config.snappy.builder_proxy_auth_api_endpoint | ||
1210 | 94 | if not secret: | ||
1211 | 95 | raise CannotBuild( | ||
1212 | 96 | "builder_proxy_auth_api_endpoint is not configured.") | ||
1213 | 97 | timestamp = int(time.time()) | ||
1214 | 98 | proxy_username = '{build_id}-{timestamp}'.format( | ||
1215 | 99 | build_id=self.build.build_cookie, | ||
1216 | 100 | timestamp=timestamp) | ||
1217 | 101 | auth_string = '{}:{}'.format(admin_username, secret).strip() | ||
1218 | 102 | auth_header = b'Basic ' + base64.b64encode(auth_string) | ||
1219 | 103 | |||
1220 | 104 | response = yield treq.post( | ||
1221 | 105 | url, headers={'Authorization': auth_header}, | ||
1222 | 106 | json={'username': proxy_username}, | ||
1223 | 107 | reactor=self._slave.reactor, | ||
1224 | 108 | pool=self._slave.pool) | ||
1225 | 109 | response = yield check_status(response) | ||
1226 | 110 | token = yield treq.json_content(response) | ||
1227 | 111 | defer.returnValue(token) | ||
1228 | 112 | |||
1229 | 113 | |||
1230 | 64 | @adapter(ISnapBuild) | 114 | @adapter(ISnapBuild) |
1231 | 65 | @implementer(IBuildFarmJobBehaviour) | 115 | @implementer(IBuildFarmJobBehaviour) |
1233 | 66 | class SnapBuildBehaviour(BuildFarmJobBehaviourBase): | 116 | class SnapBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase): |
1234 | 67 | """Dispatches `SnapBuild` jobs to slaves.""" | 117 | """Dispatches `SnapBuild` jobs to slaves.""" |
1235 | 68 | 118 | ||
1236 | 69 | builder_type = "snap" | 119 | builder_type = "snap" |
1237 | @@ -111,18 +161,7 @@ class SnapBuildBehaviour(BuildFarmJobBehaviourBase): | |||
1238 | 111 | build = self.build | 161 | build = self.build |
1239 | 112 | args = yield super(SnapBuildBehaviour, self).extraBuildArgs( | 162 | args = yield super(SnapBuildBehaviour, self).extraBuildArgs( |
1240 | 113 | logger=logger) | 163 | logger=logger) |
1253 | 114 | if config.snappy.builder_proxy_host and build.snap.allow_internet: | 164 | yield self.addProxyArgs(args, build.snap.allow_internet) |
1242 | 115 | token = yield self._requestProxyToken() | ||
1243 | 116 | args["proxy_url"] = ( | ||
1244 | 117 | "http://{username}:{password}@{host}:{port}".format( | ||
1245 | 118 | username=token['username'], | ||
1246 | 119 | password=token['secret'], | ||
1247 | 120 | host=config.snappy.builder_proxy_host, | ||
1248 | 121 | port=config.snappy.builder_proxy_port)) | ||
1249 | 122 | args["revocation_endpoint"] = ( | ||
1250 | 123 | "{endpoint}/{token}".format( | ||
1251 | 124 | endpoint=config.snappy.builder_proxy_auth_api_endpoint, | ||
1252 | 125 | token=token['username'])) | ||
1254 | 126 | args["name"] = build.snap.store_name or build.snap.name | 165 | args["name"] = build.snap.store_name or build.snap.name |
1255 | 127 | channels = build.channels or {} | 166 | channels = build.channels or {} |
1256 | 128 | if "snapcraft" not in channels: | 167 | if "snapcraft" not in channels: |
1257 | @@ -188,36 +227,6 @@ class SnapBuildBehaviour(BuildFarmJobBehaviourBase): | |||
1258 | 188 | args["build_request_timestamp"] = timestamp | 227 | args["build_request_timestamp"] = timestamp |
1259 | 189 | defer.returnValue(args) | 228 | defer.returnValue(args) |
1260 | 190 | 229 | ||
1261 | 191 | @defer.inlineCallbacks | ||
1262 | 192 | def _requestProxyToken(self): | ||
1263 | 193 | admin_username = config.snappy.builder_proxy_auth_api_admin_username | ||
1264 | 194 | if not admin_username: | ||
1265 | 195 | raise CannotBuild( | ||
1266 | 196 | "builder_proxy_auth_api_admin_username is not configured.") | ||
1267 | 197 | secret = config.snappy.builder_proxy_auth_api_admin_secret | ||
1268 | 198 | if not secret: | ||
1269 | 199 | raise CannotBuild( | ||
1270 | 200 | "builder_proxy_auth_api_admin_secret is not configured.") | ||
1271 | 201 | url = config.snappy.builder_proxy_auth_api_endpoint | ||
1272 | 202 | if not secret: | ||
1273 | 203 | raise CannotBuild( | ||
1274 | 204 | "builder_proxy_auth_api_endpoint is not configured.") | ||
1275 | 205 | timestamp = int(time.time()) | ||
1276 | 206 | proxy_username = '{build_id}-{timestamp}'.format( | ||
1277 | 207 | build_id=self.build.build_cookie, | ||
1278 | 208 | timestamp=timestamp) | ||
1279 | 209 | auth_string = '{}:{}'.format(admin_username, secret).strip() | ||
1280 | 210 | auth_header = b'Basic ' + base64.b64encode(auth_string) | ||
1281 | 211 | |||
1282 | 212 | response = yield treq.post( | ||
1283 | 213 | url, headers={'Authorization': auth_header}, | ||
1284 | 214 | json={'username': proxy_username}, | ||
1285 | 215 | reactor=self._slave.reactor, | ||
1286 | 216 | pool=self._slave.pool) | ||
1287 | 217 | response = yield check_status(response) | ||
1288 | 218 | token = yield treq.json_content(response) | ||
1289 | 219 | defer.returnValue(token) | ||
1290 | 220 | |||
1291 | 221 | def verifySuccessfulBuild(self): | 230 | def verifySuccessfulBuild(self): |
1292 | 222 | """See `IBuildFarmJobBehaviour`.""" | 231 | """See `IBuildFarmJobBehaviour`.""" |
1293 | 223 | # The implementation in BuildFarmJobBehaviourBase checks whether the | 232 | # The implementation in BuildFarmJobBehaviourBase checks whether the |
1294 | diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py | |||
1295 | index 2755523..f851008 100644 | |||
1296 | --- a/lib/lp/snappy/tests/test_snapbuildbehaviour.py | |||
1297 | +++ b/lib/lp/snappy/tests/test_snapbuildbehaviour.py | |||
1298 | @@ -24,7 +24,6 @@ from testtools.matchers import ( | |||
1299 | 24 | AfterPreprocessing, | 24 | AfterPreprocessing, |
1300 | 25 | ContainsDict, | 25 | ContainsDict, |
1301 | 26 | Equals, | 26 | Equals, |
1302 | 27 | HasLength, | ||
1303 | 28 | Is, | 27 | Is, |
1304 | 29 | IsInstance, | 28 | IsInstance, |
1305 | 30 | MatchesDict, | 29 | MatchesDict, |
1306 | @@ -38,13 +37,10 @@ from testtools.twistedsupport import ( | |||
1307 | 38 | import transaction | 37 | import transaction |
1308 | 39 | from twisted.internet import ( | 38 | from twisted.internet import ( |
1309 | 40 | defer, | 39 | defer, |
1310 | 41 | endpoints, | ||
1311 | 42 | reactor, | 40 | reactor, |
1312 | 43 | ) | 41 | ) |
1313 | 44 | from twisted.python.compat import nativeString | ||
1314 | 45 | from twisted.trial.unittest import TestCase as TrialTestCase | 42 | from twisted.trial.unittest import TestCase as TrialTestCase |
1315 | 46 | from twisted.web import ( | 43 | from twisted.web import ( |
1316 | 47 | resource, | ||
1317 | 48 | server, | 44 | server, |
1318 | 49 | xmlrpc, | 45 | xmlrpc, |
1319 | 50 | ) | 46 | ) |
1320 | @@ -71,6 +67,11 @@ from lp.buildmaster.tests.mock_slaves import ( | |||
1321 | 71 | OkSlave, | 67 | OkSlave, |
1322 | 72 | SlaveTestHelpers, | 68 | SlaveTestHelpers, |
1323 | 73 | ) | 69 | ) |
1324 | 70 | from lp.buildmaster.tests.snapbuildproxy import ( | ||
1325 | 71 | InProcessProxyAuthAPIFixture, | ||
1326 | 72 | ProxyURLMatcher, | ||
1327 | 73 | RevocationEndpointMatcher, | ||
1328 | 74 | ) | ||
1329 | 74 | from lp.buildmaster.tests.test_buildfarmjobbehaviour import ( | 75 | from lp.buildmaster.tests.test_buildfarmjobbehaviour import ( |
1330 | 75 | TestGetUploadMethodsMixin, | 76 | TestGetUploadMethodsMixin, |
1331 | 76 | TestHandleStatusMixin, | 77 | TestHandleStatusMixin, |
1332 | @@ -100,7 +101,6 @@ from lp.soyuz.adapters.archivedependencies import ( | |||
1333 | 100 | ) | 101 | ) |
1334 | 101 | from lp.soyuz.enums import PackagePublishingStatus | 102 | from lp.soyuz.enums import PackagePublishingStatus |
1335 | 102 | from lp.soyuz.interfaces.archive import ArchiveDisabled | 103 | from lp.soyuz.interfaces.archive import ArchiveDisabled |
1336 | 103 | from lp.soyuz.interfaces.component import IComponentSet | ||
1337 | 104 | from lp.soyuz.tests.soyuz import Base64KeyMatches | 104 | from lp.soyuz.tests.soyuz import Base64KeyMatches |
1338 | 105 | from lp.testing import ( | 105 | from lp.testing import ( |
1339 | 106 | TestCase, | 106 | TestCase, |
1340 | @@ -116,69 +116,6 @@ from lp.testing.layers import LaunchpadZopelessLayer | |||
1341 | 116 | from lp.xmlrpc.interfaces import IPrivateApplication | 116 | from lp.xmlrpc.interfaces import IPrivateApplication |
1342 | 117 | 117 | ||
1343 | 118 | 118 | ||
1344 | 119 | class ProxyAuthAPITokensResource(resource.Resource): | ||
1345 | 120 | """A test tokens resource for the proxy authentication API.""" | ||
1346 | 121 | |||
1347 | 122 | isLeaf = True | ||
1348 | 123 | |||
1349 | 124 | def __init__(self): | ||
1350 | 125 | resource.Resource.__init__(self) | ||
1351 | 126 | self.requests = [] | ||
1352 | 127 | |||
1353 | 128 | def render_POST(self, request): | ||
1354 | 129 | content = request.content.read() | ||
1355 | 130 | self.requests.append({ | ||
1356 | 131 | "method": request.method, | ||
1357 | 132 | "uri": request.uri, | ||
1358 | 133 | "headers": dict(request.requestHeaders.getAllRawHeaders()), | ||
1359 | 134 | "content": content, | ||
1360 | 135 | }) | ||
1361 | 136 | username = json.loads(content)["username"] | ||
1362 | 137 | return json.dumps({ | ||
1363 | 138 | "username": username, | ||
1364 | 139 | "secret": uuid.uuid4().hex, | ||
1365 | 140 | "timestamp": datetime.utcnow().isoformat(), | ||
1366 | 141 | }) | ||
1367 | 142 | |||
1368 | 143 | |||
1369 | 144 | class InProcessProxyAuthAPIFixture(fixtures.Fixture): | ||
1370 | 145 | """A fixture that pretends to be the proxy authentication API. | ||
1371 | 146 | |||
1372 | 147 | Users of this fixture must call the `start` method, which returns a | ||
1373 | 148 | `Deferred`, and arrange for that to get back to the reactor. This is | ||
1374 | 149 | necessary because the basic fixture API does not allow `setUp` to return | ||
1375 | 150 | anything. For example: | ||
1376 | 151 | |||
1377 | 152 | class TestSomething(TestCase): | ||
1378 | 153 | |||
1379 | 154 | run_tests_with = AsynchronousDeferredRunTest.make_factory( | ||
1380 | 155 | timeout=10) | ||
1381 | 156 | |||
1382 | 157 | @defer.inlineCallbacks | ||
1383 | 158 | def setUp(self): | ||
1384 | 159 | super(TestSomething, self).setUp() | ||
1385 | 160 | yield self.useFixture(InProcessProxyAuthAPIFixture()).start() | ||
1386 | 161 | """ | ||
1387 | 162 | |||
1388 | 163 | @defer.inlineCallbacks | ||
1389 | 164 | def start(self): | ||
1390 | 165 | root = resource.Resource() | ||
1391 | 166 | self.tokens = ProxyAuthAPITokensResource() | ||
1392 | 167 | root.putChild("tokens", self.tokens) | ||
1393 | 168 | endpoint = endpoints.serverFromString(reactor, nativeString("tcp:0")) | ||
1394 | 169 | site = server.Site(self.tokens) | ||
1395 | 170 | self.addCleanup(site.stopFactory) | ||
1396 | 171 | port = yield endpoint.listen(site) | ||
1397 | 172 | self.addCleanup(port.stopListening) | ||
1398 | 173 | config.push("in-process-proxy-auth-api-fixture", dedent(""" | ||
1399 | 174 | [snappy] | ||
1400 | 175 | builder_proxy_auth_api_admin_secret: admin-secret | ||
1401 | 176 | builder_proxy_auth_api_endpoint: http://%s:%s/tokens | ||
1402 | 177 | """) % | ||
1403 | 178 | (port.getHost().host, port.getHost().port)) | ||
1404 | 179 | self.addCleanup(config.pop, "in-process-proxy-auth-api-fixture") | ||
1405 | 180 | |||
1406 | 181 | |||
1407 | 182 | class InProcessAuthServer(xmlrpc.XMLRPC): | 119 | class InProcessAuthServer(xmlrpc.XMLRPC): |
1408 | 183 | 120 | ||
1409 | 184 | def __init__(self, *args, **kwargs): | 121 | def __init__(self, *args, **kwargs): |
1410 | @@ -380,21 +317,6 @@ class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase): | |||
1411 | 380 | self.addCleanup(slave.pool.closeCachedConnections) | 317 | self.addCleanup(slave.pool.closeCachedConnections) |
1412 | 381 | return job | 318 | return job |
1413 | 382 | 319 | ||
1414 | 383 | def getProxyURLMatcher(self, job): | ||
1415 | 384 | return AfterPreprocessing(urlsplit, MatchesStructure( | ||
1416 | 385 | scheme=Equals("http"), | ||
1417 | 386 | username=Equals("{}-{}".format( | ||
1418 | 387 | job.build.build_cookie, int(self.now))), | ||
1419 | 388 | password=HasLength(32), | ||
1420 | 389 | hostname=Equals(config.snappy.builder_proxy_host), | ||
1421 | 390 | port=Equals(config.snappy.builder_proxy_port), | ||
1422 | 391 | path=Equals(""))) | ||
1423 | 392 | |||
1424 | 393 | def getRevocationEndpointMatcher(self, job): | ||
1425 | 394 | return Equals("{}/{}-{}".format( | ||
1426 | 395 | config.snappy.builder_proxy_auth_api_endpoint, | ||
1427 | 396 | job.build.build_cookie, int(self.now))) | ||
1428 | 397 | |||
1429 | 398 | @defer.inlineCallbacks | 320 | @defer.inlineCallbacks |
1430 | 399 | def test_composeBuildRequest(self): | 321 | def test_composeBuildRequest(self): |
1431 | 400 | job = self.makeJob() | 322 | job = self.makeJob() |
1432 | @@ -466,8 +388,8 @@ class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase): | |||
1433 | 466 | "fast_cleanup": Is(True), | 388 | "fast_cleanup": Is(True), |
1434 | 467 | "name": Equals("test-snap"), | 389 | "name": Equals("test-snap"), |
1435 | 468 | "private": Is(False), | 390 | "private": Is(False), |
1438 | 469 | "proxy_url": self.getProxyURLMatcher(job), | 391 | "proxy_url": ProxyURLMatcher(job, self.now), |
1439 | 470 | "revocation_endpoint": self.getRevocationEndpointMatcher(job), | 392 | "revocation_endpoint": RevocationEndpointMatcher(job, self.now), |
1440 | 471 | "series": Equals("unstable"), | 393 | "series": Equals("unstable"), |
1441 | 472 | "trusted_keys": Equals(expected_trusted_keys), | 394 | "trusted_keys": Equals(expected_trusted_keys), |
1442 | 473 | })) | 395 | })) |
1443 | @@ -507,8 +429,8 @@ class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase): | |||
1444 | 507 | "git_path": Equals(ref.name), | 429 | "git_path": Equals(ref.name), |
1445 | 508 | "name": Equals("test-snap"), | 430 | "name": Equals("test-snap"), |
1446 | 509 | "private": Is(False), | 431 | "private": Is(False), |
1449 | 510 | "proxy_url": self.getProxyURLMatcher(job), | 432 | "proxy_url": ProxyURLMatcher(job, self.now), |
1450 | 511 | "revocation_endpoint": self.getRevocationEndpointMatcher(job), | 433 | "revocation_endpoint": RevocationEndpointMatcher(job, self.now), |
1451 | 512 | "series": Equals("unstable"), | 434 | "series": Equals("unstable"), |
1452 | 513 | "trusted_keys": Equals(expected_trusted_keys), | 435 | "trusted_keys": Equals(expected_trusted_keys), |
1453 | 514 | })) | 436 | })) |
1454 | @@ -537,8 +459,8 @@ class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase): | |||
1455 | 537 | "git_repository": Equals(ref.repository.git_https_url), | 459 | "git_repository": Equals(ref.repository.git_https_url), |
1456 | 538 | "name": Equals("test-snap"), | 460 | "name": Equals("test-snap"), |
1457 | 539 | "private": Is(False), | 461 | "private": Is(False), |
1460 | 540 | "proxy_url": self.getProxyURLMatcher(job), | 462 | "proxy_url": ProxyURLMatcher(job, self.now), |
1461 | 541 | "revocation_endpoint": self.getRevocationEndpointMatcher(job), | 463 | "revocation_endpoint": RevocationEndpointMatcher(job, self.now), |
1462 | 542 | "series": Equals("unstable"), | 464 | "series": Equals("unstable"), |
1463 | 543 | "trusted_keys": Equals(expected_trusted_keys), | 465 | "trusted_keys": Equals(expected_trusted_keys), |
1464 | 544 | })) | 466 | })) |
1465 | @@ -584,8 +506,8 @@ class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase): | |||
1466 | 584 | "git_path": Equals(ref.name), | 506 | "git_path": Equals(ref.name), |
1467 | 585 | "name": Equals("test-snap"), | 507 | "name": Equals("test-snap"), |
1468 | 586 | "private": Is(True), | 508 | "private": Is(True), |
1471 | 587 | "proxy_url": self.getProxyURLMatcher(job), | 509 | "proxy_url": ProxyURLMatcher(job, self.now), |
1472 | 588 | "revocation_endpoint": self.getRevocationEndpointMatcher(job), | 510 | "revocation_endpoint": RevocationEndpointMatcher(job, self.now), |
1473 | 589 | "series": Equals("unstable"), | 511 | "series": Equals("unstable"), |
1474 | 590 | "trusted_keys": Equals(expected_trusted_keys), | 512 | "trusted_keys": Equals(expected_trusted_keys), |
1475 | 591 | })) | 513 | })) |
1476 | @@ -616,8 +538,8 @@ class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase): | |||
1477 | 616 | "git_path": Equals("master"), | 538 | "git_path": Equals("master"), |
1478 | 617 | "name": Equals("test-snap"), | 539 | "name": Equals("test-snap"), |
1479 | 618 | "private": Is(False), | 540 | "private": Is(False), |
1482 | 619 | "proxy_url": self.getProxyURLMatcher(job), | 541 | "proxy_url": ProxyURLMatcher(job, self.now), |
1483 | 620 | "revocation_endpoint": self.getRevocationEndpointMatcher(job), | 542 | "revocation_endpoint": RevocationEndpointMatcher(job, self.now), |
1484 | 621 | "series": Equals("unstable"), | 543 | "series": Equals("unstable"), |
1485 | 622 | "trusted_keys": Equals(expected_trusted_keys), | 544 | "trusted_keys": Equals(expected_trusted_keys), |
1486 | 623 | })) | 545 | })) |
1487 | @@ -646,8 +568,8 @@ class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase): | |||
1488 | 646 | "git_repository": Equals(url), | 568 | "git_repository": Equals(url), |
1489 | 647 | "name": Equals("test-snap"), | 569 | "name": Equals("test-snap"), |
1490 | 648 | "private": Is(False), | 570 | "private": Is(False), |
1493 | 649 | "proxy_url": self.getProxyURLMatcher(job), | 571 | "proxy_url": ProxyURLMatcher(job, self.now), |
1494 | 650 | "revocation_endpoint": self.getRevocationEndpointMatcher(job), | 572 | "revocation_endpoint": RevocationEndpointMatcher(job, self.now), |
1495 | 651 | "series": Equals("unstable"), | 573 | "series": Equals("unstable"), |
1496 | 652 | "trusted_keys": Equals(expected_trusted_keys), | 574 | "trusted_keys": Equals(expected_trusted_keys), |
1497 | 653 | })) | 575 | })) |
1498 | @@ -808,7 +730,7 @@ class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase): | |||
1499 | 808 | job = self.makeJob() | 730 | job = self.makeJob() |
1500 | 809 | build_request = yield job.composeBuildRequest(None) | 731 | build_request = yield job.composeBuildRequest(None) |
1501 | 810 | self.assertThat( | 732 | self.assertThat( |
1503 | 811 | build_request[4]["proxy_url"], self.getProxyURLMatcher(job)) | 733 | build_request[4]["proxy_url"], ProxyURLMatcher(job, self.now)) |
1504 | 812 | 734 | ||
1505 | 813 | @defer.inlineCallbacks | 735 | @defer.inlineCallbacks |
1506 | 814 | def test_composeBuildRequest_deleted(self): | 736 | def test_composeBuildRequest_deleted(self): |
1507 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py | |||
1508 | index 85a05b6..c402c4c 100644 | |||
1509 | --- a/lib/lp/testing/factory.py | |||
1510 | +++ b/lib/lp/testing/factory.py | |||
1511 | @@ -4991,9 +4991,15 @@ class BareLaunchpadObjectFactory(ObjectFactory): | |||
1512 | 4991 | if requester is None: | 4991 | if requester is None: |
1513 | 4992 | requester = self.makePerson() | 4992 | requester = self.makePerson() |
1514 | 4993 | if distro_arch_series is None: | 4993 | if distro_arch_series is None: |
1516 | 4994 | distro_arch_series = self.makeDistroArchSeries() | 4994 | distroseries = self.makeDistroSeries(status=SeriesStatus.CURRENT) |
1517 | 4995 | processor = getUtility(IProcessorSet).getByName("386") | ||
1518 | 4996 | distro_arch_series = self.makeDistroArchSeries( | ||
1519 | 4997 | distroseries=distroseries, architecturetag="i386", | ||
1520 | 4998 | processor=processor) | ||
1521 | 4995 | if recipe is None: | 4999 | if recipe is None: |
1523 | 4996 | recipe = self.makeOCIRecipe() | 5000 | oci_project = self.makeOCIProject( |
1524 | 5001 | pillar=distro_arch_series.distroseries.distribution) | ||
1525 | 5002 | recipe = self.makeOCIRecipe(oci_project=oci_project) | ||
1526 | 4997 | oci_build = getUtility(IOCIRecipeBuildSet).new( | 5003 | oci_build = getUtility(IOCIRecipeBuildSet).new( |
1527 | 4998 | requester, recipe, distro_arch_series, date_created) | 5004 | requester, recipe, distro_arch_series, date_created) |
1528 | 4999 | if duration is not None: | 5005 | if duration is not None: |
1529 | @@ -5010,12 +5016,13 @@ class BareLaunchpadObjectFactory(ObjectFactory): | |||
1530 | 5010 | return oci_build | 5016 | return oci_build |
1531 | 5011 | 5017 | ||
1532 | 5012 | def makeOCIFile(self, build=None, library_file=None, | 5018 | def makeOCIFile(self, build=None, library_file=None, |
1534 | 5013 | layer_file_digest=None): | 5019 | layer_file_digest=None, content=None, filename=None): |
1535 | 5014 | """Make a new OCIFile.""" | 5020 | """Make a new OCIFile.""" |
1536 | 5015 | if build is None: | 5021 | if build is None: |
1537 | 5016 | build = self.makeOCIRecipeBuild() | 5022 | build = self.makeOCIRecipeBuild() |
1538 | 5017 | if library_file is None: | 5023 | if library_file is None: |
1540 | 5018 | library_file = self.makeLibraryFileAlias() | 5024 | library_file = self.makeLibraryFileAlias( |
1541 | 5025 | content=content, filename=filename) | ||
1542 | 5019 | return OCIFile(build=build, library_file=library_file, | 5026 | return OCIFile(build=build, library_file=library_file, |
1543 | 5020 | layer_file_digest=layer_file_digest) | 5027 | layer_file_digest=layer_file_digest) |
1544 | 5021 | 5028 | ||
1545 | diff --git a/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py b/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py | |||
1546 | index b387251..32dd553 100644 | |||
1547 | --- a/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py | |||
1548 | +++ b/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py | |||
1549 | @@ -138,7 +138,7 @@ class TestTranslationTemplatesBuildBehaviour( | |||
1550 | 138 | buildqueue = FakeBuildQueue(behaviour) | 138 | buildqueue = FakeBuildQueue(behaviour) |
1551 | 139 | path = behaviour.templates_tarball_path | 139 | path = behaviour.templates_tarball_path |
1552 | 140 | # Poke the file we're expecting into the mock slave. | 140 | # Poke the file we're expecting into the mock slave. |
1554 | 141 | behaviour._slave.valid_file_hashes.append(path) | 141 | behaviour._slave.valid_files[path] = '' |
1555 | 142 | 142 | ||
1556 | 143 | def got_tarball(filename): | 143 | def got_tarball(filename): |
1557 | 144 | tarball = open(filename, 'r') | 144 | tarball = open(filename, 'r') |