summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrett Holman <bholman.devel@gmail.com>2022-01-06 15:33:18 -0700
committerGitHub <noreply@github.com>2022-01-06 15:33:18 -0700
commit3e64acda13c826074a502d37b9e11e07d4238bc6 (patch)
tree4ed7cab2d004d2ce4f9426963bd4d52709aaf09f
parentfef532d7d6e8932b1b91eb0fc811335d55a79257 (diff)
downloadcloud-init-git-3e64acda13c826074a502d37b9e11e07d4238bc6.tar.gz
Don't throw exceptions for empty cloud config (#1130)
Warn during boot when an empty config is provided. Likewise, `cloud-init devel schema --annotate` should not throw exception, return something meaningful instead.
-rw-r--r--cloudinit/config/schema.py27
-rw-r--r--cloudinit/handlers/cloud_config.py9
-rw-r--r--tests/unittests/config/test_schema.py38
3 files changed, 67 insertions, 7 deletions
diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py
index 8ec4ab6a..3a77ca00 100644
--- a/cloudinit/config/schema.py
+++ b/cloudinit/config/schema.py
@@ -181,6 +181,7 @@ def validate_cloudconfig_schema(
@raises: SchemaValidationError when provided config does not validate
against the provided schema.
+ @raises: RuntimeError when provided config sourced from YAML is not a dict.
"""
try:
(cloudinitValidator, FormatChecker) = get_jsonschema_validator()
@@ -217,13 +218,21 @@ def annotated_cloudconfig_file(cloudconfig, original_content, schema_errors):
if not schema_errors:
return original_content
schemapaths = {}
+ errors_by_line = defaultdict(list)
+ error_footer = []
+ error_header = "# Errors: -------------\n{0}\n\n"
+ annotated_content = []
+ lines = original_content.decode().split("\n")
+ if not isinstance(cloudconfig, dict):
+ # Return a meaningful message on empty cloud-config
+ return "\n".join(
+ lines
+ + [error_header.format("# E1: Cloud-config is not a YAML dict.")]
+ )
if cloudconfig:
schemapaths = _schemapath_for_cloudconfig(
cloudconfig, original_content
)
- errors_by_line = defaultdict(list)
- error_footer = []
- annotated_content = []
for path, msg in schema_errors:
match = re.match(r"format-l(?P<line>\d+)\.c(?P<col>\d+).*", path)
if match:
@@ -236,7 +245,6 @@ def annotated_cloudconfig_file(cloudconfig, original_content, schema_errors):
msg = "Line {line} column {col}: {msg}".format(
line=line, col=col, msg=msg
)
- lines = original_content.decode().split("\n")
error_index = 1
for line_number, line in enumerate(lines, 1):
errors = errors_by_line[line_number]
@@ -247,11 +255,10 @@ def annotated_cloudconfig_file(cloudconfig, original_content, schema_errors):
error_footer.append("# E{0}: {1}".format(error_index, error))
error_index += 1
annotated_content.append(line + "\t\t# " + ",".join(error_label))
+
else:
annotated_content.append(line)
- annotated_content.append(
- "# Errors: -------------\n{0}\n\n".format("\n".join(error_footer))
- )
+ annotated_content.append(error_header.format("\n".join(error_footer)))
return "\n".join(annotated_content)
@@ -318,6 +325,10 @@ def validate_cloudconfig_file(config_path, schema, annotate=False):
if annotate:
print(annotated_cloudconfig_file({}, content, error.schema_errors))
raise error from e
+ if not isinstance(cloudconfig, dict):
+ # Return a meaningful message on empty cloud-config
+ if not annotate:
+ raise RuntimeError("Cloud-config is not a YAML dict.")
try:
validate_cloudconfig_schema(cloudconfig, schema, strict=True)
except SchemaValidationError as e:
@@ -662,6 +673,8 @@ def handle_schema_args(name, args):
exclusive_args = [args.config_file, args.docs, args.system]
if len([arg for arg in exclusive_args if arg]) != 1:
error("Expected one of --config-file, --system or --docs arguments")
+ if args.annotate and args.docs:
+ error("Invalid flag combination. Cannot use --annotate with --docs")
full_schema = get_schema()
if args.config_file or args.system:
try:
diff --git a/cloudinit/handlers/cloud_config.py b/cloudinit/handlers/cloud_config.py
index 2e694671..8070c6cb 100644
--- a/cloudinit/handlers/cloud_config.py
+++ b/cloudinit/handlers/cloud_config.py
@@ -89,6 +89,9 @@ class CloudConfigPartHandler(handlers.Handler):
# or the merge type from the headers or default to our own set
# if neither exists (or is empty) from the later.
payload_yaml = util.load_yaml(payload)
+ if payload_yaml is None:
+ raise ValueError("empty cloud config")
+
mergers_yaml = mergers.dict_extract_mergers(payload_yaml)
mergers_header = mergers.string_extract_mergers(merge_header_headers)
all_mergers = []
@@ -139,6 +142,12 @@ class CloudConfigPartHandler(handlers.Handler):
for i in ("\n", "\r", "\t"):
filename = filename.replace(i, " ")
self.file_names.append(filename.strip())
+ except ValueError as err:
+ LOG.warning(
+ "Failed at merging in cloud config part from %s: %s",
+ filename,
+ err,
+ )
except Exception:
util.logexc(
LOG, "Failed at merging in cloud config part from %s", filename
diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py
index fb5b891d..822efe5a 100644
--- a/tests/unittests/config/test_schema.py
+++ b/tests/unittests/config/test_schema.py
@@ -546,6 +546,31 @@ class AnnotatedCloudconfigFileTest(CiTestCase):
content, annotated_cloudconfig_file({}, content, schema_errors=[])
)
+ def test_annotated_cloudconfig_file_with_non_dict_cloud_config(self):
+ """Error when empty non-dict cloud-config is provided.
+
+ OurJSON validation when user-data is None type generates a bunch
+ schema validation errors of the format:
+ ('', "None is not of type 'object'"). Ignore those symptoms and
+ report the general problem instead.
+ """
+ content = b"\n\n\n"
+ expected = "\n".join(
+ [
+ content.decode(),
+ "# Errors: -------------",
+ "# E1: Cloud-config is not a YAML dict.\n\n",
+ ]
+ )
+ self.assertEqual(
+ expected,
+ annotated_cloudconfig_file(
+ None,
+ content,
+ schema_errors=[("", "None is not of type 'object'")],
+ ),
+ )
+
def test_annotated_cloudconfig_file_schema_annotates_and_adds_footer(self):
"""With schema_errors, error lines are annotated and a footer added."""
content = dedent(
@@ -658,6 +683,19 @@ class TestMain:
_out, err = capsys.readouterr()
assert "Error:\nConfigfile NOT_A_FILE does not exist\n" == err
+ def test_main_invalid_flag_combo(self, capsys):
+ """Main exits non-zero when invalid flag combo used."""
+ myargs = ["mycmd", "--annotate", "--docs", "DOES_NOT_MATTER"]
+ with mock.patch("sys.argv", myargs):
+ with pytest.raises(SystemExit) as context_manager:
+ main()
+ assert 1 == context_manager.value.code
+ _, err = capsys.readouterr()
+ assert (
+ "Error:\nInvalid flag combination. "
+ "Cannot use --annotate with --docs\n" == err
+ )
+
def test_main_prints_docs(self, capsys):
"""When --docs parameter is provided, main generates documentation."""
myargs = ["mycmd", "--docs", "all"]