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

Proposed by Tom Wardill
Status: Merged
Approved by: Tom Wardill
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 (community) Approve
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.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
~twom/launchpad:oci-ocirecipebuild updated
b8a2b9a... by Tom Wardill

Include virtualized tests

1e7cb5c... by Tom Wardill

Remove duplicate attributes

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
~twom/launchpad:oci-ocirecipebuild updated
130aaf6... by Tom Wardill

Rename method, fix comment

9178c04... by Tom Wardill

Format imports

9a3042d... by Tom Wardill

Fix variable name

0427d6c... by Tom Wardill

Better filename tests

e923bac... by Tom Wardill

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

to status/vote changes: