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/scripts/build_api.py b/scripts/build_api.py
index 8c2e139..01b16c1 100644
--- a/scripts/build_api.py
+++ b/scripts/build_api.py
@@ -3,19 +3,22 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-"""The build API entry point."""
+"""The Build API entry point."""
 
 from __future__ import print_function
 
 import os
-import sys
+
+from google.protobuf import json_format
 
 from chromite.api import api_config as api_config_lib
 from chromite.api import controller
 from chromite.api import router as router_lib
+from chromite.api.gen.chromite.api import build_api_config_pb2
 from chromite.lib import commandline
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_logging as logging
+from chromite.lib import osutils
 from chromite.lib import tee
 from chromite.utils import matching
 
@@ -24,83 +27,27 @@
   """Build the argument parser."""
   parser = commandline.ArgumentParser(description=__doc__)
 
-  call_group = parser.add_argument_group(
-      'API Call Options',
-      'These options are used to execute an endpoint. When making a call every '
-      'argument in this group is required.')
-  call_group.add_argument(
+  parser.add_argument(
       'service_method',
-      nargs='?',
       help='The "chromite.api.Service/Method" that is being called.')
-  call_group.add_argument(
+  parser.add_argument(
       '--input-json',
       type='path',
       help='Path to the JSON serialized input argument protobuf message.')
-  call_group.add_argument(
+  parser.add_argument(
       '--output-json',
       type='path',
       help='The path to which the result protobuf message should be written.')
-  call_group.add_argument(
+  parser.add_argument(
+      '--config-json',
+      type='path',
+      help='The path to the Build API call configs.')
+  # TODO(crbug.com/1040978): Remove after usages removed.
+  parser.add_argument(
       '--tee-log',
       type='path',
       help='The path to which stdout and stderr should be teed to.')
 
-  ux_group = parser.add_argument_group('Developer Options',
-                                       'Options to help developers.')
-  # Lists the full chromite.api.Service/Method, has both names to match
-  # whichever mental model people prefer.
-  ux_group.add_argument(
-      '--list-methods',
-      '--list-services',
-      action='store_true',
-      dest='list_services',
-      help='List the name of each registered "chromite.api.Service/Method".')
-
-  # Run configuration options.
-  test_group = parser.add_argument_group(
-      'Testing Options',
-      'These options are used to execute various tests against the API. These '
-      'options are mutually exclusive. Calling code can use these options to '
-      'validate inputs and test their handling of each return code case for '
-      'each endpoint.')
-  call_modifications = test_group.add_mutually_exclusive_group()
-  call_modifications.add_argument(
-      '--validate-only',
-      action='store_true',
-      default=False,
-      help='When set, only runs the argument validation logic. Calls produce '
-           'a return code of 0 iff the input proto comprises arguments that '
-           'are a valid call to the endpoint, or 1 otherwise.')
-  # See: api/faux.py for the mock call and error implementations.
-  call_modifications.add_argument(
-      '--mock-call',
-      action='store_true',
-      default=False,
-      help='When set, returns a valid, mock response rather than running the '
-           'endpoint. This allows API consumers to more easily test their '
-           'implementations against the version of the API being called. '
-           'This argument will always result in a return code of 0.')
-  call_modifications.add_argument(
-      '--mock-error',
-      action='store_true',
-      default=False,
-      help='When set, return a valid, mock error response rather than running '
-           'the endpoint. This allows API consumers to test their error '
-           'handling semantics against the version of the API being called. '
-           'This argument will always result in a return code of 2 iff the '
-           'endpoint ever produces a return code of 2, otherwise will always'
-           'produce a return code of 1.')
-  call_modifications.add_argument(
-      '--mock-invalid',
-      action='store_true',
-      default=False,
-      help='When set, return a mock validation error response rather than '
-           'running the endpoint. This allows API consumers to test their '
-           'validation error handling semantics against the version of the API '
-           'being called without having to understand how to construct an '
-           'invalid request. '
-           'This argument will always result in a return code of 1.')
-
   return parser
 
 
@@ -116,21 +63,11 @@
 
   methods = router.ListMethods()
 
-  if opts.list_services:
-    # We just need to print the methods and we're done.
-    for method in methods:
-      print(method)
-    sys.exit(0)
-
   # Positional service_method argument validation.
-  if not opts.service_method:
-    parser.error('Must pass "Service/Method".')
-
   parts = opts.service_method.split('/')
   if len(parts) != 2:
-    parser.error(
-        'Must pass the correct format: (e.g. chromite.api.SdkService/Create).'
-        'Use --list-methods to see a full list.')
+    parser.error('Invalid service/method specification format. It should be '
+                 'something like chromite.api.SdkService/Create.')
 
   if opts.service_method not in methods:
     # Unknown method, try to match against known methods and make a suggestion.
@@ -152,11 +89,15 @@
   if not os.path.exists(opts.input_json):
     parser.error('Input file does not exist.')
 
-  # Build the config object from the options.
-  opts.config = api_config_lib.ApiConfig(
-      validate_only=opts.validate_only,
-      mock_call=opts.mock_call,
-      mock_error=opts.mock_error)
+  config_msg = build_api_config_pb2.BuildApiConfig()
+  if opts.config_json:
+    try:
+      json_format.Parse(osutils.ReadFile(opts.config_json), config_msg,
+                        ignore_unknown_fields=True)
+    except IOError as e:
+      parser.error(e)
+
+  opts.config = api_config_lib.build_config_from_proto(config_msg)
 
   opts.Freeze()
   return opts
@@ -171,12 +112,15 @@
     if opts.tee_log:
       stack.Add(tee.Tee, opts.tee_log)
       logging.info('Teeing stdout and stderr to %s', opts.tee_log)
+    if opts.config.log_path:
+      stack.Add(tee.Tee, opts.config.log_path)
+      logging.info('Teeing stdout and stderr to %s', opts.config.log_path)
     tee_log_env_value = os.environ.get('BUILD_API_TEE_LOG_FILE')
     if tee_log_env_value:
       stack.Add(tee.Tee, tee_log_env_value)
       logging.info('Teeing stdout and stderr to env path %s', tee_log_env_value)
 
-    if opts.mock_invalid:
+    if opts.config.mock_invalid:
       # --mock-invalid handling. We print error messages, but no output is ever
       # set for validation errors, so we can handle it by just giving back the
       # correct return code here.