Write merge_flat_configs utility to simplify merging FlatConfigLists

BUG=chromium:1073073
TEST=manual

Change-Id: I7ebb5d9b7303688cd6098f6202aa991f14658e46
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/config/+/2465573
Commit-Queue: Sean McAllister <smcallis@google.com>
Auto-Submit: Sean McAllister <smcallis@google.com>
Reviewed-by: Andrew Lamb <andrewlamb@chromium.org>
diff --git a/payload_utils/checker/io_utils.py b/payload_utils/checker/io_utils.py
index ee43989..e8b6585 100644
--- a/payload_utils/checker/io_utils.py
+++ b/payload_utils/checker/io_utils.py
@@ -12,7 +12,7 @@
 from google.protobuf.message import Message
 
 from chromiumos.config.payload import config_bundle_pb2
-
+from chromiumos.config.payload import flat_config_pb2
 
 
 def write_message_json(message: Message, path: pathlib.Path, \
@@ -32,11 +32,10 @@
 
 
 def read_config(path: str) -> config_bundle_pb2.ConfigBundle:
-  """Reads a ConfigBundle mesage from a jsonpb file.
+  """Reads a ConfigBundle message from a jsonpb file.
 
   Args:
-    path: Path to the json proto. See note above about deprecated repo
-           root behavior.
+    path: Path to the json proto.
 
   Returns:
     ConfigBundle parsed from file.
@@ -47,6 +46,21 @@
   return project_config
 
 
+def read_flat_config(path: str) -> flat_config_pb2.FlatConfigList:
+  """Reads a FlatConfigList message from a jsonpb file.
+
+  Args:
+    path: Path to the json proto.
+
+  Returns:
+    FlatConfigList parsed from file.
+  """
+  flat_config = flat_config_pb2.FlatConfigList()
+  with open(path, 'r') as f:
+    json_format.Parse(f.read(), flat_config)
+  return flat_config
+
+
 def read_model_sku_json(factory_dir: pathlib.Path) -> Dict[str, Any]:
   """Reads and parses the model_sku.json file.
 
diff --git a/payload_utils/checker/io_utils_test.py b/payload_utils/checker/io_utils_test.py
index e2bed4c..d2da36f 100644
--- a/payload_utils/checker/io_utils_test.py
+++ b/payload_utils/checker/io_utils_test.py
@@ -12,6 +12,7 @@
 from google.protobuf import json_format
 
 from checker import io_utils
+from common import config_bundle_utils
 
 from chromiumos.config.payload.config_bundle_pb2 import ConfigBundle
 from chromiumos.config.api.program_pb2 import Program
@@ -22,6 +23,7 @@
 
   def setUp(self):
     self.config = ConfigBundle(program_list=[Program(name='TestProgram1')])
+    self.flat_config = config_bundle_utils.flatten_config(self.config)
     repo_path = tempfile.mkdtemp()
 
     os.mkdir(os.path.join(repo_path, 'generated'))
@@ -32,6 +34,13 @@
     with open(self.config_path, 'w') as f:
       print(json_output, file=f)
 
+    self.flat_config_path = os.path.join(repo_path, 'generated',
+                                         'flattened.jsonproto')
+    json_output = json_format.MessageToJson(
+        self.flat_config, sort_keys=True, use_integers_for_enums=True)
+    with open(self.flat_config_path, 'w') as f:
+      print(json_output, file=f)
+
     self.factory_path = os.path.join(repo_path, 'factory')
     os.makedirs(os.path.join(self.factory_path, 'generated'))
     self.model_sku = {"model": {"a": 1}}
@@ -44,6 +53,11 @@
     """Tests the json proto can be read."""
     self.assertEqual(io_utils.read_config(self.config_path), self.config)
 
+  def test_read_flat_config(self):
+    """Tests the json proto can be read."""
+    self.assertEqual(
+        io_utils.read_flat_config(self.flat_config_path), self.flat_config)
+
   def test_read_model_sku_json(self):
     """Tests model_sku.json can be read."""
     self.assertDictEqual(
diff --git a/payload_utils/merge_flat_configs.py b/payload_utils/merge_flat_configs.py
new file mode 100755
index 0000000..4bcb8b6
--- /dev/null
+++ b/payload_utils/merge_flat_configs.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Merge two FlatConfigList messages into a single FlatConfigList.
+
+The second FlatConfigList is optional, in which case the first input is
+copied to the output."""
+
+import argparse
+
+from checker import io_utils
+
+from chromiumos.config.payload import flat_config_pb2
+
+
+def merge(files, outfile):
+  """Merge multiple FlatConfigList .jsonproto files.
+
+  Merge the given files into a single FlatConfigList and write to the output.
+
+  Args:
+    files  ([str]): .jsonproto files containing FlatConfigList
+    outfile (str): filename to which to write merged config
+  """
+
+  config = flat_config_pb2.FlatConfigList()
+  for file in files:
+    config.values.MergeFrom(io_utils.read_flat_config(file).values)
+  io_utils.write_message_json(config, outfile)
+
+
+if __name__ == "__main__":
+  parser = argparse.ArgumentParser(description=__doc__)
+  parser.add_argument(
+      "input",
+      type=str,
+      nargs='+',
+      help="FlatConfigList to merge in jsonpb format.")
+  parser.add_argument(
+      '-o',
+      '--output',
+      type=str,
+      required=True,
+      help='output file to write FlatConfigList jsonproto to')
+
+  options = parser.parse_args()
+  merge(options.input, options.output)