Build API config: Use a proto-based configuration

Switch the Build API to use a proto configuration rather than
CLI options. This provides cleaner backwards compatibility for
the build api itself.

BUG=chromium:1040978
TEST=run_tests

Change-Id: I515271b4244c354c38538b30dcc1cfa07517f821
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1994725
Tested-by: Alex Klein <saklein@chromium.org>
Reviewed-by: Will Bradley <wbbradley@chromium.org>
Commit-Queue: Alex Klein <saklein@chromium.org>
diff --git a/api/api_config.py b/api/api_config.py
index 6880391..65a4bb0 100644
--- a/api/api_config.py
+++ b/api/api_config.py
@@ -7,21 +7,57 @@
 
 from __future__ import print_function
 
+from chromite.api.gen.chromite.api import build_api_config_pb2
+
+
+class Error(Exception):
+  """Base error class for the module."""
+
+
+class UnknownCallTypeEnumValue(Error):
+  """Thrown when the call type enum value in proto is not configured here."""
+
 
 class ApiConfig(object):
   """API Config class."""
+  # Call type constants.
+  CALL_TYPE_EXECUTE = 1
+  CALL_TYPE_VALIDATE_ONLY = 2
+  CALL_TYPE_MOCK_SUCCESS = 3
+  CALL_TYPE_MOCK_FAILURE = 4
+  CALL_TYPE_MOCK_INVALID = 5
 
-  def __init__(self, validate_only=False, mock_call=False, mock_error=False):
-    assert [validate_only, mock_call, mock_error].count(True) <= 1
-    self.validate_only = validate_only
-    self.mock_call = mock_call
-    self.mock_error = mock_error
-    self._is_mock = self.mock_call or self.mock_error
+  # Maps the proto enum to the type constants.
+  TYPE_ENUM_MAP = {
+      build_api_config_pb2.CALL_TYPE_NONE: CALL_TYPE_EXECUTE,
+      build_api_config_pb2.CALL_TYPE_EXECUTE: CALL_TYPE_EXECUTE,
+      build_api_config_pb2.CALL_TYPE_VALIDATE_ONLY: CALL_TYPE_VALIDATE_ONLY,
+      build_api_config_pb2.CALL_TYPE_MOCK_SUCCESS: CALL_TYPE_MOCK_SUCCESS,
+      build_api_config_pb2.CALL_TYPE_MOCK_FAILURE: CALL_TYPE_MOCK_FAILURE,
+      build_api_config_pb2.CALL_TYPE_MOCK_INVALID: CALL_TYPE_MOCK_INVALID,
+  }
+
+  # Maps the type constants to the proto enums.
+  ENUM_TYPE_MAP = {
+      CALL_TYPE_EXECUTE: build_api_config_pb2.CALL_TYPE_EXECUTE,
+      CALL_TYPE_VALIDATE_ONLY: build_api_config_pb2.CALL_TYPE_VALIDATE_ONLY,
+      CALL_TYPE_MOCK_SUCCESS: build_api_config_pb2.CALL_TYPE_MOCK_SUCCESS,
+      CALL_TYPE_MOCK_FAILURE: build_api_config_pb2.CALL_TYPE_MOCK_FAILURE,
+      CALL_TYPE_MOCK_INVALID: build_api_config_pb2.CALL_TYPE_MOCK_INVALID,
+  }
+
+  # The valid call types.
+  _VALID_CALL_TYPES = tuple(ENUM_TYPE_MAP.keys())
+
+  def __init__(self, call_type=CALL_TYPE_EXECUTE, log_path=None):
+    assert call_type in self._VALID_CALL_TYPES
+    self._call_type = call_type
+    # Explicit `or None` to simplify proto default empty string.
+    self.log_path = log_path or None
 
   def __eq__(self, other):
     if self.__class__ is other.__class__:
-      return ((self.validate_only, self.mock_call, self.mock_error) ==
-              (other.validate_only, other.mock_call, other.mock_error))
+      return self.__dict__ == other.__dict__
 
     return NotImplemented
 
@@ -29,7 +65,63 @@
 
   @property
   def do_validation(self):
-    return not self._is_mock
+    # We skip validation for all mock calls, so do validation when it's
+    # anything but a mocked call.
+    return not (self.mock_call or self.mock_error or self.mock_invalid)
+
+  @property
+  def validate_only(self):
+    return self._call_type == self.CALL_TYPE_VALIDATE_ONLY
+
+  @property
+  def mock_call(self):
+    return self._call_type == self.CALL_TYPE_MOCK_SUCCESS
+
+  @property
+  def mock_error(self):
+    return self._call_type == self.CALL_TYPE_MOCK_FAILURE
+
+  @property
+  def mock_invalid(self):
+    return self._call_type == self.CALL_TYPE_MOCK_INVALID
+
+  def get_proto(self, for_inside_execution=True):
+    """Get the config as a proto.
+
+    Args:
+      for_inside_execution (bool): Allows avoiding propagating configs that are
+        irrelevant for the build api process executed inside the chroot.
+        Enabled by default.
+
+    Returns:
+      build_api_config_pb2.BuildApiConfig
+    """
+    config = build_api_config_pb2.BuildApiConfig()
+    config.call_type = self.ENUM_TYPE_MAP[self._call_type]
+
+    if not for_inside_execution:
+      # Add values not needed when reexecuting.
+      config.log_path = self.log_path
+
+    return config
+
+
+def build_config_from_proto(config_proto):
+  """Build an ApiConfig instance from a BuildApiConfig message.
+
+  Args:
+    config_proto (build_api_config_pb2.BuildApiConfig): The proto config.
+
+  Returns:
+    ApiConfig
+  """
+  assert isinstance(config_proto, build_api_config_pb2.BuildApiConfig)
+
+  if config_proto.call_type not in ApiConfig.TYPE_ENUM_MAP:
+    raise UnknownCallTypeEnumValue('The given protobuf call_type value is not '
+                                   'configured in api_config.')
+  return ApiConfig(call_type=ApiConfig.TYPE_ENUM_MAP[config_proto.call_type],
+                   log_path=config_proto.log_path)
 
 
 class ApiConfigMixin(object):
@@ -45,7 +137,7 @@
 
   @property
   def validate_only_config(self):
-    return ApiConfig(validate_only=True)
+    return ApiConfig(call_type=ApiConfig.CALL_TYPE_VALIDATE_ONLY)
 
   @property
   def no_validate_config(self):
@@ -53,8 +145,8 @@
 
   @property
   def mock_call_config(self):
-    return ApiConfig(mock_call=True)
+    return ApiConfig(call_type=ApiConfig.CALL_TYPE_MOCK_SUCCESS)
 
   @property
   def mock_error_config(self):
-    return ApiConfig(mock_error=True)
+    return ApiConfig(call_type=ApiConfig.CALL_TYPE_MOCK_FAILURE)