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

Proposed by Tom Wardill on 2019-12-05
Status: Merged
Approved by: Tom Wardill on 2020-02-13
Approved revision: e923bacc3f895810d1eab6b9708ee026a9946249
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~twom/launchpad:oci-ocirecipebuild
Merge into: launchpad:master
Prerequisite: ~twom/launchpad:concrete-oci-projects
Diff against target: 517 lines (+336/-11)
6 files modified
lib/lp/oci/interfaces/ocirecipe.py (+3/-0)
lib/lp/oci/interfaces/ocirecipebuild.py (+47/-2)
lib/lp/oci/model/ocirecipe.py (+17/-0)
lib/lp/oci/model/ocirecipebuild.py (+75/-6)
lib/lp/oci/tests/test_ocirecipebuild.py (+168/-0)
lib/lp/testing/factory.py (+26/-3)
Reviewer Review Type Date Requested Status
Colin Watson 2019-12-05 Approve on 2020-02-13
Review via email: mp+376431@code.launchpad.net

Commit message

Implement more of OCIRecipeBuild

Description of the change

Using SnapBuild as a model, implement more of the required methods to meet the interfaces of IOCIRecipeBuild and IPackageBuild.

Ensure queueBuild works, for further work in to OCIRecipeBuildJob and BuildBehaviour

To post a comment you must log in.
Colin Watson (cjwatson) :
review: Approve
~twom/launchpad:oci-ocirecipebuild updated on 2020-02-12
b8a2b9a... by Tom Wardill on 2020-02-12

Include virtualized tests

1e7cb5c... by Tom Wardill on 2020-02-12

Remove duplicate attributes

Colin Watson (cjwatson) :
review: Approve
~twom/launchpad:oci-ocirecipebuild updated on 2020-02-13
130aaf6... by Tom Wardill on 2020-02-13

Rename method, fix comment

9178c04... by Tom Wardill on 2020-02-13

Format imports

9a3042d... by Tom Wardill on 2020-02-13

Fix variable name

0427d6c... by Tom Wardill on 2020-02-13

Better filename tests

e923bac... by Tom Wardill on 2020-02-13

Better virtualized tests for ocirecipebuild

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
2index 340adfc..c6ce359 100644
3--- a/lib/lp/oci/interfaces/ocirecipe.py
4+++ b/lib/lp/oci/interfaces/ocirecipe.py
5@@ -240,3 +240,6 @@ class IOCIRecipeSet(Interface):
6
7 def findByOwner(owner):
8 """Return all OCI Recipes with the given `owner`."""
9+
10+ def preloadDataForOCIRecipes(recipes, user):
11+ """Load the data reloated to a list of OCI Recipes."""
12diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py
13index 5ee6b46..95af3f6 100644
14--- a/lib/lp/oci/interfaces/ocirecipebuild.py
15+++ b/lib/lp/oci/interfaces/ocirecipebuild.py
16@@ -7,13 +7,14 @@ from __future__ import absolute_import, print_function, unicode_literals
17
18 __metaclass__ = type
19 __all__ = [
20+ 'IOCIFile',
21 'IOCIRecipeBuild',
22 'IOCIRecipeBuildSet',
23 ]
24
25 from lazr.restful.fields import Reference
26 from zope.interface import Interface
27-from zope.schema import Text
28+from zope.schema import TextLine
29
30 from lp import _
31 from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource
32@@ -21,11 +22,20 @@ from lp.buildmaster.interfaces.packagebuild import IPackageBuild
33 from lp.oci.interfaces.ocirecipe import IOCIRecipe
34 from lp.services.database.constants import DEFAULT
35 from lp.services.fields import PublicPersonChoice
36+from lp.services.librarian.interfaces import ILibraryFileAlias
37
38
39 class IOCIRecipeBuildEdit(Interface):
40+
41 # XXX twom 2020-02-10 This will probably need cancel() implementing
42- pass
43+
44+ def addFile(lfa, layer_file_digest):
45+ """Add an OCI file to this build.
46+
47+ :param lfa: An `ILibraryFileAlias`.
48+ :param layer_file_digest: Digest for this file, used for image layers.
49+ :return: An `IOCILayerFile`.
50+ """
51
52
53 class IOCIRecipeBuildView(IPackageBuild):
54@@ -41,6 +51,21 @@ class IOCIRecipeBuildView(IPackageBuild):
55 required=True,
56 readonly=True)
57
58+ def getByFileName():
59+ """Retrieve a file by filename
60+
61+ :return: A result set of (`IOCIFile`, `ILibraryFileAlias`,
62+ `ILibraryFileContent`).
63+ """
64+
65+ def getLayerFileByDigest(layer_file_digest):
66+ """Retrieve a layer file by the digest.
67+
68+ :param layer_file_digest: The digest to look up.
69+ :raises NotFoundError: if no file exists with the given digest.
70+ :return: The corresponding `ILibraryFileAlias`.
71+ """
72+
73
74 class IOCIRecipeBuildAdmin(Interface):
75 # XXX twom 2020-02-10 This will probably need rescore() implementing
76@@ -61,3 +86,23 @@ class IOCIRecipeBuildSet(ISpecificBuildFarmJobSource):
77
78 def preloadBuildsData(builds):
79 """Load the data related to a list of OCI recipe builds."""
80+
81+
82+class IOCIFile(Interface):
83+ """A link between an OCI recipe build and a file in the librarian."""
84+
85+ build = Reference(
86+ IOCIRecipeBuild,
87+ title=_("The OCI recipe build producing this file."),
88+ required=True, readonly=True)
89+
90+ library_file = Reference(
91+ ILibraryFileAlias, title=_("A file in the librarian."),
92+ required=True, readonly=True)
93+
94+ layer_file_digest = TextLine(
95+ title=_("Content-addressable hash of the file''s contents, "
96+ "used for reassembling image layers when pushing "
97+ "a build to a registry. This hash is in an opaque format "
98+ "generated by the OCI build tool."),
99+ required=False, readonly=True)
100diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
101index 0d4d20f..8a01b2d 100644
102--- a/lib/lp/oci/model/ocirecipe.py
103+++ b/lib/lp/oci/model/ocirecipe.py
104@@ -30,11 +30,14 @@ from storm.locals import (
105 from zope.component import getUtility
106 from zope.event import notify
107 from zope.interface import implementer
108+from zope.security.proxy import removeSecurityProxy
109
110 from lp.buildmaster.enums import BuildStatus
111 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
112 from lp.buildmaster.model.buildfarmjob import BuildFarmJob
113 from lp.buildmaster.model.buildqueue import BuildQueue
114+from lp.code.model.gitcollection import GenericGitCollection
115+from lp.code.model.gitrepository import GitRepository
116 from lp.oci.interfaces.ocirecipe import (
117 DuplicateOCIRecipeName,
118 IOCIRecipe,
119@@ -46,6 +49,8 @@ from lp.oci.interfaces.ocirecipe import (
120 )
121 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
122 from lp.oci.model.ocirecipebuild import OCIRecipeBuild
123+from lp.registry.interfaces.person import IPersonSet
124+from lp.services.database.bulk import load_related
125 from lp.services.database.constants import DEFAULT
126 from lp.services.database.decoratedresultset import DecoratedResultSet
127 from lp.services.database.interfaces import (
128@@ -293,3 +298,15 @@ class OCIRecipeSet:
129 def findByOwner(self, owner):
130 """See `IOCIRecipe`."""
131 return IStore(OCIRecipe).find(OCIRecipe, OCIRecipe.owner == owner)
132+
133+ def preloadDataForOCIRecipes(self, recipes, user=None):
134+ """See `IOCIRecipeSet`."""
135+ recipes = [removeSecurityProxy(recipe) for recipe in recipes]
136+
137+ person_ids = set()
138+ for recipe in recipes:
139+ person_ids.add(recipe.registrant_id)
140+ person_ids.add(recipe.owner_id)
141+
142+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
143+ person_ids, need_validity=True))
144diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
145index d069800..307fbd2 100644
146--- a/lib/lp/oci/model/ocirecipebuild.py
147+++ b/lib/lp/oci/model/ocirecipebuild.py
148@@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals
149
150 __metaclass__ = type
151 __all__ = [
152+ 'OCIFile',
153 'OCIRecipeBuild',
154 'OCIRecipeBuildSet',
155 ]
156@@ -28,6 +29,7 @@ from storm.store import EmptyResultSet
157 from zope.component import getUtility
158 from zope.interface import implementer
159
160+from lp.app.errors import NotFoundError
161 from lp.buildmaster.enums import (
162 BuildFarmJobType,
163 BuildStatus,
164@@ -35,10 +37,14 @@ from lp.buildmaster.enums import (
165 from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
166 from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
167 from lp.buildmaster.model.packagebuild import PackageBuildMixin
168+from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
169 from lp.oci.interfaces.ocirecipebuild import (
170+ IOCIFile,
171 IOCIRecipeBuild,
172 IOCIRecipeBuildSet,
173 )
174+from lp.registry.model.person import Person
175+from lp.services.database.bulk import load_related
176 from lp.services.database.constants import DEFAULT
177 from lp.services.database.decoratedresultset import DecoratedResultSet
178 from lp.services.database.enumcol import DBEnum
179@@ -46,6 +52,33 @@ from lp.services.database.interfaces import (
180 IMasterStore,
181 IStore,
182 )
183+from lp.services.librarian.model import (
184+ LibraryFileAlias,
185+ LibraryFileContent,
186+ )
187+
188+
189+@implementer(IOCIFile)
190+class OCIFile(Storm):
191+
192+ __storm_table__ = 'OCIFile'
193+
194+ id = Int(name='id', primary=True)
195+
196+ build_id = Int(name='build', allow_none=False)
197+ build = Reference(build_id, 'OCIRecipeBuild.id')
198+
199+ library_file_id = Int(name='library_file', allow_none=False)
200+ library_file = Reference(library_file_id, 'LibraryFileAlias.id')
201+
202+ layer_file_digest = Unicode(name='layer_file_digest', allow_none=True)
203+
204+ def __init__(self, build, library_file, layer_file_digest=None):
205+ """Construct a `OCIFile`."""
206+ super(OCIFile, self).__init__()
207+ self.build = build
208+ self.library_file = library_file
209+ self.layer_file_digest = layer_file_digest
210
211
212 @implementer(IOCIRecipeBuild)
213@@ -93,6 +126,11 @@ class OCIRecipeBuild(PackageBuildMixin, Storm):
214 build_farm_job_id = Int(name='build_farm_job', allow_none=False)
215 build_farm_job = Reference(build_farm_job_id, 'BuildFarmJob.id')
216
217+ # Stub attributes to match the IPackageBuild interface that we
218+ # are not using in this implementation at this time.
219+ pocket = None
220+ distro_series = None
221+
222 def __init__(self, build_farm_job, requester, recipe,
223 processor, virtualized, date_created):
224
225@@ -130,6 +168,34 @@ class OCIRecipeBuild(PackageBuildMixin, Storm):
226 durations.sort()
227 return durations[len(durations) // 2]
228
229+ def getByFileName(self, filename):
230+ result = Store.of(self).find(
231+ (OCIFile, LibraryFileAlias, LibraryFileContent),
232+ OCIFile.build == self.id,
233+ LibraryFileAlias.id == OCIFile.library_file_id,
234+ LibraryFileContent.id == LibraryFileAlias.contentID,
235+ LibraryFileAlias.filename == filename).one()
236+ if result is not None:
237+ return result
238+ raise NotFoundError(filename)
239+
240+ def getLayerFileByDigest(self, layer_file_digest):
241+ file_object = Store.of(self).find(
242+ (OCIFile, LibraryFileAlias, LibraryFileContent),
243+ OCIFile.build == self.id,
244+ LibraryFileAlias.id == OCIFile.library_file_id,
245+ LibraryFileContent.id == LibraryFileAlias.contentID,
246+ OCIFile.layer_file_digest == layer_file_digest).one()
247+ if file_object is not None:
248+ return file_object
249+ raise NotFoundError(layer_file_digest)
250+
251+ def addFile(self, lfa, layer_file_digest=None):
252+ oci_file = OCIFile(
253+ build=self, library_file=lfa, layer_file_digest=layer_file_digest)
254+ IMasterStore(OCIFile).add(oci_file)
255+ return oci_file
256+
257 @property
258 def archive(self):
259 # XXX twom 2019-12-05 This may need to change when an OCIProject
260@@ -142,11 +208,6 @@ class OCIRecipeBuild(PackageBuildMixin, Storm):
261 # pillar isn't just a distribution
262 return self.recipe.oci_project.distribution
263
264- # Stub attributes to match the IPackageBuild interface that we
265- # will not use in this implementation.
266- pocket = None
267- distro_series = None
268-
269
270 @implementer(IOCIRecipeBuildSet)
271 class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
272@@ -171,7 +232,15 @@ class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
273
274 def preloadBuildsData(self, builds):
275 """See `IOCIRecipeBuildSet`."""
276- # XXX twom 2019-12-02 Currently a no-op skeleton, to be filled in
277+ # Circular import.
278+ from lp.oci.model.ocirecipe import OCIRecipe
279+ load_related(Person, builds, ["requester_id"])
280+ lfas = load_related(LibraryFileAlias, builds, ["log_id"])
281+ load_related(LibraryFileContent, lfas, ["contentID"])
282+ recipes = load_related(OCIRecipe, builds, ["recipe_id"])
283+ getUtility(IOCIRecipeSet).preloadDataForOCIRecipes(recipes)
284+ # XXX twom 2019-12-05 This needs to be extended to include
285+ # OCIRecipeBuildJob when that exists.
286 return
287
288 def getByID(self, build_id):
289diff --git a/lib/lp/oci/tests/test_ocirecipebuild.py b/lib/lp/oci/tests/test_ocirecipebuild.py
290new file mode 100644
291index 0000000..3d51de2
292--- /dev/null
293+++ b/lib/lp/oci/tests/test_ocirecipebuild.py
294@@ -0,0 +1,168 @@
295+# Copyright 2019 Canonical Ltd. This software is licensed under the
296+# GNU Affero General Public License version 3 (see the file LICENSE).
297+
298+"""Tests for OCI image building recipe functionality."""
299+
300+from __future__ import absolute_import, print_function, unicode_literals
301+
302+from datetime import timedelta
303+
304+import six
305+from zope.component import getUtility
306+from zope.security.proxy import removeSecurityProxy
307+
308+from lp.app.errors import NotFoundError
309+from lp.buildmaster.enums import BuildStatus
310+from lp.buildmaster.interfaces.buildqueue import IBuildQueue
311+from lp.buildmaster.interfaces.packagebuild import IPackageBuild
312+from lp.oci.interfaces.ocirecipebuild import (
313+ IOCIRecipeBuild,
314+ IOCIRecipeBuildSet,
315+ )
316+from lp.oci.model.ocirecipebuild import OCIRecipeBuildSet
317+from lp.testing import (
318+ admin_logged_in,
319+ TestCaseWithFactory,
320+ )
321+from lp.testing.layers import (
322+ DatabaseFunctionalLayer,
323+ LaunchpadZopelessLayer,
324+ )
325+
326+
327+class TestOCIRecipeBuild(TestCaseWithFactory):
328+
329+ layer = LaunchpadZopelessLayer
330+
331+ def setUp(self):
332+ super(TestOCIRecipeBuild, self).setUp()
333+ self.build = self.factory.makeOCIRecipeBuild()
334+
335+ def test_implements_interface(self):
336+ with admin_logged_in():
337+ self.assertProvides(self.build, IOCIRecipeBuild)
338+ self.assertProvides(self.build, IPackageBuild)
339+
340+ def test_addFile(self):
341+ lfa = self.factory.makeLibraryFileAlias()
342+ self.build.addFile(lfa)
343+ _, result_lfa, _ = self.build.getByFileName(lfa.filename)
344+ self.assertEqual(result_lfa, lfa)
345+
346+ def test_getByFileName(self):
347+ files = [self.factory.makeOCIFile(build=self.build) for x in range(3)]
348+ result, _, _ = self.build.getByFileName(
349+ files[0].library_file.filename)
350+ self.assertEqual(result, files[0])
351+
352+ def test_getByFileName_missing(self):
353+ self.assertRaises(
354+ NotFoundError,
355+ self.build.getByFileName,
356+ "missing")
357+
358+ def test_getLayerFileByDigest(self):
359+ files = [self.factory.makeOCIFile(
360+ build=self.build, layer_file_digest=six.text_type(x))
361+ for x in range(3)]
362+ result, _, _ = self.build.getLayerFileByDigest(
363+ files[0].layer_file_digest)
364+ self.assertEqual(result, files[0])
365+
366+ def test_getLayerFileByDigest_missing(self):
367+ [self.factory.makeOCIFile(
368+ build=self.build, layer_file_digest=six.text_type(x))
369+ for x in range(3)]
370+ self.assertRaises(
371+ NotFoundError,
372+ self.build.getLayerFileByDigest,
373+ 'missing')
374+
375+ def test_estimateDuration(self):
376+ # Without previous builds, the default time estimate is 30m.
377+ self.assertEqual(1800, self.build.estimateDuration().seconds)
378+
379+ def test_estimateDuration_with_history(self):
380+ # Previous successful builds of the same OCI recipe are used for
381+ # estimates.
382+ oci_build = self.factory.makeOCIRecipeBuild(
383+ status=BuildStatus.FULLYBUILT, duration=timedelta(seconds=335))
384+ for i in range(3):
385+ self.factory.makeOCIRecipeBuild(
386+ requester=oci_build.requester, recipe=oci_build.recipe,
387+ status=BuildStatus.FAILEDTOBUILD,
388+ duration=timedelta(seconds=20))
389+ self.assertEqual(335, oci_build.estimateDuration().seconds)
390+
391+ def test_queueBuild(self):
392+ # OCIRecipeBuild can create the queue entry for itself.
393+ bq = self.build.queueBuild()
394+ self.assertProvides(bq, IBuildQueue)
395+ self.assertEqual(
396+ self.build.build_farm_job, removeSecurityProxy(bq)._build_farm_job)
397+ self.assertEqual(self.build, bq.specific_build)
398+ self.assertEqual(self.build.virtualized, bq.virtualized)
399+ self.assertIsNotNone(bq.processor)
400+ self.assertEqual(bq, self.build.buildqueue_record)
401+
402+
403+class TestOCIRecipeBuildSet(TestCaseWithFactory):
404+
405+ layer = DatabaseFunctionalLayer
406+
407+ def test_implements_interface(self):
408+ target = OCIRecipeBuildSet()
409+ with admin_logged_in():
410+ self.assertProvides(target, IOCIRecipeBuildSet)
411+
412+ def test_new(self):
413+ requester = self.factory.makePerson()
414+ recipe = self.factory.makeOCIRecipe()
415+ distro_arch_series = self.factory.makeDistroArchSeries()
416+ target = getUtility(IOCIRecipeBuildSet).new(
417+ requester, recipe, distro_arch_series)
418+ with admin_logged_in():
419+ self.assertProvides(target, IOCIRecipeBuild)
420+
421+ def test_getByID(self):
422+ builds = [self.factory.makeOCIRecipeBuild() for x in range(3)]
423+ result = getUtility(IOCIRecipeBuildSet).getByID(builds[1].id)
424+ self.assertEqual(result, builds[1])
425+
426+ def test_getByBuildFarmJob(self):
427+ builds = [self.factory.makeOCIRecipeBuild() for x in range(3)]
428+ result = getUtility(IOCIRecipeBuildSet).getByBuildFarmJob(
429+ builds[1].build_farm_job)
430+ self.assertEqual(result, builds[1])
431+
432+ def test_getByBuildFarmJobs(self):
433+ builds = [self.factory.makeOCIRecipeBuild() for x in range(3)]
434+ self.assertContentEqual(
435+ builds,
436+ getUtility(IOCIRecipeBuildSet).getByBuildFarmJobs(
437+ [build.build_farm_job for build in builds]))
438+
439+ def test_getByBuildFarmJobs_empty(self):
440+ self.assertContentEqual(
441+ [], getUtility(IOCIRecipeBuildSet).getByBuildFarmJobs([]))
442+
443+ def test_virtualized_recipe_requires(self):
444+ recipe = self.factory.makeOCIRecipe(require_virtualized=True)
445+ target = self.factory.makeOCIRecipeBuild(recipe=recipe)
446+ self.assertTrue(target.virtualized)
447+
448+ def test_virtualized_processor_requires(self):
449+ distro_arch_series = self.factory.makeDistroArchSeries()
450+ distro_arch_series.processor.supports_nonvirtualized = False
451+ recipe = self.factory.makeOCIRecipe(require_virtualized=False)
452+ target = self.factory.makeOCIRecipeBuild(
453+ distro_arch_series=distro_arch_series, recipe=recipe)
454+ self.assertTrue(target.virtualized)
455+
456+ def test_virtualized_no_support(self):
457+ recipe = self.factory.makeOCIRecipe(require_virtualized=False)
458+ distro_arch_series = self.factory.makeDistroArchSeries()
459+ distro_arch_series.processor.supports_nonvirtualized = True
460+ target = self.factory.makeOCIRecipeBuild(
461+ recipe=recipe, distro_arch_series=distro_arch_series)
462+ self.assertFalse(target.virtualized)
463diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
464index d0ecd18..f0788d8 100644
465--- a/lib/lp/testing/factory.py
466+++ b/lib/lp/testing/factory.py
467@@ -160,6 +160,7 @@ from lp.hardwaredb.interfaces.hwdb import (
468 from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
469 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
470 from lp.oci.model.ocirecipe import OCIRecipeArch
471+from lp.oci.model.ocirecipebuild import OCIFile
472 from lp.registry.enums import (
473 BranchSharingPolicy,
474 BugSharingPolicy,
475@@ -4979,7 +4980,9 @@ class BareLaunchpadObjectFactory(ObjectFactory):
476 return OCIRecipeArch(recipe, processor)
477
478 def makeOCIRecipeBuild(self, requester=None, recipe=None,
479- distro_arch_series=None, date_created=DEFAULT):
480+ distro_arch_series=None, date_created=DEFAULT,
481+ status=BuildStatus.NEEDSBUILD, builder=None,
482+ duration=None):
483 """Make a new OCIRecipeBuild."""
484 if requester is None:
485 requester = self.makePerson()
486@@ -4987,9 +4990,29 @@ class BareLaunchpadObjectFactory(ObjectFactory):
487 distro_arch_series = self.makeDistroArchSeries()
488 if recipe is None:
489 recipe = self.makeOCIRecipe()
490-
491- return getUtility(IOCIRecipeBuildSet).new(
492+ oci_build = getUtility(IOCIRecipeBuildSet).new(
493 requester, recipe, distro_arch_series, date_created)
494+ if duration is not None:
495+ removeSecurityProxy(oci_build).updateStatus(
496+ BuildStatus.BUILDING, builder=builder,
497+ date_started=oci_build.date_created)
498+ removeSecurityProxy(oci_build).updateStatus(
499+ status, builder=builder,
500+ date_finished=oci_build.date_started + duration)
501+ else:
502+ removeSecurityProxy(oci_build).updateStatus(
503+ status, builder=builder)
504+ return oci_build
505+
506+ def makeOCIFile(self, build=None, library_file=None,
507+ layer_file_digest=None):
508+ """Make a new OCIFile."""
509+ if build is None:
510+ build = self.makeOCIRecipeBuild()
511+ if library_file is None:
512+ library_file = self.makeLibraryFileAlias()
513+ return OCIFile(build=build, library_file=library_file,
514+ layer_file_digest=layer_file_digest)
515
516
517 # Some factory methods return simple Python types. We don't add

Subscribers

People subscribed via source and target branches