Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/_ext/validating_code_block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import List
from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.parsers.rst import directives
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective
from sphinx.directives.code import CodeBlock
from sphinx.errors import ExtensionError

import subprocess

class ValidatingCodeBlock(CodeBlock):

Comment thread
dmitri-d marked this conversation as resolved.
has_content = True
required_arguments = CodeBlock.required_arguments
optional_arguments = CodeBlock.optional_arguments
final_argument_whitespace = CodeBlock.final_argument_whitespace
option_spec = {
'type-name': directives.unchanged,
}
option_spec.update(CodeBlock.option_spec)

def run(self):
source, line = self.state_machine.get_source_and_line(self.lineno)
# built-in directives.unchanged_required option validator produces a confusing error message
if (self.options.get('type-name') == None):
Comment thread
dmitri-d marked this conversation as resolved.
Outdated
raise ExtensionError("Expected type name in: {0} line: {1}".format(source, line))

process = subprocess.Popen(['bazel', 'run', '//tools/config_validation:validate_fragment', '--', self.options.get('type-name'), "-s", "\n".join(self.content)],
Comment thread
dmitri-d marked this conversation as resolved.
Outdated
Comment thread
dmitri-d marked this conversation as resolved.
Outdated
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = process.communicate()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really nice and clean. You mention on Slack that this is pretty slow (around 10s), which won't scale. You mentioned a "customer builder", curious to learn more. If you want to continue with your existing PR, I'd suggest that you build a .par with all dependencies in docs/build.sh before invoking Sphinx. Then you can just reference the path of this .par here. That will allow you to skip all the Bazel overhead on each YAML processing.

That might still end up being too slow, e.g. if it takes ~1s and you have 100, that's over a minute and a half. When I would do then is add an env var to control validation. In CI we would always enable, but for local iterations on docs builds, we could disable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned a "customer builder", curious to learn more

My thinking is that we'd use a dedicated pass to verify example configs by invoking a dedicated builder. All it would do is to validate configs and generate a report. When a "normal" builder is used, examples would still be rendered, but without validation.

Basically we'd be doing config validation in one batch and it would be explicitly invoked. Current implementation combines doc generation and validation, and I'm not sure I can/should continue on validation errors: I think putting a global state into a directive (which is what ValidatingCodeBlock is) is a way to go.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to failing hard on validation errors.

How about this for an idea..

  1. We have a Sphinx plugin that just writes out the (YAML fragment, type) tuples to some directory and build all the docs.
  2. We then run them all through the config_validator in a single bazel run at the end?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what you are suggesting is quite close to a dedicated builder, it would qork quite similar to what you have described. This is the way to go If we want to validate all config examples and then report the ones that failed (as opposed to stopping at the first failure, like it's currently implemented).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think the other advantage is pure speed. Right now, you are invoking Bazel and Python multiple times to be able to do the validation. I'm guessing only a small fraction of CPU cycles are spent actually in the validation (could be worth measuring).

if (process.poll()):
raise ExtensionError("Failed config validation for type: '{0}' in: {1} line: {2}".format(self.options.get('type-name'), source, line))

self.options.pop('type-name', None)
return list(CodeBlock.run(self))

def setup(app):
app.add_directive("validated-code-block", ValidatingCodeBlock)

return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
2 changes: 1 addition & 1 deletion docs/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,6 @@ cp -f "${CONFIGS_DIR}"/google-vrp/envoy-edge.yaml "${GENERATED_RST_DIR}"/configu

rsync -rav $API_DIR/diagrams "${GENERATED_RST_DIR}/api-docs"

rsync -av "${SCRIPT_DIR}"/root/ "${SCRIPT_DIR}"/conf.py "${GENERATED_RST_DIR}"
rsync -av "${SCRIPT_DIR}"/root/ "${SCRIPT_DIR}"/conf.py "${SCRIPT_DIR}"/_ext "${GENERATED_RST_DIR}"

sphinx-build -W --keep-going -b html "${GENERATED_RST_DIR}" "${DOCS_OUTPUT_DIR}"
5 changes: 4 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ def setup(app):
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinxcontrib.httpdomain', 'sphinx.ext.extlinks', 'sphinx.ext.ifconfig']

sys.path.append(os.path.abspath("./_ext"))

extensions = ['sphinxcontrib.httpdomain', 'sphinx.ext.extlinks', 'sphinx.ext.ifconfig', 'validating_code_block']
extlinks = {
'repo': ('https://github.com/envoyproxy/envoy/blob/{}/%s'.format(blob_sha), ''),
'api': ('https://github.com/envoyproxy/envoy/blob/{}/api/%s'.format(blob_sha), ''),
Expand Down
3 changes: 2 additions & 1 deletion docs/root/configuration/operations/runtime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ runtime <envoy_v3_api_msg_config.bootstrap.v3.LayeredRuntime>` bootstrap configu
layering. Runtime settings in later layers override earlier layers. A typical configuration might
be:

.. code-block:: yaml
.. validated-code-block:: yaml
:type-name: envoy.config.bootstrap.v3.LayeredRuntime

layers:
- name: static_layer_0
Expand Down
7 changes: 4 additions & 3 deletions tools/config_validation/validate_fragment.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def ValidateFragment(type_name, fragment):


if __name__ == '__main__':
type_name, yaml_path = sys.argv[1:]
ValidateFragment(type_name, yaml.load(pathlib.Path(yaml_path).read_text(),
Loader=yaml.FullLoader))
type_name = sys.argv[1]
yaml_path = sys.argv[2]
content = sys.argv[3] if (yaml_path == "-s") else pathlib.Path(yaml_path).read_text()
Comment thread
dmitri-d marked this conversation as resolved.
Outdated
ValidateFragment(type_name, yaml.load(content, Loader=yaml.FullLoader))