ImageService/Create: Implement base_is_recovery

An image named "recovery_image.bin" needs to exist in order for the
Chromebook Recovery Utility, GoldenEye and other tools to discover an
image for a given build target.

This change adds an attribute to the
BuildConfig, base_is_recovery, that when true, skips recovery image
creation and copies the base image to "recovery_image.bin" instead.

BUG=b:229382044
TEST=./run_tests

Change-Id: I622a0a8cb68e032060c978523eb22698dc786ba4
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/3687376
Tested-by: Joseph Sussman <josephsussman@google.com>
Reviewed-by: Chris Gerber <gerb@google.com>
Auto-Submit: Joseph Sussman <josephsussman@google.com>
Commit-Queue: Chris Gerber <gerb@google.com>
diff --git a/api/controller/image_unittest.py b/api/controller/image_unittest.py
index ee63b29..6d31fa9 100644
--- a/api/controller/image_unittest.py
+++ b/api/controller/image_unittest.py
@@ -5,6 +5,7 @@
 """Image service tests."""
 
 import os
+from pathlib import Path
 from unittest import mock
 
 from chromite.api import api_config
@@ -33,7 +34,8 @@
                   types=None,
                   version=None,
                   builder_path=None,
-                  disable_rootfs_verification=False):
+                  disable_rootfs_verification=False,
+                  base_is_recovery=False):
     """Helper to build a request instance."""
     return image_pb2.CreateImageRequest(
         build_target={'name': board},
@@ -41,6 +43,7 @@
         disable_rootfs_verification=disable_rootfs_verification,
         version=version,
         builder_path=builder_path,
+        base_is_recovery=base_is_recovery,
     )
 
   def testValidateOnly(self):
@@ -166,6 +169,35 @@
                         rc)
     self.assertFalse(self.response.failed_packages)
 
+  def testBaseIsRecoveryTrue(self):
+    """Test that cp is called."""
+    types = [common_pb2.IMAGE_TYPE_BASE,
+             common_pb2.IMAGE_TYPE_RECOVERY]
+    input_proto = self._GetRequest(board='board', types=types,
+                                   base_is_recovery=True)
+    # Images must exist to be added to the result.
+    self.PatchObject(Path, 'exists', return_value=True)
+    rc_mock = cros_test_lib.RunCommandMock()
+    rc_mock.SetDefaultCmdResult(returncode=0)
+    with rc_mock:
+      image_controller.Create(input_proto, self.response, self.api_config)
+    rc_mock.assertCommandContains(['cp', mock.ANY, mock.ANY])
+
+  def testBaseIsRecoveryFalse(self):
+    """Test that mod_image_for_recovery.sh is called."""
+    types = [common_pb2.IMAGE_TYPE_BASE,
+             common_pb2.IMAGE_TYPE_RECOVERY]
+    input_proto = self._GetRequest(board='board', types=types,
+                                   base_is_recovery=False)
+    self.PatchObject(Path, 'exists', return_value=True)
+    rc_mock = cros_test_lib.RunCommandMock()
+    rc_mock.SetDefaultCmdResult(returncode=0)
+    with rc_mock:
+      image_controller.Create(input_proto, self.response, self.api_config)
+    mod_script = os.path.join(constants.CROSUTILS_DIR,
+                              'mod_image_for_recovery.sh')
+    rc_mock.assertCommandContains([mod_script, '--board', 'board', mock.ANY])
+
 
 class ImageSignerTestTest(cros_test_lib.MockTempDirTestCase,
                           api_config.ApiConfigMixin):