← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:oci-registry-upload-status-ui into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:oci-registry-upload-status-ui into launchpad:master with ~cjwatson/launchpad:oci-recipe-schedule-registry-upload as a prerequisite.

Commit message:
Add UI indicating status of OCI registry uploads

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/384980
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:oci-registry-upload-status-ui into launchpad:master.
diff --git a/lib/lp/oci/browser/ocirecipebuild.py b/lib/lp/oci/browser/ocirecipebuild.py
index 5d58f5e..b38a531 100644
--- a/lib/lp/oci/browser/ocirecipebuild.py
+++ b/lib/lp/oci/browser/ocirecipebuild.py
@@ -20,7 +20,10 @@ from lp.app.browser.launchpadform import (
     action,
     LaunchpadFormView,
     )
-from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
+from lp.oci.interfaces.ocirecipebuild import (
+    CannotScheduleRegistryUpload,
+    IOCIRecipeBuild,
+    )
 from lp.services.librarian.browser import (
     FileNavigationMixin,
     ProxiedLibraryFileAlias,
@@ -93,6 +96,18 @@ class OCIRecipeBuildView(LaunchpadFormView):
     def next_url(self):
         return canonical_url(self.context)
 
+    @action("Upload build to registries", name="upload")
+    def upload_action(self, action, data):
+        """Schedule an upload of this build to each configured registry."""
+        try:
+            self.context.scheduleRegistryUpload()
+        except CannotScheduleRegistryUpload as e:
+            self.request.response.addWarningNotification(str(e))
+        else:
+            self.request.response.addInfoNotification(
+                "An upload has been scheduled and will run as soon as "
+                "possible.")
+
 
 class OCIRecipeBuildCancelView(LaunchpadFormView):
     """View for cancelling an OCI recipe build."""
diff --git a/lib/lp/oci/browser/tests/test_ocirecipebuild.py b/lib/lp/oci/browser/tests/test_ocirecipebuild.py
index c366055..625594e 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipebuild.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipebuild.py
@@ -22,7 +22,9 @@ from zope.testbrowser.browser import LinkNotFoundError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.buildmaster.enums import BuildStatus
 from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
+from lp.oci.interfaces.ocirecipebuildjob import IOCIRegistryUploadJobSource
 from lp.services.features.testing import FeatureFixture
+from lp.services.job.interfaces.job import JobStatus
 from lp.services.webapp import canonical_url
 from lp.testing import (
     ANONYMOUS,
@@ -107,6 +109,52 @@ class TestOCIRecipeBuildView(BrowserTestCase):
         build_view = create_initialized_view(build, "+index")
         self.assertEqual([], build_view.files)
 
+    def test_registry_upload_status_in_progress(self):
+        build = self.factory.makeOCIRecipeBuild(status=BuildStatus.FULLYBUILT)
+        getUtility(IOCIRegistryUploadJobSource).create(build)
+        build_view = create_initialized_view(build, "+index")
+        self.assertThat(build_view(), soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                "registry upload status", "li",
+                attrs={"id": "registry-upload-status"},
+                text=re.compile(r"^\s*Registry upload in progress\s*$"))))
+
+    def test_registry_upload_status_completed(self):
+        build = self.factory.makeOCIRecipeBuild(status=BuildStatus.FULLYBUILT)
+        job = getUtility(IOCIRegistryUploadJobSource).create(build)
+        naked_job = removeSecurityProxy(job)
+        naked_job.job._status = JobStatus.COMPLETED
+        build_view = create_initialized_view(build, "+index")
+        self.assertThat(build_view(), soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                "registry upload status", "li",
+                attrs={"id": "registry-upload-status"},
+                text=re.compile(r"^\s*Registry upload complete\s*$"))))
+
+    def test_registry_upload_status_failed(self):
+        build = self.factory.makeOCIRecipeBuild(status=BuildStatus.FULLYBUILT)
+        job = getUtility(IOCIRegistryUploadJobSource).create(build)
+        naked_job = removeSecurityProxy(job)
+        naked_job.job._status = JobStatus.FAILED
+        naked_job.error_summary = (
+            "Upload of test-digest for test-image failed")
+        build_view = create_initialized_view(build, "+index")
+        self.assertThat(build_view(), soupmatchers.HTMLContains(
+            soupmatchers.Within(
+                soupmatchers.Tag(
+                    "registry upload status", "li",
+                    attrs={"id": "registry-upload-status"},
+                    text=re.compile(
+                        r"^\s*Registry upload failed:\s+"
+                        r"Upload of test-digest for test-image failed\s*$")),
+                soupmatchers.Tag(
+                    "retry button", "input",
+                    attrs={
+                        "type": "submit",
+                        "name": "field.actions.upload",
+                        "value": "Retry",
+                        }))))
+
 
 class TestOCIRecipeBuildOperations(BrowserTestCase):
 
diff --git a/lib/lp/oci/templates/ocirecipebuild-index.pt b/lib/lp/oci/templates/ocirecipebuild-index.pt
index 2f88103..bea084b 100644
--- a/lib/lp/oci/templates/ocirecipebuild-index.pt
+++ b/lib/lp/oci/templates/ocirecipebuild-index.pt
@@ -136,6 +136,36 @@
            tal:attributes="href context/upload_log_url">uploadlog</a>
         (<span tal:replace="file/content/filesize/fmt:bytes" />)
       </li>
+      <li id="registry-upload-status"
+          tal:define="job context/last_registry_upload_job"
+          tal:condition="job">
+        <tal:pending
+            condition="context/registry_upload_status/enumvalue:PENDING">
+          Registry upload in progress
+        </tal:pending>
+        <tal:failed-upload
+            condition="context/registry_upload_status/enumvalue:FAILEDTOUPLOAD">
+          Registry upload failed:
+          <span tal:replace="context/registry_upload_error_summary" />
+          <form action="" method="POST">
+            <input type="submit" name="field.actions.upload" value="Retry" />
+          </form>
+        </tal:failed-upload>
+        <tal:uploaded
+            condition="context/registry_upload_status/enumvalue:UPLOADED">
+          Registry upload complete
+        </tal:uploaded>
+      </li>
+      <li id="registry-upload-status"
+          tal:condition="python:
+            context.status.title == 'Successfully built' and
+            context.recipe.can_upload_to_registry and
+            context.last_registry_upload_job is None">
+        <form action="" method="POST">
+          <input type="submit" name="field.actions.upload"
+                 value="Upload this build to registries" />
+        </form>
+      </li>
     </ul>
 
     <div