summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/config/schema.py97
-rw-r--r--tests/unittests/config/test_schema.py55
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: