Merge ~twom/launchpad:oci-buildbehaviour into launchpad:master

Proposed by Tom Wardill
Status: Superseded
Proposed branch: ~twom/launchpad:oci-buildbehaviour
Merge into: launchpad:master
Prerequisite: ~twom/launchpad:oci-buildjob
Diff against target: 877 lines (+473/-79)
11 files modified
dev/null (+0/-49)
lib/lp/buildmaster/model/buildfarmjobbehaviour.py (+17/-13)
lib/lp/buildmaster/tests/mock_slaves.py (+9/-3)
lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py (+1/-1)
lib/lp/oci/configure.zcml (+6/-0)
lib/lp/oci/model/ocirecipe.py (+0/-10)
lib/lp/oci/model/ocirecipebuild.py (+9/-0)
lib/lp/oci/model/ocirecipebuildbehaviour.py (+115/-0)
lib/lp/oci/tests/test_ocirecipebuildbehaviour.py (+312/-0)
lib/lp/testing/factory.py (+3/-2)
lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py (+1/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Needs Fixing
Review via email: mp+376678@code.launchpad.net

This proposal has been superseded by a proposal from 2020-02-14.

Commit message

Add OCIRecipeBuildBehaviour

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) :
review: Needs Fixing
Revision history for this message
Tom Wardill (twom) :
Revision history for this message
Colin Watson (cjwatson) :

There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/buildmaster/model/buildfarmjobbehaviour.py b/lib/lp/buildmaster/model/buildfarmjobbehaviour.py
index 2f3515a..4b28693 100644
--- a/lib/lp/buildmaster/model/buildfarmjobbehaviour.py
+++ b/lib/lp/buildmaster/model/buildfarmjobbehaviour.py
@@ -301,6 +301,22 @@ class BuildFarmJobBehaviourBase:
301 transaction.commit()301 transaction.commit()
302302
303 @defer.inlineCallbacks303 @defer.inlineCallbacks
304 def _downloadFiles(self, filemap, upload_path, logger):
305 filenames_to_download = []
306 for filename, sha1 in filemap.items():
307 logger.info("Grabbing file: %s (%s)" % (
308 filename, self._slave.getURL(sha1)))
309 out_file_name = os.path.join(upload_path, filename)
310 # If the evaluated output file name is not within our
311 # upload path, then we don't try to copy this or any
312 # subsequent files.
313 if not os.path.realpath(out_file_name).startswith(upload_path):
314 raise BuildDaemonError(
315 "Build returned a file named %r." % filename)
316 filenames_to_download.append((sha1, out_file_name))
317 yield self._slave.getFiles(filenames_to_download, logger=logger)
318
319 @defer.inlineCallbacks
304 def handleSuccess(self, slave_status, logger):320 def handleSuccess(self, slave_status, logger):
305 """Handle a package that built successfully.321 """Handle a package that built successfully.
306322
@@ -337,19 +353,7 @@ class BuildFarmJobBehaviourBase:
337 grab_dir, str(build.archive.id), build.distribution.name)353 grab_dir, str(build.archive.id), build.distribution.name)
338 os.makedirs(upload_path)354 os.makedirs(upload_path)
339355
340 filenames_to_download = []356 yield self._downloadFiles(filemap, upload_path, logger)
341 for filename, sha1 in filemap.items():
342 logger.info("Grabbing file: %s (%s)" % (
343 filename, self._slave.getURL(sha1)))
344 out_file_name = os.path.join(upload_path, filename)
345 # If the evaluated output file name is not within our
346 # upload path, then we don't try to copy this or any
347 # subsequent files.
348 if not os.path.realpath(out_file_name).startswith(upload_path):
349 raise BuildDaemonError(
350 "Build returned a file named %r." % filename)
351 filenames_to_download.append((sha1, out_file_name))
352 yield self._slave.getFiles(filenames_to_download, logger=logger)
353357
354 transaction.commit()358 transaction.commit()
355359
diff --git a/lib/lp/buildmaster/tests/mock_slaves.py b/lib/lp/buildmaster/tests/mock_slaves.py
index 8b00c32..eca171a 100644
--- a/lib/lp/buildmaster/tests/mock_slaves.py
+++ b/lib/lp/buildmaster/tests/mock_slaves.py
@@ -194,7 +194,8 @@ class WaitingSlave(OkSlave):
194194
195 # By default, the slave only has a buildlog, but callsites195 # By default, the slave only has a buildlog, but callsites
196 # can update this list as needed.196 # can update this list as needed.
197 self.valid_file_hashes = ['buildlog']197 self.valid_files = {'buildlog': ''}
198 self._got_file_record = []
198199
199 def status(self):200 def status(self):
200 self.call_log.append('status')201 self.call_log.append('status')
@@ -208,12 +209,17 @@ class WaitingSlave(OkSlave):
208209
209 def getFile(self, hash, file_to_write):210 def getFile(self, hash, file_to_write):
210 self.call_log.append('getFile')211 self.call_log.append('getFile')
211 if hash in self.valid_file_hashes:212 if hash in self.valid_files:
212 content = "This is a %s" % hash
213 if isinstance(file_to_write, types.StringTypes):213 if isinstance(file_to_write, types.StringTypes):
214 file_to_write = open(file_to_write, 'wb')214 file_to_write = open(file_to_write, 'wb')
215 if not self.valid_files[hash]:
216 content = "This is a %s" % hash
217 else:
218 with open(self.valid_files[hash], 'rb') as source:
219 content = source.read()
215 file_to_write.write(content)220 file_to_write.write(content)
216 file_to_write.close()221 file_to_write.close()
222 self._got_file_record.append(hash)
217 return defer.succeed(None)223 return defer.succeed(None)
218224
219225
diff --git a/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py b/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py
index 697ff52..d6ecd85 100644
--- a/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py
+++ b/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py
@@ -337,7 +337,7 @@ class TestHandleStatusMixin:
337 self.builder = self.factory.makeBuilder()337 self.builder = self.factory.makeBuilder()
338 self.build.buildqueue_record.markAsBuilding(self.builder)338 self.build.buildqueue_record.markAsBuilding(self.builder)
339 self.slave = WaitingSlave('BuildStatus.OK')339 self.slave = WaitingSlave('BuildStatus.OK')
340 self.slave.valid_file_hashes.append('test_file_hash')340 self.slave.valid_file_hashes['test_file_hash'] = ''
341 self.interactor = BuilderInteractor()341 self.interactor = BuilderInteractor()
342 self.behaviour = self.interactor.getBuildBehaviour(342 self.behaviour = self.interactor.getBuildBehaviour(
343 self.build.buildqueue_record, self.builder, self.slave)343 self.build.buildqueue_record, self.builder, self.slave)
diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml
index 30212b9..51e1d31 100644
--- a/lib/lp/oci/configure.zcml
+++ b/lib/lp/oci/configure.zcml
@@ -56,4 +56,10 @@
56 <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />56 <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />
57 </securedutility>57 </securedutility>
5858
59 <adapter
60 for="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuild"
61 provides="lp.buildmaster.interfaces.buildfarmjobbehaviour.IBuildFarmJobBehaviour"
62 factory="lp.oci.model.ocirecipebuildbehaviour.OCIRecipeBuildBehaviour"
63 permission="zope.Public" />
64
59</configure>65</configure>
diff --git a/lib/lp/oci/interfaces/ocirecipebuildjob.py b/lib/lp/oci/interfaces/ocirecipebuildjob.py
60deleted file mode 10064466deleted file mode 100644
index cd72177..0000000
--- a/lib/lp/oci/interfaces/ocirecipebuildjob.py
+++ /dev/null
@@ -1,38 +0,0 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""OCIRecipe build job interfaces"""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'IOCIRecipeBuildJob',
11 ]
12
13from lazr.restful.fields import Reference
14from zope.interface import (
15 Attribute,
16 Interface,
17 )
18from zope.schema import TextLine
19
20from lp import _
21from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
22from lp.services.job.interfaces.job import (
23 IJob,
24 IJobSource,
25 IRunnableJob,
26 )
27
28
29class IOCIRecipeBuildJob(Interface):
30 job = Reference(
31 title=_("The common Job attributes."), schema=IJob,
32 required=True, readonly=True)
33
34 build = Reference(
35 title=_("The OCI Recipe Build to use for this job."),
36 schema=IOCIRecipeBuild, required=True, readonly=True)
37
38 json_data = Attribute(_("A dict of data about the job."))
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 300f335..8a01b2d 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -308,15 +308,5 @@ class OCIRecipeSet:
308 person_ids.add(recipe.registrant_id)308 person_ids.add(recipe.registrant_id)
309 person_ids.add(recipe.owner_id)309 person_ids.add(recipe.owner_id)
310310
311 repositories = load_related(
312 GitRepository, recipes, ["git_repository_id"])
313 if repositories:
314 GenericGitCollection.preloadDataForRepositories(repositories)
315 GenericGitCollection.preloadVisibleRepositories(repositories, user)
316
317 # We need the target repository owner as well; unlike branches,
318 # repository unique names aren't trigger-maintained.
319 person_ids.update(repository.owner_id for repository in repositories)
320
321 list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(311 list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
322 person_ids, need_validity=True))312 person_ids, need_validity=True))
diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
index 307fbd2..5d2d21d 100644
--- a/lib/lp/oci/model/ocirecipebuild.py
+++ b/lib/lp/oci/model/ocirecipebuild.py
@@ -44,6 +44,7 @@ from lp.oci.interfaces.ocirecipebuild import (
44 IOCIRecipeBuildSet,44 IOCIRecipeBuildSet,
45 )45 )
46from lp.registry.model.person import Person46from lp.registry.model.person import Person
47from lp.services.config import config
47from lp.services.database.bulk import load_related48from lp.services.database.bulk import load_related
48from lp.services.database.constants import DEFAULT49from lp.services.database.constants import DEFAULT
49from lp.services.database.decoratedresultset import DecoratedResultSet50from lp.services.database.decoratedresultset import DecoratedResultSet
@@ -208,6 +209,14 @@ class OCIRecipeBuild(PackageBuildMixin, Storm):
208 # pillar isn't just a distribution209 # pillar isn't just a distribution
209 return self.recipe.oci_project.distribution210 return self.recipe.oci_project.distribution
210211
212 def notify(self, extra_info=None):
213 """See `IPackageBuild`."""
214 if not config.builddmaster.send_build_notification:
215 return
216 if self.status == BuildStatus.FULLYBUILT:
217 return
218 # XXX twom 2019-12-11 This should send mail
219
211220
212@implementer(IOCIRecipeBuildSet)221@implementer(IOCIRecipeBuildSet)
213class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin):222class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
diff --git a/lib/lp/oci/model/ocirecipebuildbehaviour.py b/lib/lp/oci/model/ocirecipebuildbehaviour.py
214new file mode 100644223new file mode 100644
index 0000000..e1a1215
--- /dev/null
+++ b/lib/lp/oci/model/ocirecipebuildbehaviour.py
@@ -0,0 +1,115 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""An `IBuildFarmJobBehaviour` for `OCIRecipeBuild`.
5
6Dispatches OCI image build jobs to build-farm slaves.
7"""
8
9from __future__ import absolute_import, print_function, unicode_literals
10
11__metaclass__ = type
12__all__ = [
13 'OCIRecipeBuildBehaviour',
14 ]
15
16
17import json
18import os
19
20from twisted.internet import defer
21from zope.interface import implementer
22
23from lp.app.errors import NotFoundError
24from lp.buildmaster.enums import BuildBaseImageType
25from lp.buildmaster.interfaces.builder import BuildDaemonError
26from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
27 IBuildFarmJobBehaviour,
28 )
29from lp.buildmaster.model.buildfarmjobbehaviour import (
30 BuildFarmJobBehaviourBase,
31 )
32from lp.services.librarian.utils import copy_and_close
33
34
35@implementer(IBuildFarmJobBehaviour)
36class OCIRecipeBuildBehaviour(BuildFarmJobBehaviourBase):
37
38 builder_type = "oci"
39 image_types = [BuildBaseImageType.LXD]
40
41 # These attributes are defined in `IOCIBuildFarmJobBehaviour`,
42 # but are not used in this implementation.
43 distro_arch_series = None
44
45 def _ensureFilePath(self, file_name, file_path, upload_path):
46 # If the evaluated output file name is not within our
47 # upload path, then we don't try to copy this or any
48 # subsequent files.
49 if not os.path.normpath(file_path).startswith(upload_path):
50 raise BuildDaemonError(
51 "Build returned a file named '%s'." % file_name)
52
53 def _fetchIntermediaryFile(self, name, filemap, upload_path):
54 file_hash = filemap[name]
55 file_path = os.path.join(upload_path, name)
56 self._ensureFilePath(name, file_path, upload_path)
57 self._slave.getFile(file_hash, file_path)
58
59 with open(file_path, 'r') as file_fp:
60 contents = json.load(file_fp)
61 return contents
62
63 def _extractLayerFiles(self, upload_path, section, config, digests, files):
64 # These are different sets of ids, in the same order
65 # layer_id is the filename, diff_id is the internal (docker) id
66 for diff_id in config['rootfs']['diff_ids']:
67 layer_id = digests[diff_id]['layer_id']
68 # This is in the form '<id>/layer.tar', we only need the first
69 layer_filename = "{}.tar.gz".format(layer_id.split('/')[0])
70 digest = digests[diff_id]['digest']
71 try:
72 _, librarian_layer_file, _ = self.build.getLayerFileByDigest(
73 digest)
74 except NotFoundError:
75 files.add(layer_filename)
76 continue
77 layer_path = os.path.join(upload_path, layer_filename)
78 librarian_layer_file.open()
79 with open(layer_path, 'wb') as layer_fp:
80 copy_and_close(librarian_layer_file, layer_fp)
81
82 def _convertToRetrievableFile(self, upload_path, file_name, filemap):
83 file_path = os.path.join(upload_path, file_name)
84 self._ensureFilePath(file_name, file_path, upload_path)
85 return (filemap[file_name], file_path)
86
87 @defer.inlineCallbacks
88 def _downloadFiles(self, filemap, upload_path, logger):
89 """Download required artifact files."""
90 # We don't want to download all of the files that have been created,
91 # just the ones that are mentioned in the manifest and config.
92
93 manifest = self._fetchIntermediaryFile(
94 'manifest.json', filemap, upload_path)
95 digests = self._fetchIntermediaryFile(
96 'digests.json', filemap, upload_path)
97
98 files = set()
99 for section in manifest:
100 config = self._fetchIntermediaryFile(
101 section['Config'], filemap, upload_path)
102 self._extractLayerFiles(
103 upload_path, section, config, digests, files)
104
105 files_to_download = [
106 self._convertToRetrievableFile(upload_path, filename, filemap)
107 for filename in files]
108 yield self._slave.getFiles(files_to_download, logger=logger)
109
110 def verifySuccessfulBuild(self):
111 """See `IBuildFarmJobBehaviour`."""
112 # The implementation in BuildFarmJobBehaviourBase checks whether the
113 # target suite is modifiable in the target archive. However, an
114 # `OCIRecipeBuild` does not use an archive in this manner.
115 return True
diff --git a/lib/lp/oci/model/ocirecipebuildjob.py b/lib/lp/oci/model/ocirecipebuildjob.py
0deleted file mode 100644116deleted file mode 100644
index 6df00cf..0000000
--- a/lib/lp/oci/model/ocirecipebuildjob.py
+++ /dev/null
@@ -1,143 +0,0 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""OCIRecipe build jobs."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'OCIRecipeBuildJob',
11 'OCIRecipeBuildJobType',
12 ]
13
14import json
15
16from lazr.delegates import delegate_to
17from lazr.enum import (
18 DBEnumeratedType,
19 DBItem,
20 )
21from storm.locals import (
22 Int,
23 JSON,
24 Reference,
25 )
26from zope.interface import (
27 implementer,
28 provider,
29 )
30
31from lp.app.errors import NotFoundError
32from lp.oci.interfaces.ocirecipebuildjob import IOCIRecipeBuildJob
33from lp.services.database.enumcol import DBEnum
34from lp.services.database.interfaces import (
35 IMasterStore,
36 IStore,
37 )
38from lp.services.database.stormbase import StormBase
39from lp.services.job.model.job import (
40 EnumeratedSubclass,
41 Job,
42 )
43from lp.services.job.runner import BaseRunnableJob
44from lp.services.propertycache import get_property_cache
45
46
47class OCIRecipeBuildJobType(DBEnumeratedType):
48 """Values that `OCIBuildJobType.job_type` can take."""
49
50 REGISTRY_UPLOAD = DBItem(0, """
51 Registry upload
52
53 This job uploads an OCI Image to registry.
54 """)
55
56
57@implementer(IOCIRecipeBuildJob)
58class OCIRecipeBuildJob(StormBase):
59 """See `IOCIRecipeBuildJob`."""
60
61 __storm_table__ = 'OCIRecipeBuildJob'
62
63 job_id = Int(name='job', primary=True, allow_none=False)
64 job = Reference(job_id, 'Job.id')
65
66 build_id = Int(name='build', allow_none=False)
67 build = Reference(build_id, 'OCIRecipeBuild.id')
68
69 job_type = DBEnum(enum=OCIRecipeBuildJobType, allow_none=True)
70
71 json_data = JSON('json_data', allow_none=False)
72
73 def __init__(self, build, job_type, json_data, **job_args):
74 """Constructor.
75
76 Extra keyword arguments are used to construct the underlying Job
77 object.
78
79 :param build: The `IOCIRecipeBuild` this job relates to.
80 :param job_type: The `OCIRecipeBuildJobType` of this job.
81 :param json_data: The type-specific variables, as a JSON-compatible
82 dict.
83 """
84 super(OCIRecipeBuildJob, self).__init__()
85 self.job = Job(**job_args)
86 self.build = build
87 self.job_type = job_type
88 self.json_data = json_data
89
90 def makeDerived(self):
91 return OCIRecipeBuildJob.makeSubclass(self)
92
93
94@delegate_to(IOCIRecipeBuildJob)
95class OCIRecipeBuildJobDerived(BaseRunnableJob):
96
97 __metaclass__ = EnumeratedSubclass
98
99 def __init__(self, oci_build_job):
100 self.context = oci_build_job
101
102 def __repr__(self):
103 """An informative representation of the job."""
104 return "<%s for %s>" % (
105 self.__class__.__name__, self.build.id)
106
107 @classmethod
108 def get(cls, job_id):
109 """Get a job by id.
110
111 :return: The `OCIBuildJob` with the specified id, as the current
112 `OCIBuildJobDerived` subclass.
113 :raises: `NotFoundError` if there is no job with the specified id,
114 or its `job_type` does not match the desired subclass.
115 """
116 oci_build_job = IStore(OCIRecipeBuildJob).get(
117 OCIRecipeBuildJob, job_id)
118 if oci_build_job.job_type != cls.class_job_type:
119 raise NotFoundError(
120 "No object found with id %d and type %s" %
121 (job_id, cls.class_job_type.title))
122 return cls(oci_build_job)
123
124 @classmethod
125 def iterReady(cls):
126 """See `IJobSource`."""
127 jobs = IMasterStore(OCIRecipeBuildJob).find(
128 OCIRecipeBuildJob,
129 OCIRecipeBuildJob.job_type == cls.class_job_type,
130 OCIRecipeBuildJob.job == Job.id,
131 Job.id.is_in(Job.ready_jobs))
132 return (cls(job) for job in jobs)
133
134 def getOopsVars(self):
135 """See `IRunnableJob`."""
136 oops_vars = super(OCIRecipeBuildJobDerived, self).getOopsVars()
137 oops_vars.extend([
138 ('job_type', self.context.job_type.title),
139 ('build_id', self.context.build.id),
140 ('owner_id', self.context.build.recipe.owner.id),
141 ('project_name', self.context.build.recipe.ociproject.name)
142 ])
143 return oops_vars
diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
144new file mode 1006440new file mode 100644
index 0000000..56d3592
--- /dev/null
+++ b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
@@ -0,0 +1,312 @@
1# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for `OCIRecipeBuildBehavior`."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10import json
11import os
12import shutil
13import tempfile
14
15from testtools import ExpectedException
16from twisted.internet import defer
17from zope.security.proxy import removeSecurityProxy
18
19from lp.buildmaster.enums import BuildStatus
20from lp.buildmaster.interactor import BuilderInteractor
21from lp.buildmaster.interfaces.builder import BuildDaemonError
22from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
23 IBuildFarmJobBehaviour,
24 )
25from lp.buildmaster.tests.mock_slaves import WaitingSlave
26from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
27 TestGetUploadMethodsMixin,
28 )
29from lp.oci.model.ocirecipebuildbehaviour import OCIRecipeBuildBehaviour
30from lp.services.config import config
31from lp.testing import TestCaseWithFactory
32from lp.testing.dbuser import dbuser
33from lp.testing.factory import LaunchpadObjectFactory
34from lp.testing.fakemethod import FakeMethod
35from lp.testing.layers import LaunchpadZopelessLayer
36from lp.testing.mail_helpers import pop_notifications
37
38
39class MakeOCIBuildMixin:
40
41 def makeBuild(self):
42 build = self.factory.makeOCIRecipeBuild()
43 build.queueBuild()
44 return build
45
46 def makeUnmodifiableBuild(self):
47 build = self.factory.makeOCIRecipeBuild()
48 build.distro_arch_series = 'failed'
49 build.queueBuild()
50 return build
51
52
53class TestOCIBuildBehaviour(TestCaseWithFactory):
54
55 layer = LaunchpadZopelessLayer
56
57 def test_provides_interface(self):
58 # OCIRecipeBuildBehaviour provides IBuildFarmJobBehaviour.
59 job = OCIRecipeBuildBehaviour(None)
60 self.assertProvides(job, IBuildFarmJobBehaviour)
61
62 def test_adapts_IOCIRecipeBuild(self):
63 # IBuildFarmJobBehaviour adapts an IOCIRecipeBuild.
64 build = self.factory.makeOCIRecipeBuild()
65 job = IBuildFarmJobBehaviour(build)
66 self.assertProvides(job, IBuildFarmJobBehaviour)
67
68
69class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
70 TestCaseWithFactory):
71 # This is mostly copied from TestHandleStatusMixin, however
72 # we can't use all of those tests, due to the way OCIRecipeBuildBehaviour
73 # parses the file contents, rather than just retrieving all that are
74 # available. There's also some differences in the filemap handling, as
75 # we need a much more complex filemap here.
76
77 layer = LaunchpadZopelessLayer
78
79 def _createTestFile(self, name, content, hash):
80 path = os.path.join(self.test_files_dir, name)
81 with open(path, 'wb') as fp:
82 fp.write(content)
83 self.slave.valid_files[hash] = path
84
85 def setUp(self):
86 super(TestHandleStatusForOCIRecipeBuild, self).setUp()
87 self.factory = LaunchpadObjectFactory()
88 self.build = self.makeBuild()
89 # For the moment, we require a builder for the build so that
90 # handleStatus_OK can get a reference to the slave.
91 self.builder = self.factory.makeBuilder()
92 self.build.buildqueue_record.markAsBuilding(self.builder)
93 self.slave = WaitingSlave('BuildStatus.OK')
94 self.slave.valid_files['test_file_hash'] = ''
95 self.interactor = BuilderInteractor()
96 self.behaviour = self.interactor.getBuildBehaviour(
97 self.build.buildqueue_record, self.builder, self.slave)
98
99 # We overwrite the buildmaster root to use a temp directory.
100 tempdir = tempfile.mkdtemp()
101 self.addCleanup(shutil.rmtree, tempdir)
102 self.upload_root = tempdir
103 self.pushConfig('builddmaster', root=self.upload_root)
104
105 # We stub out our builds getUploaderCommand() method so
106 # we can check whether it was called as well as
107 # verifySuccessfulUpload().
108 removeSecurityProxy(self.build).verifySuccessfulUpload = FakeMethod(
109 result=True)
110
111 digests = {
112 "diff_id_1": {
113 "digest": "digest_1",
114 "source": "test/base_1",
115 "layer_id": "layer_1"
116 },
117 "diff_id_2": {
118 "digest": "digest_2",
119 "source": "",
120 "layer_id": "layer_2"
121 }
122 }
123
124 self.test_files_dir = tempfile.mkdtemp()
125 self._createTestFile('buildlog', '', 'buildlog')
126 self._createTestFile(
127 'manifest.json',
128 '[{"Config": "config_file_1.json", '
129 '"Layers": ["layer_1/layer.tar", "layer_2/layer.tar"]}]',
130 'manifest_hash')
131 self._createTestFile(
132 'digests.json',
133 json.dumps(digests),
134 'digests_hash')
135 self._createTestFile(
136 'config_file_1.json',
137 '{"rootfs": {"diff_ids": ["diff_id_1", "diff_id_2"]}}',
138 'config_1_hash')
139 self._createTestFile(
140 'layer_2.tar.gz',
141 '',
142 'layer_2_hash'
143 )
144
145 self.filemap = {
146 'manifest.json': 'manifest_hash',
147 'digests.json': 'digests_hash',
148 'config_file_1.json': 'config_1_hash',
149 'layer_1.tar.gz': 'layer_1_hash',
150 'layer_2.tar.gz': 'layer_2_hash'
151 }
152 self.factory.makeOCIFile(
153 build=self.build, layer_file_digest=u'digest_1',
154 content="retrieved from librarian")
155
156 def assertResultCount(self, count, result):
157 self.assertEqual(
158 1, len(os.listdir(os.path.join(self.upload_root, result))))
159
160 @defer.inlineCallbacks
161 def test_handleStatus_OK_normal_image(self):
162 with dbuser(config.builddmaster.dbuser):
163 yield self.behaviour.handleStatus(
164 self.build.buildqueue_record, 'OK',
165 {'filemap': self.filemap})
166 self.assertEqual(
167 ['buildlog', 'manifest_hash', 'digests_hash', 'config_1_hash',
168 'layer_2_hash'],
169 self.slave._got_file_record)
170 # This hash should not appear as it is already in the librarian
171 self.assertNotIn('layer_1_hash', self.slave._got_file_record)
172 self.assertEqual(BuildStatus.UPLOADING, self.build.status)
173 self.assertResultCount(1, "incoming")
174
175 # layer_1 should have been retrieved from the librarian
176 layer_1_path = os.path.join(
177 self.upload_root,
178 "incoming",
179 self.behaviour.getUploadDirLeaf(self.build.build_cookie),
180 str(self.build.archive.id),
181 self.build.distribution.name,
182 "layer_1.tar.gz"
183 )
184 with open(layer_1_path, 'rb') as layer_1_fp:
185 contents = layer_1_fp.read()
186 self.assertEqual(contents, b'retrieved from librarian')
187
188 @defer.inlineCallbacks
189 def test_handleStatus_OK_absolute_filepath(self):
190
191 self._createTestFile(
192 'manifest.json',
193 '[{"Config": "/notvalid/config_file_1.json", '
194 '"Layers": ["layer_1/layer.tar", "layer_2/layer.tar"]}]',
195 'manifest_hash')
196
197 self.filemap['/notvalid/config_file_1.json'] = 'config_1_hash'
198
199 # A filemap that tries to write to files outside of the upload
200 # directory will not be collected.
201 with ExpectedException(
202 BuildDaemonError,
203 "Build returned a file named "
204 "'/notvalid/config_file_1.json'."):
205 with dbuser(config.builddmaster.dbuser):
206 yield self.behaviour.handleStatus(
207 self.build.buildqueue_record, 'OK',
208 {'filemap': self.filemap})
209
210 @defer.inlineCallbacks
211 def test_handleStatus_OK_relative_filepath(self):
212
213 self._createTestFile(
214 'manifest.json',
215 '[{"Config": "../config_file_1.json", '
216 '"Layers": ["layer_1/layer.tar", "layer_2/layer.tar"]}]',
217 'manifest_hash')
218
219 self.filemap['../config_file_1.json'] = 'config_1_hash'
220 # A filemap that tries to write to files outside of
221 # the upload directory will not be collected.
222 with ExpectedException(
223 BuildDaemonError,
224 "Build returned a file named '../config_file_1.json'."):
225 with dbuser(config.builddmaster.dbuser):
226 yield self.behaviour.handleStatus(
227 self.build.buildqueue_record, 'OK',
228 {'filemap': self.filemap})
229
230 @defer.inlineCallbacks
231 def test_handleStatus_OK_sets_build_log(self):
232 # The build log is set during handleStatus.
233 self.assertEqual(None, self.build.log)
234 with dbuser(config.builddmaster.dbuser):
235 yield self.behaviour.handleStatus(
236 self.build.buildqueue_record, 'OK',
237 {'filemap': self.filemap})
238 self.assertNotEqual(None, self.build.log)
239
240 @defer.inlineCallbacks
241 def test_handleStatus_ABORTED_cancels_cancelling(self):
242 with dbuser(config.builddmaster.dbuser):
243 self.build.updateStatus(BuildStatus.CANCELLING)
244 yield self.behaviour.handleStatus(
245 self.build.buildqueue_record, "ABORTED", {})
246 self.assertEqual(0, len(pop_notifications()), "Notifications received")
247 self.assertEqual(BuildStatus.CANCELLED, self.build.status)
248
249 @defer.inlineCallbacks
250 def test_handleStatus_ABORTED_illegal_when_building(self):
251 self.builder.vm_host = "fake_vm_host"
252 self.behaviour = self.interactor.getBuildBehaviour(
253 self.build.buildqueue_record, self.builder, self.slave)
254 with dbuser(config.builddmaster.dbuser):
255 self.build.updateStatus(BuildStatus.BUILDING)
256 with ExpectedException(
257 BuildDaemonError,
258 "Build returned unexpected status: u'ABORTED'"):
259 yield self.behaviour.handleStatus(
260 self.build.buildqueue_record, "ABORTED", {})
261
262 @defer.inlineCallbacks
263 def test_handleStatus_ABORTED_cancelling_sets_build_log(self):
264 # If a build is intentionally cancelled, the build log is set.
265 self.assertEqual(None, self.build.log)
266 with dbuser(config.builddmaster.dbuser):
267 self.build.updateStatus(BuildStatus.CANCELLING)
268 yield self.behaviour.handleStatus(
269 self.build.buildqueue_record, "ABORTED", {})
270 self.assertNotEqual(None, self.build.log)
271
272 @defer.inlineCallbacks
273 def test_date_finished_set(self):
274 # The date finished is updated during handleStatus_OK.
275 self.assertEqual(None, self.build.date_finished)
276 with dbuser(config.builddmaster.dbuser):
277 yield self.behaviour.handleStatus(
278 self.build.buildqueue_record, 'OK',
279 {'filemap': self.filemap})
280 self.assertNotEqual(None, self.build.date_finished)
281
282 @defer.inlineCallbacks
283 def test_givenback_collection(self):
284 with ExpectedException(
285 BuildDaemonError,
286 "Build returned unexpected status: u'GIVENBACK'"):
287 with dbuser(config.builddmaster.dbuser):
288 yield self.behaviour.handleStatus(
289 self.build.buildqueue_record, "GIVENBACK", {})
290
291 @defer.inlineCallbacks
292 def test_builderfail_collection(self):
293 with ExpectedException(
294 BuildDaemonError,
295 "Build returned unexpected status: u'BUILDERFAIL'"):
296 with dbuser(config.builddmaster.dbuser):
297 yield self.behaviour.handleStatus(
298 self.build.buildqueue_record, "BUILDERFAIL", {})
299
300 @defer.inlineCallbacks
301 def test_invalid_status_collection(self):
302 with ExpectedException(
303 BuildDaemonError,
304 "Build returned unexpected status: u'BORKED'"):
305 with dbuser(config.builddmaster.dbuser):
306 yield self.behaviour.handleStatus(
307 self.build.buildqueue_record, "BORKED", {})
308
309
310class TestGetUploadMethodsForOCIRecipeBuild(
311 MakeOCIBuildMixin, TestGetUploadMethodsMixin, TestCaseWithFactory):
312 """IPackageBuild.getUpload-related methods work with OCI recipe builds."""
diff --git a/lib/lp/oci/tests/test_ocirecipebuildjob.py b/lib/lp/oci/tests/test_ocirecipebuildjob.py
0deleted file mode 100644313deleted file mode 100644
index cda3eef..0000000
--- a/lib/lp/oci/tests/test_ocirecipebuildjob.py
+++ /dev/null
@@ -1,49 +0,0 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""OCIRecipeBuildJob tests"""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10
11from lp.oci.interfaces.ocirecipebuildjob import IOCIRecipeBuildJob
12from lp.oci.model.ocirecipebuildjob import (
13 OCIRecipeBuildJob,
14 OCIRecipeBuildJobDerived,
15 OCIRecipeBuildJobType,
16 )
17from lp.testing import TestCaseWithFactory
18from lp.testing.layers import DatabaseFunctionalLayer
19
20
21class FakeOCIBuildJob(OCIRecipeBuildJobDerived):
22 """For testing OCIRecipeBuildJobDerived without a child class."""
23
24
25class TestOCIRecipeBuildJob(TestCaseWithFactory):
26
27 layer = DatabaseFunctionalLayer
28
29 def test_provides_interface(self):
30 oci_build = self.factory.makeOCIRecipeBuild()
31 self.assertProvides(
32 OCIRecipeBuildJob(
33 oci_build, OCIRecipeBuildJobType.REGISTRY_UPLOAD, {}),
34 IOCIRecipeBuildJob)
35
36 def test_getOopsVars(self):
37 oci_build = self.factory.makeOCIRecipeBuild()
38 build_job = OCIRecipeBuildJob(
39 oci_build, OCIRecipeBuildJobType.REGISTRY_UPLOAD, {})
40 derived = FakeOCIBuildJob(build_job)
41 oops = derived.getOopsVars()
42 expected = [
43 ('job_id', build_job.job.id),
44 ('job_type', build_job.job_type.title),
45 ('build_id', oci_build.id),
46 ('owner_id', oci_build.recipe.owner.id),
47 ('project_name', oci_build.recipe.ociproject.name),
48 ]
49 self.assertEqual(expected, oops)
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index f0788d8..4a25c88 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -5005,12 +5005,13 @@ class BareLaunchpadObjectFactory(ObjectFactory):
5005 return oci_build5005 return oci_build
50065006
5007 def makeOCIFile(self, build=None, library_file=None,5007 def makeOCIFile(self, build=None, library_file=None,
5008 layer_file_digest=None):5008 layer_file_digest=None, content=None, filename=None):
5009 """Make a new OCIFile."""5009 """Make a new OCIFile."""
5010 if build is None:5010 if build is None:
5011 build = self.makeOCIRecipeBuild()5011 build = self.makeOCIRecipeBuild()
5012 if library_file is None:5012 if library_file is None:
5013 library_file = self.makeLibraryFileAlias()5013 library_file = self.makeLibraryFileAlias(
5014 content=content, filename=filename)
5014 return OCIFile(build=build, library_file=library_file,5015 return OCIFile(build=build, library_file=library_file,
5015 layer_file_digest=layer_file_digest)5016 layer_file_digest=layer_file_digest)
50165017
diff --git a/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py b/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py
index b387251..32dd553 100644
--- a/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py
+++ b/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py
@@ -138,7 +138,7 @@ class TestTranslationTemplatesBuildBehaviour(
138 buildqueue = FakeBuildQueue(behaviour)138 buildqueue = FakeBuildQueue(behaviour)
139 path = behaviour.templates_tarball_path139 path = behaviour.templates_tarball_path
140 # Poke the file we're expecting into the mock slave.140 # Poke the file we're expecting into the mock slave.
141 behaviour._slave.valid_file_hashes.append(path)141 behaviour._slave.valid_files[path] = ''
142142
143 def got_tarball(filename):143 def got_tarball(filename):
144 tarball = open(filename, 'r')144 tarball = open(filename, 'r')

Subscribers

People subscribed via source and target branches

to status/vote changes: