build api push_image endpoint fix

BUG=chromium:1157040
TEST=run_tests and call_scripts

Change-Id: Ibb500c07c610d8750d0298ef212a60eb29ba72b6
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2582144
Reviewed-by: LaMont Jones <lamontjones@chromium.org>
Reviewed-by: George Engelbrecht <engeg@google.com>
Commit-Queue: Jack Neus <jackneus@google.com>
Tested-by: Jack Neus <jackneus@google.com>
diff --git a/api/contrib/call_templates/image__push_image_example_input.json b/api/contrib/call_templates/image__push_image_example_input.json
new file mode 100644
index 0000000..2d0d4f7
--- /dev/null
+++ b/api/contrib/call_templates/image__push_image_example_input.json
@@ -0,0 +1,14 @@
+{
+    "dryrun": true,
+    "gs_image_dir": "gs://chromeos-image-archive/atlas-release/R89-13604.0.0",
+    "sysroot": {
+        "build_target": {
+            "name": "atlas"
+        }
+    },
+    "profile": {
+        "name": "baz"
+    },
+    "sign_types_description": ["IMAGE_TYPE_BASE", "IMAGE_TYPE_RECOVERY"],
+    "sign_types": [1, 6]
+}
diff --git a/api/controller/image.py b/api/controller/image.py
index bc25c15..b7968e9 100644
--- a/api/controller/image.py
+++ b/api/controller/image.py
@@ -21,10 +21,11 @@
 from chromite.lib import cros_build_lib
 from chromite.lib import constants
 from chromite.lib import image_lib
+from chromite.lib import cros_logging as logging
+from chromite.scripts import pushimage
 from chromite.service import image
 from chromite.utils import metrics
 
-
 # The image.proto ImageType enum ids.
 _BASE_ID = common_pb2.BASE
 _DEV_ID = common_pb2.DEV
@@ -60,6 +61,17 @@
     _TEST_GUEST_VM_ID: _IMAGE_MAPPING[_TEST_ID],
 }
 
+# Supported image types for PushImage.
+SUPPORTED_IMAGE_TYPES = {
+    common_pb2.IMAGE_TYPE_RECOVERY: constants.IMAGE_TYPE_RECOVERY,
+    common_pb2.IMAGE_TYPE_FACTORY: constants.IMAGE_TYPE_FACTORY,
+    common_pb2.IMAGE_TYPE_FIRMWARE: constants.IMAGE_TYPE_FIRMWARE,
+    common_pb2.IMAGE_TYPE_ACCESSORY_USBPD: constants.IMAGE_TYPE_ACCESSORY_USBPD,
+    common_pb2.IMAGE_TYPE_ACCESSORY_RWSIG: constants.IMAGE_TYPE_ACCESSORY_RWSIG,
+    common_pb2.IMAGE_TYPE_BASE: constants.IMAGE_TYPE_BASE,
+    common_pb2.IMAGE_TYPE_GSC_FIRMWARE: constants.IMAGE_TYPE_GSC_FIRMWARE
+}
+
 
 def _CreateResponse(_input_proto, output_proto, _config):
   """Set output_proto success field on a successful Create response."""
@@ -88,8 +100,8 @@
   build_config = _ParseCreateBuildConfig(input_proto)
 
   # Sorted isn't really necessary here, but it's much easier to test.
-  result = image.Build(board=board, images=sorted(list(image_types)),
-                       config=build_config)
+  result = image.Build(
+      board=board, images=sorted(list(image_types)), config=build_config)
 
   output_proto.success = result.success
 
@@ -171,8 +183,11 @@
   disk_layout = input_proto.disk_layout or None
   builder_path = input_proto.builder_path or None
   return image.BuildConfig(
-      enable_rootfs_verification=enable_rootfs_verification, replace=True,
-      version=version, disk_layout=disk_layout, builder_path=builder_path,
+      enable_rootfs_verification=enable_rootfs_verification,
+      replace=True,
+      version=version,
+      disk_layout=disk_layout,
+      builder_path=builder_path,
   )
 
 
@@ -258,3 +273,43 @@
     return controller.RETURN_CODE_SUCCESS
   else:
     return controller.RETURN_CODE_COMPLETED_UNSUCCESSFULLY
+
+
+@faux.empty_success
+@faux.empty_completed_unsuccessfully_error
+@validate.require('gs_image_dir', 'sysroot.build_target.name')
+def PushImage(input_proto, _output_proto, config):
+  """Push artifacts from the archive bucket to the release bucket.
+
+  Wraps chromite/scripts/pushimage.py.
+
+  Args:
+    input_proto (PushImageRequest): Input proto.
+    _output_proto (PushImageResponse): Output proto.
+    config (api.config.ApiConfig): The API call config.
+
+  Returns:
+    A controller return code (e.g. controller.RETURN_CODE_SUCCESS).
+  """
+  sign_types = []
+  if input_proto.sign_types:
+    for sign_type in input_proto.sign_types:
+      if sign_type not in SUPPORTED_IMAGE_TYPES:
+        logging.error('unsupported sign type %g', sign_type)
+        return controller.RETURN_CODE_INVALID_INPUT
+      sign_types.append(SUPPORTED_IMAGE_TYPES[sign_type])
+
+  # If configured for validation only we're done here.
+  if config.validate_only:
+    return controller.RETURN_CODE_VALID_INPUT
+
+  try:
+    pushimage.PushImage(
+        input_proto.gs_image_dir,
+        input_proto.sysroot.build_target.name,
+        dry_run=input_proto.dryrun,
+        profile=input_proto.profile.name,
+        sign_types=sign_types)
+    return controller.RETURN_CODE_SUCCESS
+  except Exception:
+    return controller.RETURN_CODE_COMPLETED_UNSUCCESSFULLY
diff --git a/api/controller/image_unittest.py b/api/controller/image_unittest.py
index 56de3c3..c647e11 100644
--- a/api/controller/image_unittest.py
+++ b/api/controller/image_unittest.py
@@ -16,11 +16,13 @@
 from chromite.api.controller import image as image_controller
 from chromite.api.gen.chromite.api import image_pb2
 from chromite.api.gen.chromiumos import common_pb2
+from chromite.api.gen.chromite.api import sysroot_pb2
 from chromite.lib import constants
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_test_lib
 from chromite.lib import image_lib
 from chromite.lib import osutils
+from chromite.scripts import pushimage
 from chromite.service import image as image_service
 
 
@@ -30,7 +32,11 @@
   def setUp(self):
     self.response = image_pb2.CreateImageResult()
 
-  def _GetRequest(self, board=None, types=None, version=None, builder_path=None,
+  def _GetRequest(self,
+                  board=None,
+                  types=None,
+                  version=None,
+                  builder_path=None,
                   disable_rootfs_verification=False):
     """Helper to build a request instance."""
     return image_pb2.CreateImageRequest(
@@ -84,8 +90,8 @@
     build_patch = self.PatchObject(image_service, 'Build', return_value=result)
 
     image_controller.Create(request, self.response, self.api_config)
-    build_patch.assert_called_with(images=[constants.IMAGE_TYPE_BASE],
-                                   board='board', config=mock.ANY)
+    build_patch.assert_called_with(
+        images=[constants.IMAGE_TYPE_BASE], board='board', config=mock.ANY)
 
   def testSingleTypeSpecified(self):
     """Test it's properly using a specified type."""
@@ -96,8 +102,8 @@
     build_patch = self.PatchObject(image_service, 'Build', return_value=result)
 
     image_controller.Create(request, self.response, self.api_config)
-    build_patch.assert_called_with(images=[constants.IMAGE_TYPE_DEV],
-                                   board='board', config=mock.ANY)
+    build_patch.assert_called_with(
+        images=[constants.IMAGE_TYPE_DEV], board='board', config=mock.ANY)
 
   def testMultipleAndImpliedTypes(self):
     """Test multiple types and implied type handling."""
@@ -112,8 +118,8 @@
     build_patch = self.PatchObject(image_service, 'Build', return_value=result)
 
     image_controller.Create(request, self.response, self.api_config)
-    build_patch.assert_called_with(images=expected_images, board='board',
-                                   config=mock.ANY)
+    build_patch.assert_called_with(
+        images=expected_images, board='board', config=mock.ANY)
 
   def testFailedPackageHandling(self):
     """Test failed packages are populated correctly."""
@@ -320,3 +326,111 @@
     self.PatchObject(image_service, 'Test', return_value=False)
     image_controller.Test(input_proto, output_proto, self.api_config)
     self.assertFalse(output_proto.success)
+
+
+class PushImageTest(cros_test_lib.MockTestCase, api_config.ApiConfigMixin):
+  """Push image test."""
+
+  def setUp(self):
+    self.response = image_pb2.PushImageResponse()
+
+  def _GetRequest(
+      self,
+      gs_image_dir='gs://chromeos-image-archive/atlas-release/R89-13604.0.0',
+      build_target_name='atlas',
+      profile='foo',
+      sign_types=None,
+      dryrun=True):
+    return image_pb2.PushImageRequest(
+        gs_image_dir=gs_image_dir,
+        sysroot=sysroot_pb2.Sysroot(
+            build_target=common_pb2.BuildTarget(name=build_target_name)),
+        profile=common_pb2.Profile(name=profile),
+        sign_types=sign_types,
+        dryrun=dryrun)
+
+  def testValidateOnly(self):
+    """Check that a validate only call does not execute any logic."""
+    patch = self.PatchObject(pushimage, 'PushImage')
+
+    req = self._GetRequest(sign_types=[
+        common_pb2.IMAGE_TYPE_RECOVERY, common_pb2.IMAGE_TYPE_FACTORY,
+        common_pb2.IMAGE_TYPE_FIRMWARE, common_pb2.IMAGE_TYPE_ACCESSORY_USBPD,
+        common_pb2.IMAGE_TYPE_ACCESSORY_RWSIG, common_pb2.IMAGE_TYPE_BASE,
+        common_pb2.IMAGE_TYPE_GSC_FIRMWARE
+    ])
+    res = image_controller.PushImage(req, self.response,
+                                     self.validate_only_config)
+    patch.assert_not_called()
+    self.assertEqual(res, controller.RETURN_CODE_VALID_INPUT)
+
+  def testValidateOnlyInvalid(self):
+    """Check that validate call rejects invalid sign types."""
+    patch = self.PatchObject(pushimage, 'PushImage')
+
+    # Pass unsupported image type.
+    req = self._GetRequest(sign_types=[common_pb2.IMAGE_TYPE_DLC])
+    res = image_controller.PushImage(req, self.response,
+                                     self.validate_only_config)
+    patch.assert_not_called()
+    self.assertEqual(res, controller.RETURN_CODE_INVALID_INPUT)
+
+  def testMockCall(self):
+    """Test that mock call does not execute any logic, returns mocked value."""
+    patch = self.PatchObject(pushimage, 'PushImage')
+
+    rc = image_controller.PushImage(self._GetRequest(), self.response,
+                                    self.mock_call_config)
+    patch.assert_not_called()
+    self.assertEqual(controller.RETURN_CODE_SUCCESS, rc)
+
+  def testMockError(self):
+    """Test that mock call does not execute any logic, returns error."""
+    patch = self.PatchObject(pushimage, 'PushImage')
+
+    rc = image_controller.PushImage(self._GetRequest(), self.response,
+                                    self.mock_error_config)
+    patch.assert_not_called()
+    self.assertEqual(controller.RETURN_CODE_COMPLETED_UNSUCCESSFULLY, rc)
+
+  def testNoBuildTarget(self):
+    """Test no build target given fails."""
+    request = self._GetRequest(build_target_name='')
+
+    # No build target should cause it to fail.
+    with self.assertRaises(cros_build_lib.DieSystemExit):
+      image_controller.PushImage(request, self.response, self.api_config)
+
+  def testNoGsImageDir(self):
+    """Test no image dir given fails."""
+    request = self._GetRequest(gs_image_dir='')
+
+    # No image dir should cause it to fail.
+    with self.assertRaises(cros_build_lib.DieSystemExit):
+      image_controller.PushImage(request, self.response, self.api_config)
+
+  def testCallCorrect(self):
+    """Check that a call is called with the correct parameters."""
+    patch = self.PatchObject(pushimage, 'PushImage')
+
+    request = self._GetRequest(
+        dryrun=False, profile='', sign_types=[common_pb2.IMAGE_TYPE_RECOVERY])
+    image_controller.PushImage(request, self.response, self.api_config)
+    patch.assert_called_with(
+        request.gs_image_dir,
+        request.sysroot.build_target.name,
+        dry_run=request.dryrun,
+        profile=request.profile.name,
+        sign_types=['recovery'])
+
+  def testCallSucceeds(self):
+    """Check that a (dry run) call is made successfully."""
+    request = self._GetRequest(sign_types=[common_pb2.IMAGE_TYPE_RECOVERY])
+    res = image_controller.PushImage(request, self.response, self.api_config)
+    self.assertEqual(res, controller.RETURN_CODE_SUCCESS)
+
+  def testCallFailsWithBadImageDir(self):
+    """Check that a (dry run) call fails when given a bad gs_image_dir."""
+    request = self._GetRequest(gs_image_dir='foo')
+    res = image_controller.PushImage(request, self.response, self.api_config)
+    self.assertEqual(res, controller.RETURN_CODE_COMPLETED_UNSUCCESSFULLY)