Skip to content

Commit 1186057

Browse files
committed
feat(validator): add validator crate
We need a new crate to be able to validate gw configurations offline, without starting dataplane. This patch adds a vanilla validator, as a rust binary. The intent is to compile this validator as wasm/wasi and use it externally. The validator expects a GatewayAgent CRD from stdin in JSON/YAML and produces a YAML-encoded response in stdout. If the response cannot be serialized, an error is written to stderr. On success, the validator outputs: success: true errors: [] In case the configuration is invalid, or cannot be validated for some reason (e.g. malformed json/yaml), a yaml-encoded string is output to stderr, with success = false and the errors populated. E.g. success: false errors: - type: Conversion message: 'Invalid Gateway Agent object: Could not create VPC: ''0'' is not a valid VNI' or success: false errors: - type: Deserialization message: 'spec.gateway.neighbors[1].asn: invalid value: integer `6500200000000`, expected u32 at line 68 column 16' Each error includes a type and a text message describing the problem. The possible error types mirror the sequence of steps the validator takes to evaluate a configuration and is one of: "Environment": for errors of the tool "Deserialization": for malformed inputs or invalid types "Metadata": for illegal or missing metadata fields "Conversion": for errors converting CRD to configuration "Configuration": for missing or semantically incorrect configs In general, only the last two types of errors may be due to an incorrect configuration from the user. E.g. success: false errors: - type: Configuration message: A VPC peering object refers to non-existent VPC 'VPC-1' Signed-off-by: Fredi Raspall <fredi@githedgehog.com>
1 parent baff8c7 commit 1186057

File tree

4 files changed

+233
-0
lines changed

4 files changed

+233
-0
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ members = [
3030
"sysfs",
3131
"test-utils",
3232
"tracectl",
33+
"validator",
3334
"vpcmap",
3435
]
3536

@@ -75,6 +76,7 @@ stats = { path = "./stats", package = "dataplane-stats", features = [] }
7576
sysfs = { path = "./sysfs", package = "dataplane-sysfs", features = [] }
7677
test-utils = { path = "./test-utils", package = "dataplane-test-utils", features = [] }
7778
tracectl = { path = "./tracectl", package = "dataplane-tracectl", features = [] }
79+
validator = { path = "./validator", package = "dataplane-validator", features = [] }
7880
vpcmap = { path = "./vpcmap", package = "dataplane-vpcmap", features = [] }
7981

8082
# External

validator/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "dataplane-validator"
3+
edition.workspace = true
4+
license.workspace = true
5+
publish.workspace = true
6+
version.workspace = true
7+
8+
[[bin]]
9+
name = "validator"
10+
path = "src/main.rs"
11+
12+
[dependencies]
13+
config = { workspace = true }
14+
k8s-intf = { workspace = true }
15+
16+
serde = { workspace = true }
17+
serde_yaml_ng = { workspace = true }

validator/src/main.rs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Open Network Fabric Authors
3+
4+
//! A configuration validator. This validator may perform the same validation that
5+
//! the dataplane process. The intent is to compile this validator as WASM / WASI.
6+
//! The validator expects a `GatewayAgent` CRD in JSON or YAML from stdin and produces
7+
//! a result as a YAML string in stdout.
8+
9+
#![deny(clippy::all)]
10+
#![allow(clippy::result_large_err)]
11+
#![allow(clippy::field_reassign_with_default)]
12+
13+
use config::{ExternalConfig, GwConfig};
14+
use k8s_intf::generated::gateway_agent_crd::GatewayAgent;
15+
use serde::{Deserialize, Serialize};
16+
use std::io::{self, Read};
17+
18+
#[derive(Default)]
19+
struct ConfigErrors {
20+
errors: Vec<String>, // only one error is supported at the moment
21+
}
22+
23+
/// The type representing an error when validating a request
24+
enum ValidateError {
25+
/// This type contains errors that may occur when using this tool.
26+
EnvironmentError(String),
27+
28+
/// This type contains errors that may occur when deserializing from JSON or YAML.
29+
/// If the inputs are machine-generated, these should not occur.
30+
DeserializeError(String),
31+
32+
/// This type contains errors that may occur if the metadata is incomplete or wrong.
33+
/// This should catch integration issues or problems in the K8s infrastructure.
34+
MetadataError(String),
35+
36+
/// This type contains errors that may occur when converting the CRD to a gateway configuration.
37+
/// These may happen mostly due to type violations, out-of-range values, etc.
38+
ConversionError(String),
39+
40+
/// This type contains configuration errors. If errors of this type are produced, this means
41+
/// that the configuration is syntactically correct and could be parsed, but it is:
42+
/// - incomplete or
43+
/// - contains values that are semantically incorrect as a whole or
44+
/// - contains values that are not allowed / supported
45+
///
46+
/// which would prevent the gateway from functioning correctly.
47+
/// Together with some conversion errors, these are errors the user is responsible for.
48+
Configuration(ConfigErrors),
49+
}
50+
impl ValidateError {
51+
/// Provide a string indicating the type of error
52+
fn get_type(&self) -> &str {
53+
match self {
54+
ValidateError::EnvironmentError(_) => "Environment",
55+
ValidateError::DeserializeError(_) => "Deserialization",
56+
ValidateError::MetadataError(_) => "Metadata",
57+
ValidateError::ConversionError(_) => "Conversion",
58+
ValidateError::Configuration(_) => "Configuration",
59+
}
60+
}
61+
62+
/// Provide a list of messages depending on the error type
63+
fn get_msg(&self) -> Vec<String> {
64+
match self {
65+
ValidateError::EnvironmentError(v) => vec![v.clone()],
66+
ValidateError::DeserializeError(v) => vec![v.clone()],
67+
ValidateError::MetadataError(v) => vec![v.clone()],
68+
ValidateError::ConversionError(v) => vec![v.clone()],
69+
ValidateError::Configuration(v) => v.errors.to_vec(),
70+
}
71+
}
72+
}
73+
74+
impl From<&ValidateError> for ValidateReply {
75+
fn from(value: &ValidateError) -> Self {
76+
let r#type = value.get_type();
77+
let msg = value.get_msg();
78+
79+
ValidateReply {
80+
success: false,
81+
errors: msg
82+
.iter()
83+
.map(|m| ValidateErrorOut {
84+
r#type: r#type.to_owned(),
85+
message: m.clone(),
86+
context: None,
87+
})
88+
.collect(),
89+
}
90+
}
91+
}
92+
93+
#[derive(Serialize, Deserialize)]
94+
struct ValidateErrorOut {
95+
r#type: String,
96+
message: String,
97+
#[serde(skip_serializing_if = "Option::is_none")]
98+
context: Option<String>,
99+
}
100+
101+
/// The type representing the outcome of a validation request
102+
#[derive(Serialize, Deserialize)]
103+
struct ValidateReply {
104+
success: bool,
105+
errors: Vec<ValidateErrorOut>,
106+
}
107+
impl ValidateReply {
108+
fn success() -> Self {
109+
Self {
110+
success: true,
111+
errors: vec![],
112+
}
113+
}
114+
}
115+
116+
/// Deserialize JSON/YAML string as a `GatewayAgent`
117+
fn deserialize(ga_input: &str) -> Result<GatewayAgent, ValidateError> {
118+
let crd = serde_yaml_ng::from_str::<GatewayAgent>(ga_input)
119+
.map_err(|e| ValidateError::DeserializeError(e.to_string()))?;
120+
Ok(crd)
121+
}
122+
123+
/// Validate metadata
124+
fn validate_metadata(crd: &GatewayAgent) -> Result<&str, ValidateError> {
125+
let genid = crd.metadata.generation.ok_or(ValidateError::MetadataError(
126+
"Missing generation Id".to_string(),
127+
))?;
128+
if genid == 0 {
129+
return Err(ValidateError::MetadataError(
130+
"Invalid generation Id".to_string(),
131+
));
132+
}
133+
let gwname = crd
134+
.metadata
135+
.name
136+
.as_ref()
137+
.ok_or(ValidateError::MetadataError(
138+
"Missing gateway name".to_string(),
139+
))?;
140+
if gwname.is_empty() {
141+
return Err(ValidateError::MetadataError(
142+
"Invalid gateway name".to_string(),
143+
));
144+
}
145+
let namespace = crd
146+
.metadata
147+
.namespace
148+
.as_ref()
149+
.ok_or(ValidateError::MetadataError(
150+
"Missing namespace".to_string(),
151+
))?;
152+
if namespace.as_str() != "fab" {
153+
return Err(ValidateError::MetadataError(format!(
154+
"Invalid namespace {namespace}"
155+
)));
156+
}
157+
158+
Ok(gwname.as_str())
159+
}
160+
161+
/// Main validation function
162+
fn validate(gwagent_json: &str) -> Result<(), ValidateError> {
163+
let crd = deserialize(gwagent_json)?;
164+
let gwname = validate_metadata(&crd)?;
165+
166+
let external = ExternalConfig::try_from(&crd)
167+
.map_err(|e| ValidateError::ConversionError(e.to_string()))?;
168+
169+
let mut gwconfig = GwConfig::new(gwname, external);
170+
gwconfig.validate().map_err(|e| {
171+
let mut config = ConfigErrors::default();
172+
config.errors.push(e.to_string());
173+
ValidateError::Configuration(config)
174+
})?;
175+
176+
Ok(())
177+
}
178+
179+
/// Read from stdin, deserialize as JSON and validate
180+
fn validate_from_stdin() -> Result<(), ValidateError> {
181+
let mut input = String::new();
182+
io::stdin()
183+
.read_to_string(&mut input)
184+
.map_err(|e| ValidateError::EnvironmentError(format!("Failed to read from stdin: {e}")))?;
185+
186+
validate(&input)
187+
}
188+
189+
/// Build a validation reply to be output as JSON
190+
fn build_reply(result: Result<(), ValidateError>) -> ValidateReply {
191+
match result {
192+
Ok(()) => ValidateReply::success(),
193+
Err(e) => ValidateReply::from(&e),
194+
}
195+
}
196+
197+
fn main() {
198+
let result = validate_from_stdin();
199+
let reply = build_reply(result);
200+
match serde_yaml_ng::to_string(&reply) {
201+
Ok(out) => println!("{out}"),
202+
Err(e) => eprintln!("Failure serializing validation response: {e}"),
203+
}
204+
}

0 commit comments

Comments
 (0)