Validate configuration file against JSON schema - #1050
Conversation
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
c054fd0 to
077675a
Compare
077675a to
01fb468
Compare
fb1fae0 to
504d274
Compare
504d274 to
f6a081a
Compare
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
All format and hook schemas have been rewritten from scratch to match their actual `parse` implementation and specify an exhaustive list of properties and include an `additionalProperties: false` directive. The single-use subschemas (e.g. `config/nodes/signals`) have been moved into subschema "definitions". Schemas are categorized by prefix instead of by directory. Bundling the schema using `redocly bundle` flattens `#/schemas/components` into a single object. This makes debugging harder when some schemas are renamed to avoid name collisions. |prefix |meaning | |----------|-------------------------------------| |`config-` |Global configuration file section | |`node-` |Node plugin instance configuration | |`hook-` |Hook plugin instance configuration | |`shared-` |Schemas referenced in multiple places| Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
f6a081a to
a1117fd
Compare
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
- Remove villas-conf2json and villas-test-config - Add villas-config tool for migration and validation Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
Signed-off-by: Philipp Jungkamp <philipp.jungkamp@rwth-aachen.de>
a1117fd to
437930e
Compare
|
I'm quite happy with this PR now. Tests are passing. Documentation is running. And the error messages are gorgeous. @stv0g Sorry for the gigantic diff on the schema rewrite. Enumerating every problem the old schemas had would have taken days to describe in git commits. I think just browsing the documentation in the linked documentation PR and trying this branch yourself is the best way to validate whether the rewrite is reasonable. Commit 4197cb5 is also on the larger side. I should have split it into "Schema Validation" + |
stv0g
left a comment
There was a problem hiding this comment.
Looks good to me overall.
I added some small comments. Please feel free to merge when addressed.
Thanks :)
There was a problem hiding this comment.
Please rename to bundle-schema.sh
| "dependencies": { | ||
| "@redocly/cli": "^2.25.0" | ||
| "@redocly/cli": "1.16.0" |
There was a problem hiding this comment.
Could you clarify why we are downgrading redocly here?
There was a problem hiding this comment.
It's the latest version of redoc that's supported by redocusaurus. I've had problems when I tried to get the documentation running with these changes because I had used features introduced in a more recent version of redocly. Pinning this version decreases the chances of documentation-breaking changes.
This really isn't too useful currently because the CI uses npx without a pinned version an the Nix devshell also just uses the most recent version...
I should probably drop it from this PR anyway, but I'll maybe add another PR later that fixes this version mismatch across our stack.
There was a problem hiding this comment.
Just wondering, why did you remove this test?
There was a problem hiding this comment.
I've merged its functionality into the test-config.sh scripts when I merged villas-conf2json and villas-test-config. The test-config.sh script detects both missing and invalid example configurations now.
| }; | ||
| in | ||
| rec { | ||
| default = gcc; | ||
|
|
||
| gcc = mkShellFor pkgs.stdenv pkgs.villas-node; | ||
| gcc = mkShellFor pkgs.gcc14Stdenv pkgs.villas-node; |
There was a problem hiding this comment.
Could you clarify why we need to build with GCC 14? Is this only due to the OpenDSS node-type?
There was a problem hiding this comment.
The lib/nodes/opendss.cpp node implementation includes the broken OpenDSS headers, so we need to build VILLASnode itself with gcc14 currently (see packaging/nix/villas.nix on the master branch). This change just ensures that we use the same stdenv for nix develop that we're already using for nix build.
|
I want to restate that this will break configurations that just happen to work despite setting unnecessary or mistyped options. Notifying users of these problems by terminating with a clear error is really important. One of our example configurations was mixing up I can't really judge how large the real breakage will be here. |
Description
This change adds in-process JSON schemas validation to villas-node. This enables us to catch many more invalid configurations and present more readable errors for these. The main goals I wanted to achieve here are:
Implementation
Adding a Validator
The first goal was quite easy to achieve:
We've previously added a new dependency on
nlohmann::jsonfor a more idiomatic and less error-prone JSON library written in C++. I had to choose between the two established JSON schema validator projects that supportnlohmann::jsonvalues: json-schema-validator and valijson. The choice here fell on json-schema-validator due to its support for patching default values into the configuration after validation.Reworking the Schema
This first step already applies the schema checks to all options in the configuration and thus make a lot of the checks and exception throwing in the C++ code unnecessary. But this wouldn't be able to catch unknown keys or typos. Both json-schema-validator and valijson only support JSON Schema Draft-07 schemas. The only way to achieve goal two - rejecting unknown keys and typos - in Draft-07 is to use an
additionalProperties: falseannotation, but that requires all properties of any object values to be declared in a single schema. This is fundamentally incompatible with the way our schemas are currently structured.This schema rework was partially LLM assisted in schema generation and validation against the C++ parsing code. I've rewritten every single schema file from scratch, copying descriptions and titles from the existing schemas where possible. The new schemas now live in a single flat directory following a new naming convention.
config-node-format-hook-shared-The schema bundling routine uses the
redoclyCLI tool, which will place all subschemas into a single flat JSON object at/components/schemas. The key of the schema in the flattened object is derived from the file's basename, not its directory which would have previously caused e.g. theconfig/nodes/stats.yamlandconfig/hooks/stats.yamlin the old directory layout to collide. These collisions aren't fatal,redoclywill just rename one of the schemas to prevent a collision, but this will cause the schema name to be unpredictable, which I deemed undesirable. Placing all schemas in a single directory with a proper naming convention prevents these collisions by design.Plugin Subschemas
This refactor achieved the second and third goal - rejecting unknown keys or typos and providing location information in the form of a json pointer. The problem that remains for goal four is that discriminated unions do not have a good representation in a JSON schema. There isn't a way to specify that some property within an object serves as a discriminator. The
discriminatorkeyword we have is only an OpenAPI annotation which isn't used for validation or error reporting. This means that if we have a typo in a property, the validator will try to do a structural match for all plugin types and report why each plugin would not pass validation, burying the actual plugin as indicated bytypein a sea of unrelated errors.I've solved this by validating the configuration in stages. The schema sitting in
doc/openapiis not completely valid by itself. The schema for plugin types likeNode,HookorFormatonly specify adiscriminatorwithout an adjacent logical combinator. That discriminator has ax-villas-pluginannotation that is expanded to a proper logical combinator for the documentation by thedoc/villas.jsredocly plugin. The schema bundled into the binary will does not expand thediscriminator, I can then use that annotation to discover subschemas myself and report proper, readable errors based on the actual plugin type.Migrations
The schema rework led me to discover a bunch of unintuitive, undesirable or straight up broken behavior in our parsing code. I've added migration code that detects some of these cases, where I've deemed that retaining the current behavior would lead to worse schemas, but the option is common enough to break the experience for many users. I'm using the structural schema traversal from the subschemas above to apply these migrations depending on the plugin type specified.
Note that this migration is ephemeral; it will run on every node startup and emit warnings for all migration operations that had to be done, but it won't write those changes back to disk. You'll have to check those migrations and apply them manually.
NodemigrationsI've removed six options from the
Nodebase-class:enabled: This option is completely unnecessary. ANodethat is not used by any path will be disabled by itsSuperNoderegardless of this configuration value. Setting this configuration value tofalsefor a node that is used by a path will just throw an exception on startup. There is literally no case where specifying this option makes a useful difference so I don't provide a migration here.initial_sequenceno: This option is also completely unnecessary, this functionality is better served by ashift_seqhook. I've only ever seen one use of this option in thenode-statsintegration test so I didn't provide an automatic migration here either.builtin/vectorize/signals/hooks: These options can be specified at the top-level of a node. TheNodeParsefunction then distributes these values into theinandoutsubobjects if they are present. This code was used in a few places, it allowed the user to e.g. specifyvectorizefor both theinandoutdirection of a node at the same time. The problem I have with this is that this code bloats our schemas, these four keys would have had to be added to everynode-*.yamlschema file, where I deemed the overhead to be larger than these options' usefulness. Another problem is that this code introduced subtle bugs. I've added automatic migrations for these to keep existing configurations working.I've also removed the object-with-count and string representations for signal lists because those needed a giant amount of work to properly support in the JSON schema and we didn't even bother to implement these any node specific schema in the past. These are also automatically migrated.
PathmigrationsI've made the
reverseoption into a migration, this option would only work on paths that didn't have any mapping expressions and I wanted to be sure that a path that validates against our schema would also work correctly. Encoding this logic into the schema is more complicated than just removing it, especially because it's seldomly used.Tooling
I've also worked on our tooling that supports our configuration files. We've previously had
villas-conf2json, which just read in a libconfig file and printed a JSON version of that file to stdout, andvillas-test-config, which tries to validate a configuration by parsing it without any starting any node.I've merged both tools into a single
villas-configtool that serves multiple purposes:I've needed all these features for our integration tests and found them to be also genuinely useful for interactive introspection and debugging of a configuration file.
You can also use this tool for your manual migrations. I still consider these to be manual because all comments will be dropped, which often information that useful for documentation purposes.