summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrett Holman <bholman.devel@gmail.com>2022-04-27 10:05:55 -0500
committerGitHub <noreply@github.com>2022-04-27 09:05:55 -0600
commit3bcffacb216d683241cf955e4f7f3e89431c1491 (patch)
tree4a0ada8fb7146e79d6c0b4aa70e4fa882bad559c
parent2f496d60d16ebf7c85ef6afab2b73d449280932d (diff)
downloadcloud-init-git-3bcffacb216d683241cf955e4f7f3e89431c1491.tar.gz
Promote cloud-init schema from devel to top level subcommand (#1402)
-rw-r--r--Makefile3
-rw-r--r--bash_completion/cloud-init6
-rw-r--r--cloudinit/cmd/devel/parser.py8
-rw-r--r--cloudinit/cmd/main.py32
-rw-r--r--doc/man/cloud-init.14
-rw-r--r--doc/rtd/topics/cli.rst29
-rw-r--r--doc/rtd/topics/faq.rst2
-rw-r--r--doc/rtd/topics/tutorial.rst2
-rw-r--r--tests/integration_tests/modules/test_cli.py8
-rw-r--r--tests/unittests/test_cli.py30
10 files changed, 77 insertions, 47 deletions
diff --git a/Makefile b/Makefile
index cb005467..9584ccc1 100644
--- a/Makefile
+++ b/Makefile
@@ -133,7 +133,8 @@ _CHECK_SPELLING := find doc -type f -exec spellintian {} + | \
grep -v -e 'doc/rtd/topics/cli.rst: modules modules' \
-e 'doc/examples/cloud-config-mcollective.txt: WARNING WARNING' \
-e 'doc/examples/cloud-config-power-state.txt: Bye Bye' \
- -e 'doc/examples/cloud-config.txt: Bye Bye'
+ -e 'doc/examples/cloud-config.txt: Bye Bye' \
+ -e 'doc/rtd/topics/cli.rst: DOCS DOCS'
# For CI we require a failing return code when spellintian finds spelling errors
diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init
index b9f137b1..1eceb472 100644
--- a/bash_completion/cloud-init
+++ b/bash_completion/cloud-init
@@ -45,6 +45,9 @@ _cloudinit_complete()
query)
COMPREPLY=($(compgen -W "--all --help --instance-data --list-keys --user-data --vendor-data --debug" -- $cur_word));;
+ schema)
+ COMPREPLY=($(compgen -W "--help --config-file --docs --annotate --system" -- $cur_word))
+ ;;
single)
COMPREPLY=($(compgen -W "--help --name --frequency --report" -- $cur_word))
;;
@@ -72,9 +75,6 @@ _cloudinit_complete()
;;
render)
COMPREPLY=($(compgen -W "--help --instance-data --debug" -- $cur_word));;
- schema)
- COMPREPLY=($(compgen -W "--help --config-file --doc --annotate" -- $cur_word))
- ;;
show)
COMPREPLY=($(compgen -W "--help --format --infile --outfile" -- $cur_word))
;;
diff --git a/cloudinit/cmd/devel/parser.py b/cloudinit/cmd/devel/parser.py
index 76b16c2e..460b94b3 100644
--- a/cloudinit/cmd/devel/parser.py
+++ b/cloudinit/cmd/devel/parser.py
@@ -6,8 +6,6 @@
import argparse
-from cloudinit.config import schema
-
from . import hotplug_hook, make_mime, net_convert, render
@@ -28,12 +26,6 @@ def get_parser(parser=None):
hotplug_hook.handle_args,
),
(
- "schema",
- "Validate cloud-config files for document schema",
- schema.get_parser,
- schema.handle_schema_args,
- ),
- (
net_convert.NAME,
net_convert.__doc__,
net_convert.get_parser,
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index c9be41b3..afd0a8d8 100644
--- a/cloudinit/cmd/main.py
+++ b/cloudinit/cmd/main.py
@@ -836,8 +836,7 @@ def main_features(name, args):
def main(sysv_args=None):
if not sysv_args:
sysv_args = sys.argv
- parser = argparse.ArgumentParser(prog=sysv_args[0])
- sysv_args = sysv_args[1:]
+ parser = argparse.ArgumentParser(prog=sysv_args.pop(0))
# Top level args
parser.add_argument(
@@ -956,7 +955,9 @@ def main(sysv_args=None):
"analyze", help="Devel tool: Analyze cloud-init logs and data"
)
- parser_devel = subparsers.add_parser("devel", help="Run development tools")
+ parser_devel = subparsers.add_parser(
+ "devel", help="Run development tools."
+ )
parser_collect_logs = subparsers.add_parser(
"collect-logs", help="Collect and tar all cloud-init debug info"
@@ -970,19 +971,24 @@ def main(sysv_args=None):
"status", help="Report cloud-init status or wait on completion."
)
+ parser_schema = subparsers.add_parser(
+ "schema", help="Validate cloud-config files using jsonschema."
+ )
+
if sysv_args:
# Only load subparsers if subcommand is specified to avoid load cost
- if sysv_args[0] == "analyze":
+ subcommand = sysv_args[0]
+ if subcommand == "analyze":
from cloudinit.analyze.__main__ import get_parser as analyze_parser
# Construct analyze subcommand parser
analyze_parser(parser_analyze)
- elif sysv_args[0] == "devel":
+ elif subcommand == "devel":
from cloudinit.cmd.devel.parser import get_parser as devel_parser
# Construct devel subcommand parser
devel_parser(parser_devel)
- elif sysv_args[0] == "collect-logs":
+ elif subcommand == "collect-logs":
from cloudinit.cmd.devel.logs import (
get_parser as logs_parser,
handle_collect_logs_args,
@@ -992,7 +998,7 @@ def main(sysv_args=None):
parser_collect_logs.set_defaults(
action=("collect-logs", handle_collect_logs_args)
)
- elif sysv_args[0] == "clean":
+ elif subcommand == "clean":
from cloudinit.cmd.clean import (
get_parser as clean_parser,
handle_clean_args,
@@ -1000,7 +1006,7 @@ def main(sysv_args=None):
clean_parser(parser_clean)
parser_clean.set_defaults(action=("clean", handle_clean_args))
- elif sysv_args[0] == "query":
+ elif subcommand == "query":
from cloudinit.cmd.query import (
get_parser as query_parser,
handle_args as handle_query_args,
@@ -1008,7 +1014,15 @@ def main(sysv_args=None):
query_parser(parser_query)
parser_query.set_defaults(action=("render", handle_query_args))
- elif sysv_args[0] == "status":
+ elif subcommand == "schema":
+ from cloudinit.config.schema import (
+ get_parser as schema_parser,
+ handle_schema_args,
+ )
+
+ schema_parser(parser_schema)
+ parser_schema.set_defaults(action=("schema", handle_schema_args))
+ elif subcommand == "status":
from cloudinit.cmd.status import (
get_parser as status_parser,
handle_status_args,
diff --git a/doc/man/cloud-init.1 b/doc/man/cloud-init.1
index 2cb63135..1da4335b 100644
--- a/doc/man/cloud-init.1
+++ b/doc/man/cloud-init.1
@@ -79,6 +79,10 @@ Activates modules using a given configuration key.
Query standardized instance metadata from the command line.
.TP
+.B "schema"
+Validate cloud-config files using jsonschema.
+
+.TP
.B "single"
Run a single module.
diff --git a/doc/rtd/topics/cli.rst b/doc/rtd/topics/cli.rst
index e90706fc..4a26cb45 100644
--- a/doc/rtd/topics/cli.rst
+++ b/doc/rtd/topics/cli.rst
@@ -36,6 +36,7 @@ option. This can be used against cloud-init itself or any of its subcommands.
collect-logs Collect and tar all cloud-init debug info
clean Remove logs and artifacts so cloud-init can re-run.
status Report cloud-init status or wait on completion.
+ schema Validate cloud-config files using jsonschema.
The rest of this document will give an overview of each of the subcommands.
@@ -114,11 +115,6 @@ Current subcommands:
from ``/run/cloud-init/instance-data.json``. It accepts a user-data file
containing the jinja template header ``## template: jinja`` and renders
that content with any instance-data.json variables present.
- * ``schema``: a **#cloud-config** format and schema
- validator. It accepts a cloud-config YAML file and annotates potential
- schema errors locally without the need for deployment. Schema
- validation is work in progress and supports a subset of cloud-config
- modules.
* ``hotplug-hook``: respond to newly added system devices by retrieving
updated system metadata and bringing up/down the corresponding device.
This command is intended to be called via a systemd service and is
@@ -249,6 +245,29 @@ This data can then be formatted to generate custom strings or data:
custom-i-0e91f69987f37ec74.us-east-2.aws.com
+.. _cli_schema:
+
+schema
+======
+
+Validate cloud-config files using jsonschema.
+
+* ``-h, --help``: show this help message and exit
+* ``-c CONFIG_FILE, --config-file CONFIG_FILE``: Path of the cloud-config yaml
+ file to validate
+* ``--system``: Validate the system cloud-config userdata
+* ``-d DOCS [DOCS ...], --docs DOCS [DOCS ...]``: Print schema module docs.
+ Choices: all or space-delimited cc_names.
+* ``--annotate``: Annotate existing cloud-config file with errors
+
+The following example checks a config file and annotates the config file with
+errors on stdout.
+
+.. code-block:: shell-session
+
+ $ cloud-init schema -c ./config.yml --annotate
+
+
.. _cli_single:
single
diff --git a/doc/rtd/topics/faq.rst b/doc/rtd/topics/faq.rst
index 59138c1d..e5784b71 100644
--- a/doc/rtd/topics/faq.rst
+++ b/doc/rtd/topics/faq.rst
@@ -146,7 +146,7 @@ provided to the system:
.. code-block:: shell-session
- $ cloud-init devel schema --system --annotate
+ $ cloud-init schema --system --annotate
As launching instances in the cloud can cost money and take a bit longer,
sometimes it is easier to launch instances locally using Multipass or LXD:
diff --git a/doc/rtd/topics/tutorial.rst b/doc/rtd/topics/tutorial.rst
index ad04bbc5..07b8fe5d 100644
--- a/doc/rtd/topics/tutorial.rst
+++ b/doc/rtd/topics/tutorial.rst
@@ -95,7 +95,7 @@ We can also assert the user data we provided is a valid cloud-config:
.. code-block:: shell-session
- $ cloud-init devel schema --system --annotate
+ $ cloud-init schema --system --annotate
Valid cloud-config: system userdata
$
diff --git a/tests/integration_tests/modules/test_cli.py b/tests/integration_tests/modules/test_cli.py
index baaa7567..e878176f 100644
--- a/tests/integration_tests/modules/test_cli.py
+++ b/tests/integration_tests/modules/test_cli.py
@@ -28,11 +28,11 @@ apt_pipelining: bogus
@pytest.mark.user_data(VALID_USER_DATA)
def test_valid_userdata(client: IntegrationInstance):
- """Test `cloud-init devel schema` with valid userdata.
+ """Test `cloud-init schema` with valid userdata.
PR #575
"""
- result = client.execute("cloud-init devel schema --system")
+ result = client.execute("cloud-init schema --system")
assert result.ok
assert "Valid cloud-config: system userdata" == result.stdout.strip()
result = client.execute("cloud-init status --long")
@@ -44,11 +44,11 @@ def test_valid_userdata(client: IntegrationInstance):
@pytest.mark.user_data(INVALID_USER_DATA_HEADER)
def test_invalid_userdata(client: IntegrationInstance):
- """Test `cloud-init devel schema` with invalid userdata.
+ """Test `cloud-init schema` with invalid userdata.
PR #575
"""
- result = client.execute("cloud-init devel schema --system")
+ result = client.execute("cloud-init schema --system")
assert not result.ok
assert "Cloud config schema errors" in result.stderr
assert 'needs to begin with "#cloud-config"' in result.stderr
diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py
index d2de9c87..7846d0d3 100644
--- a/tests/unittests/test_cli.py
+++ b/tests/unittests/test_cli.py
@@ -134,6 +134,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
"init",
"modules",
"single",
+ "schema",
]
for subcommand in expected_subcommands:
self.assertIn(subcommand, error)
@@ -169,6 +170,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
"usage: cloud-init collect-logs",
"usage: cloud-init devel",
"usage: cloud-init status",
+ "usage: cloud-init schema",
]
conditional_subcommands = [
"analyze",
@@ -176,6 +178,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
"collect-logs",
"devel",
"status",
+ "schema",
]
# The cloud-init entrypoint calls main without passing sys_argv
for subcommand in conditional_subcommands:
@@ -220,18 +223,18 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
self._call_main(["cloud-init", "status", "-h"])
self.assertIn("usage: cloud-init status", stdout.getvalue())
- def test_devel_subcommand_parser(self):
- """The subcommand cloud-init devel calls the correct subparser."""
- self._call_main(["cloud-init", "devel"])
+ def test_subcommand_parser(self):
+ """The subcommand cloud-init schema calls the correct subparser."""
+ self._call_main(["cloud-init"])
# These subcommands only valid for cloud-init schema script
expected_subcommands = ["schema"]
error = self.stderr.getvalue()
for subcommand in expected_subcommands:
self.assertIn(subcommand, error)
- def test_wb_devel_schema_subcommand_parser(self):
+ def test_wb_schema_subcommand_parser(self):
"""The subcommand cloud-init schema calls the correct subparser."""
- exit_code = self._call_main(["cloud-init", "devel", "schema"])
+ exit_code = self._call_main(["cloud-init", "schema"])
self.assertEqual(1, exit_code)
# Known whitebox output from schema subcommand
self.assertEqual(
@@ -240,7 +243,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
self.stderr.getvalue(),
)
- def test_wb_devel_schema_subcommand_doc_all_spot_check(self):
+ def test_wb_schema_subcommand_doc_all_spot_check(self):
"""Validate that doc content has correct values from known examples.
Ensure that schema doc is returned
@@ -252,7 +255,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
# manager
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
- self._call_main(["cloud-init", "devel", "schema", "--docs", "all"])
+ self._call_main(["cloud-init", "schema", "--docs", "all"])
expected_doc_sections = [
"**Supported distros:** all",
"**Supported distros:** almalinux, alpine, centos, "
@@ -267,7 +270,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
for expected in expected_doc_sections:
self.assertIn(expected, stdout)
- def test_wb_devel_schema_subcommand_single_spot_check(self):
+ def test_wb_schema_subcommand_single_spot_check(self):
"""Validate that doc content has correct values from known example.
Validate 'all' arg
@@ -279,9 +282,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
# manager
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
- self._call_main(
- ["cloud-init", "devel", "schema", "--docs", "cc_runcmd"]
- )
+ self._call_main(["cloud-init", "schema", "--docs", "cc_runcmd"])
expected_doc_sections = [
"Runcmd\n------\n**Summary:** Run arbitrary commands"
]
@@ -289,7 +290,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
for expected in expected_doc_sections:
self.assertIn(expected, stdout)
- def test_wb_devel_schema_subcommand_multiple_spot_check(self):
+ def test_wb_schema_subcommand_multiple_spot_check(self):
"""Validate that doc content has correct values from known example.
Validate single arg
@@ -300,7 +301,6 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
self._call_main(
[
"cloud-init",
- "devel",
"schema",
"--docs",
"cc_runcmd",
@@ -315,7 +315,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
for expected in expected_doc_sections:
self.assertIn(expected, stdout)
- def test_wb_devel_schema_subcommand_bad_arg_fails(self):
+ def test_wb_schema_subcommand_bad_arg_fails(self):
"""Validate that doc content has correct values from known example.
Validate multiple args
@@ -328,7 +328,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
stderr = io.StringIO()
with contextlib.redirect_stderr(stderr):
self._call_main(
- ["cloud-init", "devel", "schema", "--docs", "garbage_value"]
+ ["cloud-init", "schema", "--docs", "garbage_value"]
)
expected_doc_sections = ["Invalid --docs value"]
stderr = stderr.getvalue()