← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~ruinedyourlife/launchpad:fix-missing-craft-api-endpoints into launchpad:master

 

Quentin Debhi has proposed merging ~ruinedyourlife/launchpad:fix-missing-craft-api-endpoints into launchpad:master.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ruinedyourlife/launchpad/+git/launchpad/+merge/474922
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad:fix-missing-craft-api-endpoints into launchpad:master.
diff --git a/lib/lp/crafts/interfaces/craftrecipe.py b/lib/lp/crafts/interfaces/craftrecipe.py
index c4ed465..d45bc71 100644
--- a/lib/lp/crafts/interfaces/craftrecipe.py
+++ b/lib/lp/crafts/interfaces/craftrecipe.py
@@ -784,6 +784,9 @@ class ICraftRecipeSet(Interface):
             "project",
             "name",
             "description",
+            "git_repository",
+            "git_repository_url",
+            "git_path",
             "git_ref",
             "build_path",
             "auto_build",
diff --git a/lib/lp/crafts/interfaces/craftrecipebuild.py b/lib/lp/crafts/interfaces/craftrecipebuild.py
index 4c77eb8..7b57a27 100644
--- a/lib/lp/crafts/interfaces/craftrecipebuild.py
+++ b/lib/lp/crafts/interfaces/craftrecipebuild.py
@@ -9,7 +9,12 @@ __all__ = [
     "ICraftRecipeBuildSet",
 ]
 
-from lazr.restful.declarations import exported, exported_as_webservice_entry
+from lazr.restful.declarations import (
+    export_read_operation,
+    exported,
+    exported_as_webservice_entry,
+    operation_for_version,
+)
 from lazr.restful.fields import Reference
 from zope.interface import Attribute, Interface
 from zope.schema import Bool, Datetime, Dict, Int, TextLine
@@ -130,6 +135,18 @@ class ICraftRecipeBuildView(IPackageBuildView):
         _("A dict of data about store upload progress.")
     )
 
+    build_metadata_url = exported(
+        TextLine(
+            title=_("URL of the build metadata file"),
+            description=_(
+                "URL of the metadata file generated by the fetch service, if "
+                "it exists."
+            ),
+            required=False,
+            readonly=True,
+        )
+    )
+
     def getFiles():
         """Retrieve the build's ICraftFile records.
 
@@ -153,6 +170,13 @@ class ICraftRecipeBuildView(IPackageBuildView):
         :return: The corresponding ILibraryFileAlias.
         """
 
+    @export_read_operation()
+    @operation_for_version("devel")
+    def getFileUrls():
+        """URLs for all the files produced by this build.
+
+        :return: A collection of URLs for this build."""
+
 
 class ICraftRecipeBuildEdit(IBuildFarmJobEdit):
     """ICraftRecipeBuild methods that require launchpad.Edit."""
diff --git a/lib/lp/crafts/model/craftrecipebuild.py b/lib/lp/crafts/model/craftrecipebuild.py
index 2999b45..ef32f9f 100644
--- a/lib/lp/crafts/model/craftrecipebuild.py
+++ b/lib/lp/crafts/model/craftrecipebuild.py
@@ -18,6 +18,7 @@ from zope.component import getUtility
 from zope.interface import implementer
 
 from lp.app.errors import NotFoundError
+from lp.buildmaster.builderproxy import BUILD_METADATA_FILENAME_FORMAT
 from lp.buildmaster.enums import (
     BuildFarmJobType,
     BuildQueueStatus,
@@ -44,6 +45,7 @@ from lp.services.database.decoratedresultset import DecoratedResultSet
 from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import IPrimaryStore, IStore
 from lp.services.database.stormbase import StormBase
+from lp.services.librarian.browser import ProxiedLibraryFileAlias
 from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent
 from lp.services.propertycache import cachedproperty, get_property_cache
 from lp.services.webapp.snapshot import notify_modified
@@ -358,6 +360,23 @@ class CraftRecipeBuild(PackageBuildMixin, StormBase):
         mailer = CraftRecipeBuildMailer.forStatus(self)
         mailer.sendAll()
 
+    def getFileUrls(self):
+        """See `ICraftRecipeBuild`."""
+        return [
+            ProxiedLibraryFileAlias(lfa, self).http_url
+            for _, lfa, _ in self.getFiles()
+        ]
+
+    @property
+    def build_metadata_url(self):
+        metadata_filename = BUILD_METADATA_FILENAME_FORMAT.format(
+            build_id=self.build_cookie
+        )
+        for url in self.getFileUrls():
+            if url.endswith(metadata_filename):
+                return url
+        return None
+
 
 @implementer(ICraftRecipeBuildSet)
 class CraftRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
diff --git a/lib/lp/crafts/tests/test_craftrecipebuild.py b/lib/lp/crafts/tests/test_craftrecipebuild.py
index 5821ae3..7ffdcab 100644
--- a/lib/lp/crafts/tests/test_craftrecipebuild.py
+++ b/lib/lp/crafts/tests/test_craftrecipebuild.py
@@ -30,6 +30,7 @@ from lp.registry.enums import PersonVisibility, TeamMembershipPolicy
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.config import config
 from lp.services.features.testing import FeatureFixture
+from lp.services.librarian.browser import ProxiedLibraryFileAlias
 from lp.services.propertycache import clear_property_cache
 from lp.services.webapp.interfaces import OAuthPermission
 from lp.testing import (
@@ -596,3 +597,70 @@ class TestCraftRecipeBuildWebservice(TestCaseWithFactory):
         self.assertCanOpenRedirectedUrl(browser, build["build_log_url"])
         self.assertIsNotNone(build["upload_log_url"])
         self.assertCanOpenRedirectedUrl(browser, build["upload_log_url"])
+
+    def test_getFileUrls(self):
+        # API clients can fetch files attached to builds.
+        db_build = self.factory.makeCraftRecipeBuild(requester=self.person)
+        db_files = [
+            self.factory.makeCraftFile(build=db_build) for i in range(2)
+        ]
+        build_url = api_url(db_build)
+        file_urls = [
+            ProxiedLibraryFileAlias(file.library_file, db_build).http_url
+            for file in db_files
+        ]
+        logout()
+        response = self.webservice.named_get(build_url, "getFileUrls")
+        self.assertEqual(200, response.status)
+        self.assertContentEqual(file_urls, response.jsonBody())
+        browser = self.getNonRedirectingBrowser(user=self.person)
+        browser.raiseHttpErrors = False
+        for file_url in file_urls:
+            self.assertCanOpenRedirectedUrl(browser, file_url)
+
+    def test_build_metadata_url(self):
+        # API clients can fetch the metadata from the build, generated by the
+        # fetch service
+        db_build = self.factory.makeCraftRecipeBuild(requester=self.person)
+        metadata_filename = f"{db_build.build_cookie}_metadata.json"
+        with person_logged_in(self.person):
+            file_1 = self.factory.makeLibraryFileAlias(
+                content="some_json",
+                filename="test_file.json",
+            )
+            db_build.addFile(file_1)
+            metadata_file = self.factory.makeLibraryFileAlias(
+                content="some_json",
+                filename=metadata_filename,
+            )
+            db_build.addFile(metadata_file)
+            file_2 = self.factory.makeLibraryFileAlias(
+                content="some_json",
+                filename="another_test_file.tar",
+            )
+            db_build.addFile(file_2)
+        build_url = api_url(db_build)
+        logout()
+        build = self.webservice.get(build_url).jsonBody()
+        self.assertIsNotNone(build["build_metadata_url"])
+        self.assertEndsWith(build["build_metadata_url"], metadata_filename)
+
+    def test_build_metadata_url_no_metadata_file(self):
+        # The attribute `build_metadata_url` returns None when metadata file
+        # does not exist.
+        db_build = self.factory.makeCraftRecipeBuild(requester=self.person)
+        with person_logged_in(self.person):
+            file_1 = self.factory.makeLibraryFileAlias(
+                content="some_json",
+                filename="test_file.json",
+            )
+            db_build.addFile(file_1)
+            file_2 = self.factory.makeLibraryFileAlias(
+                content="some_json",
+                filename="another_test_file.tar",
+            )
+            db_build.addFile(file_2)
+        build_url = api_url(db_build)
+        logout()
+        build = self.webservice.get(build_url).jsonBody()
+        self.assertIsNone(build["build_metadata_url"])