diff options
-rw-r--r-- | cloudinit/config/schema.py | 97 | ||||
-rw-r--r-- | tests/unittests/config/test_schema.py | 55 |
2 files changed, 113 insertions, 39 deletions
diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py index d32b7c01..d772b4f9 100644 --- a/cloudinit/config/schema.py +++ b/cloudinit/config/schema.py @@ -17,6 +17,7 @@ import sys import yaml error = partial(error, sys_exit=True) +LOG = logging.getLogger(__name__) _YAML_MAP = {True: 'true', False: 'false', None: 'null'} CLOUD_CONFIG_HEADER = b'#cloud-config' @@ -91,7 +92,16 @@ def get_jsonschema_validator(): # This allows #cloud-config to provide valid yaml "content: !!binary | ..." strict_metaschema = deepcopy(Draft4Validator.META_SCHEMA) - strict_metaschema['additionalProperties'] = False + strict_metaschema["additionalProperties"] = False + + # This additional label allows us to specify a different name + # than the property key when generating docs. + # This is especially useful when using a "patternProperties" regex, + # otherwise the property label in the generated docs will be a + # regular expression. + # http://json-schema.org/understanding-json-schema/reference/object.html#pattern-properties + strict_metaschema["properties"]["label"] = {"type": "string"} + if hasattr(Draft4Validator, 'TYPE_CHECKER'): # jsonschema 3.0+ type_checker = Draft4Validator.TYPE_CHECKER.redefine( 'string', is_schema_byte_string) @@ -140,7 +150,7 @@ def validate_cloudconfig_metaschema(validator, schema: dict, throw=True): ('.'.join([str(p) for p in err.path]), err.message), ) ) from err - logging.warning( + LOG.warning( "Meta-schema validation failed, attempting to validate config " "anyway: %s", err) @@ -168,7 +178,7 @@ def validate_cloudconfig_schema( validate_cloudconfig_metaschema( cloudinitValidator, schema, throw=False) except ImportError: - logging.debug("Ignoring schema validation. jsonschema is not present") + LOG.debug("Ignoring schema validation. jsonschema is not present") return validator = cloudinitValidator(schema, format_checker=FormatChecker()) @@ -180,8 +190,8 @@ def validate_cloudconfig_schema( if strict: raise SchemaValidationError(errors) else: - messages = ['{0}: {1}'.format(k, msg) for k, msg in errors] - logging.warning('Invalid config:\n%s', '\n'.join(messages)) + messages = ["{0}: {1}".format(k, msg) for k, msg in errors] + LOG.warning("Invalid config:\n%s", "\n".join(messages)) def annotated_cloudconfig_file(cloudconfig, original_content, schema_errors): @@ -410,34 +420,53 @@ def _get_property_doc(schema: dict, prefix=" ") -> str: """Return restructured text describing the supported schema properties.""" new_prefix = prefix + ' ' properties = [] - for prop_key, prop_config in schema.get('properties', {}).items(): - # Define prop_name and description for SCHEMA_PROPERTY_TMPL - description = prop_config.get('description', '') - - # Define prop_name and description for SCHEMA_PROPERTY_TMPL - properties.append( - SCHEMA_PROPERTY_TMPL.format( - prefix=prefix, - prop_name=prop_key, - description=_parse_description(description, prefix), - prop_type=_get_property_type(prop_config), + property_keys = [ + schema.get("properties", {}), + schema.get("patternProperties", {}), + ] + + for props in property_keys: + for prop_key, prop_config in props.items(): + # Define prop_name and description for SCHEMA_PROPERTY_TMPL + description = prop_config.get("description", "") + + # Define prop_name and description for SCHEMA_PROPERTY_TMPL + label = prop_config.get("label", prop_key) + properties.append( + SCHEMA_PROPERTY_TMPL.format( + prefix=prefix, + prop_name=label, + description=_parse_description(description, prefix), + prop_type=_get_property_type(prop_config), + ) ) - ) - items = prop_config.get("items") - if items: - if isinstance(items, list): - for item in items: + items = prop_config.get("items") + if items: + if isinstance(items, list): + for item in items: + properties.append( + _get_property_doc(item, prefix=new_prefix) + ) + elif isinstance(items, dict) and ( + items.get("properties") or items.get("patternProperties") + ): properties.append( - _get_property_doc(item, prefix=new_prefix)) - elif isinstance(items, dict) and items.get('properties'): - properties.append(SCHEMA_LIST_ITEM_TMPL.format( - prefix=new_prefix, prop_name=prop_key)) - new_prefix += ' ' - properties.append(_get_property_doc(items, prefix=new_prefix)) - if 'properties' in prop_config: - properties.append( - _get_property_doc(prop_config, prefix=new_prefix)) - return '\n\n'.join(properties) + SCHEMA_LIST_ITEM_TMPL.format( + prefix=new_prefix, prop_name=label + ) + ) + new_prefix += " " + properties.append( + _get_property_doc(items, prefix=new_prefix) + ) + if ( + "properties" in prop_config + or "patternProperties" in prop_config + ): + properties.append( + _get_property_doc(prop_config, prefix=new_prefix) + ) + return "\n\n".join(properties) def _get_examples(meta: MetaSchema) -> str: @@ -494,7 +523,11 @@ def get_meta_doc(meta: MetaSchema, schema: dict) -> str: # cast away type annotation meta_copy = dict(deepcopy(meta)) - meta_copy["property_doc"] = _get_property_doc(schema) + try: + meta_copy["property_doc"] = _get_property_doc(schema) + except AttributeError: + LOG.warning("Unable to render property_doc due to invalid schema") + meta_copy["property_doc"] = "" meta_copy["examples"] = _get_examples(meta) meta_copy["distros"] = ", ".join(meta["distros"]) # Need an underbar of the same length as the name diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py index ed7ab527..40803cae 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -55,7 +55,7 @@ def get_module_variable(var_name) -> dict: schemas = {} files = list( - Path(cloud_init_project_dir("../../cloudinit/config/")).glob("cc_*.py") + Path(cloud_init_project_dir("cloudinit/config/")).glob("cc_*.py") ) modules = [mod.stem for mod in files] @@ -215,12 +215,13 @@ class TestCloudConfigExamples: @pytest.mark.parametrize("schema_id, example", params) @skipUnlessJsonSchema() def test_validateconfig_schema_of_example(self, schema_id, example): - """ For a given example in a config module we test if it is valid + """For a given example in a config module we test if it is valid according to the unified schema of all config modules """ config_load = safe_load(example) validate_cloudconfig_schema( - config_load, self.schema, strict=True) + config_load, self.schema[schema_id], strict=True + ) class ValidateCloudConfigFileTest(CiTestCase): @@ -462,6 +463,44 @@ class GetSchemaDocTest(CiTestCase): get_meta_doc(invalid_meta, schema) self.assertIn(key, str(context_mgr.exception)) + def test_label_overrides_property_name(self): + """get_meta_doc overrides property name with label.""" + schema = { + "properties": { + "prop1": { + "type": "string", + "label": "label1", + }, + "prop_no_label": { + "type": "string", + }, + "prop_array": { + "label": 'array_label', + "type": "array", + "items": { + "type": "object", + "properties": { + "some_prop": {"type": "number"}, + }, + }, + }, + }, + "patternProperties": { + "^.*$": { + "type": "string", + "label": "label2", + } + } + } + meta_doc = get_meta_doc(self.meta, schema) + assert "**label1:** (string)" in meta_doc + assert "**label2:** (string" in meta_doc + assert "**prop_no_label:** (string)" in meta_doc + assert "Each item in **array_label** list" in meta_doc + + assert "prop1" not in meta_doc + assert ".*" not in meta_doc + class AnnotatedCloudconfigFileTest(CiTestCase): maxDiff = None @@ -626,9 +665,11 @@ def _get_meta_doc_examples(): examples_dir = Path(cloud_init_project_dir('doc/examples')) assert examples_dir.is_dir() - all_text_files = (f for f in examples_dir.glob('cloud-config*.txt') - if not f.name.startswith('cloud-config-archive')) - return all_text_files + return ( + str(f) + for f in examples_dir.glob("cloud-config*.txt") + if not f.name.startswith("cloud-config-archive") + ) class TestSchemaDocExamples: @@ -637,7 +678,7 @@ class TestSchemaDocExamples: @pytest.mark.parametrize("example_path", _get_meta_doc_examples()) @skipUnlessJsonSchema() def test_schema_doc_examples(self, example_path): - validate_cloudconfig_file(str(example_path), self.schema) + validate_cloudconfig_file(example_path, self.schema) class TestStrictMetaschema: |