]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
common: make y2c.py choke on duplicate keys 40891/head
authorIlya Dryomov <idryomov@gmail.com>
Fri, 16 Apr 2021 11:55:56 +0000 (13:55 +0200)
committerIlya Dryomov <idryomov@gmail.com>
Fri, 16 Apr 2021 14:53:49 +0000 (16:53 +0200)
Commit 5505fc0051a3 ("common: generate legacy_config_opts.h from
.yaml.in files") inadvertently reverted a change of a default value by
adding a second "default" key with the old value.  This was corrected
in commit 75e07f8638ef ("common/options/global: correct default of
auth_mon_ticket_ttl"), but highlights that mis-merging a yaml file
is rather easy.

To prevent this happening again, fail the build if duplicate keys
exist in any of src/common/options/*.yaml.in files.

Signed-off-by: Ilya Dryomov <idryomov@gmail.com>
src/common/options/y2c.py

index 8064177296f23be380c197df5527d9bbf8752d58..3ff34fa8239a81e294c4791ad5f27876009477dc 100755 (executable)
@@ -201,6 +201,7 @@ def yaml_to_h(opt):
     else:
         return ''
 
+
 TEMPLATE_CC = '''#include "common/options.h"
 {headers}
 
@@ -212,6 +213,26 @@ std::vector<Option> get_{name}_options() {{
 '''
 
 
+# PyYAML doesn't check for duplicates even though the YAML spec says
+# that mapping keys must be unique and that duplicates must be treated
+# as an error.  See https://github.com/yaml/pyyaml/issues/165.
+#
+# This workaround breaks merge keys -- in "<<: *xyz", duplicate keys
+# from xyz mapping raise an error instead of being discarded.
+class UniqueKeySafeLoader(yaml.SafeLoader):
+    def construct_mapping(self, node, deep=False):
+        mapping = super().construct_mapping(node, deep)
+        keys = set()
+        for key_node, _ in node.value:
+            key = self.construct_object(key_node, deep=deep)
+            if key in keys:
+                raise yaml.constructor.ConstructorError(None, None,
+                                                        "found duplicate key",
+                                                        key_node.start_mark)
+            keys.add(key)
+        return mapping
+
+
 def translate(opts):
     if opts.raw:
         prelude, epilogue = '', ''
@@ -228,7 +249,7 @@ def translate(opts):
     with open(opts.input) as infile, \
          open(opts.output, 'w') as cc_file, \
          open(opts.legacy, 'w') as h_file:
-        yml = yaml.safe_load(infile)
+        yml = yaml.load(infile, Loader=UniqueKeySafeLoader)
         headers = yml.get('headers', '')
         cc_file.write(prelude.format(name=name, headers=headers))
         options = yml['options']
@@ -292,7 +313,7 @@ def readable_millisecs(value, typ):
 
 def readable(opts):
     with open(opts.input) as infile, open(opts.output, 'w') as outfile:
-        yml = yaml.safe_load(infile)
+        yml = yaml.load(infile, Loader=UniqueKeySafeLoader)
         options = yml['options']
         for option in options:
             typ = option['type']