Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add expect enum validation #19

Merged
merged 6 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tlint"
version = "0.9.2"
version = "0.9.3"
edition = "2021"

[dependencies]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Validation engine for Trento Checks DSL.
## Usage
```sh
$ tlint -h
tlint 0.9.2
tlint 0.9.3

USAGE:
tlint <SUBCOMMAND>
Expand Down
2 changes: 2 additions & 0 deletions src/dsl/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ pub struct Expectation {
pub name: String,
pub expect: Option<String>,
pub expect_same: Option<String>,
pub expect_enum: Option<String>,
pub failure_message: Option<String>,
pub warning_message: Option<String>,
}

#[derive(Debug)]
Expand Down
297 changes: 294 additions & 3 deletions src/dsl/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,22 @@ pub fn validate(
.flat_map(|(index, value)| {
let expect = value.get("expect");
let expect_same = value.get("expect_same");
let expect_enum = value.get("expect_enum");
let failure_message = value.get("failure_message");
let warning_message = value.get("warning_message");

let is_expect = expect.is_some();
let is_expect_same = expect_same.is_some();
let is_expect_enum = expect_enum.is_some();

let expectation_expression = if is_expect {
expect.unwrap().as_str().unwrap()
expect.unwrap().as_str().unwrap()
} else if is_expect_same {
expect_same.unwrap().as_str().unwrap()
} else if is_expect_enum {
expect_enum.unwrap().as_str().unwrap()
} else {
expect_same.unwrap().as_str().unwrap()
""
};

let mut results = vec![];
Expand All @@ -75,7 +83,32 @@ pub fn validate(
index,
is_expect,
));
};
}

if warning_message.is_some() && !is_expect_enum {
results.push(Err(ValidationError {
check_id: check_id.to_string(),
error: "warning_message is only available for expect_enum expectations".to_string(),
instance_path: format!("/expectations/{:?}", index).to_string(),
}));
} else if warning_message.is_some() {
let warning_message_expression = warning_message.unwrap().as_str().unwrap();
results.push(validate_string_expression(
warning_message_expression,
engine,
check_id,
index,
is_expect_enum,
));
}

if is_expect_enum {
results.append(&mut validate_expect_enum_content(
expectation_expression,
check_id,
index,
));
}

results
})
Expand Down Expand Up @@ -195,6 +228,32 @@ fn validate_string_expression(
}
}

fn validate_expect_enum_content(
expression: &str,
check_id: &str,
index: usize,
) -> Vec<Result<(), ValidationError>> {
let mut results = vec![];

if !expression.contains("\"passing\"") {
results.push(Err(ValidationError {
check_id: check_id.to_string(),
error: "passing return value not found".to_string(),
instance_path: format!("/expectations/{:?}", index).to_string(),
}));
}

if !expression.contains("\"warning\"") {
results.push(Err(ValidationError {
check_id: check_id.to_string(),
error: "warning return value not found. Consider using `expect` expression if a warning return is not needed".to_string(),
instance_path: format!("/expectations/{:?}", index).to_string(),
}));
}

results
}

pub fn get_json_schema() -> JSONSchema {
let value = serde_json::from_str(SCHEMA).unwrap();

Expand Down Expand Up @@ -480,6 +539,55 @@ mod tests {
assert!(deserialization_result.is_ok());
}

#[test]
fn validate_check_expect_enum() {
let input = r#"
id: 156F64
name: Corosync configuration file
group: Corosync
description: |
Corosync `token` timeout is set to expected value
remediation: |
## Abstract
The value of the Corosync `token` timeout is not set as recommended.
## Remediation
...
facts:
- name: corosync_token_timeout
gatherer: corosync.conf
values:
- name: expected_passing_value
default: 5000
- name: expected_warning_value
default: 3000
expectations:
- name: timeout
expect_enum: |
if facts.corosync_token_timeout == values.expected_passing_value {
"passing"
} else if facts.corosync_token_timeout == values.expected_warning_value {
"warning"
} else {
"critical"
}
failure_message: some critical message
warning_message: some warning message
"#;

let engine = Engine::new();

let json_value: serde_json::Value =
serde_yaml::from_str(input).expect("Unable to parse yaml");

let deserialization_result = serde_yaml::from_str::<Check>(input);

let json_schema = get_json_schema();
let validation_result = validate(&json_value, "156F64", &json_schema, &engine);

assert!(validation_result.is_ok());
assert!(deserialization_result.is_ok());
}

#[test]
fn validate_check_failure_message_expect_ok() {
let input = r#"
Expand Down Expand Up @@ -669,4 +777,187 @@ mod tests {
);
assert_eq!(validation_errors[0].instance_path, "/metadata");
}

#[test]
fn validate_expression_missing() {
let input = r#"
id: 156F64
name: Corosync configuration file
group: Corosync
description: |
Corosync `token` timeout is set to expected value
remediation: |
## Abstract
The value of the Corosync `token` timeout is not set as recommended.
## Remediation
...
facts:
- name: corosync_token_timeout
gatherer: corosync.conf
argument: totem.token
values:
- name: expected_token_timeout
default: 5000

expectations:
- name: timeout
failure_message: critical!
"#;

let engine = Engine::new();

let json_value: serde_json::Value =
serde_yaml::from_str(input).expect("Unable to parse yaml");
let json_schema = get_json_schema();
let validation_errors = validate(&json_value, "156F64", &json_schema, &engine).unwrap_err();
assert_eq!(validation_errors[0].check_id, "156F64");
assert_eq!(
validation_errors[0].error,
"{\"failure_message\":\"critical!\",\"name\":\"timeout\"} is not valid under any of the given schemas"
);
assert_eq!(validation_errors[0].instance_path, "/expectations/0");
}

#[test]
fn validate_invalid_warning_message() {
let input = r#"
id: 156F64
name: Corosync configuration file
group: Corosync
description: |
Corosync `token` timeout is set to expected value
remediation: |
## Abstract
The value of the Corosync `token` timeout is not set as recommended.
## Remediation
...
facts:
- name: corosync_token_timeout
gatherer: corosync.conf
values:
- name: expected_passing_value
default: 5000
- name: expected_warning_value
default: 3000
expectations:
- name: timeout
expect_enum: |
if facts.corosync_token_timeout == values.expected_passing_value {
"passing"
} else if facts.corosync_token_timeout == values.expected_warning_value {
"warning"
} else {
"critical"
}
failure_message: some critical message
warning_message: some warning message with ${facts.corosync_token_timeout
"#;

let engine = Engine::new();

let json_value: serde_json::Value =
serde_yaml::from_str(input).expect("Unable to parse yaml");
let json_schema = get_json_schema();
let validation_errors = validate(&json_value, "156F64", &json_schema, &engine).unwrap_err();
assert_eq!(validation_errors[0].check_id, "156F64");
assert_eq!(
validation_errors[0].error,
"Open string is not terminated (line 1, position 58)"
);
assert_eq!(validation_errors[0].instance_path, "/expectations/0");
}

#[test]
fn validate_warning_message_only_expect_enum() {
let input = r#"
id: 156F64
name: Corosync configuration file
group: Corosync
description: |
Corosync `token` timeout is set to expected value
remediation: |
## Abstract
The value of the Corosync `token` timeout is not set as recommended.
## Remediation
...
facts:
- name: corosync_token_timeout
gatherer: corosync.conf
argument: totem.token
values:
- name: expected_token_timeout
default: 5000

expectations:
- name: timeout
expect: facts.corosync_token_timeout == values.expected_token_timeout
warning_message: some message
- name: timeout_same
expect_same: facts.corosync_token_timeout
warning_message: some message
"#;

let engine = Engine::new();

let json_value: serde_json::Value =
serde_yaml::from_str(input).expect("Unable to parse yaml");
let json_schema = get_json_schema();
let validation_errors = validate(&json_value, "156F64", &json_schema, &engine).unwrap_err();
assert_eq!(validation_errors[0].check_id, "156F64");
assert_eq!(
validation_errors[0].error,
"warning_message is only available for expect_enum expectations"
);
assert_eq!(validation_errors[0].instance_path, "/expectations/0");

assert_eq!(validation_errors[1].check_id, "156F64");
assert_eq!(
validation_errors[1].error,
"warning_message is only available for expect_enum expectations"
);
assert_eq!(validation_errors[1].instance_path, "/expectations/1");
}

#[test]
fn validate_invalid_expect_enum_without_returns() {
let input = r#"
id: 156F64
name: Corosync configuration file
group: Corosync
description: |
Corosync `token` timeout is set to expected value
remediation: |
## Abstract
The value of the Corosync `token` timeout is not set as recommended.
## Remediation
...
facts:
- name: corosync_token_timeout
gatherer: corosync.conf
values:
- name: expected_passing_value
default: 5000
expectations:
- name: timeout
expect_enum: facts.corosync_token_timeout == values.expected_passing_value
"#;

let engine = Engine::new();

let json_value: serde_json::Value =
serde_yaml::from_str(input).expect("Unable to parse yaml");
let json_schema = get_json_schema();
let validation_errors = validate(&json_value, "156F64", &json_schema, &engine).unwrap_err();
assert_eq!(validation_errors[0].check_id, "156F64");
assert_eq!(
validation_errors[0].error,
"passing return value not found"
);
assert_eq!(validation_errors[0].instance_path, "/expectations/0");
assert_eq!(
validation_errors[1].error,
"warning return value not found. Consider using `expect` expression if a warning return is not needed"
);
assert_eq!(validation_errors[1].instance_path, "/expectations/0");
}
}
2 changes: 1 addition & 1 deletion wanda
Submodule wanda updated 40 files
+100 −32 .github/workflows/ci.yaml
+2 −2 .tool-versions
+4 −8 Dockerfile
+10 −198 LICENSE
+1 −1 README.md
+1 −1 config/test.exs
+1 −1 demo/fake_gathered_facts.ex
+16 −2 guides/check_definition.schema.json
+78 −2 guides/specification.md
+4 −3 lib/wanda/catalog/expectation.ex
+107 −24 lib/wanda/executions/evaluation.ex
+2 −2 lib/wanda/executions/expectation_evaluation.ex
+2 −2 lib/wanda/executions/expectation_result.ex
+2 −1 lib/wanda/executions/server.ex
+1 −1 lib/wanda_web/schemas/catalog/check.ex
+1 −1 lib/wanda_web/schemas/execution/expectation_evaluation.ex
+1 −1 lib/wanda_web/schemas/execution/expectation_result.ex
+0 −53 lib/wanda_web/schemas/execution/start_execution_request.ex
+5 −4 mix.exs
+32 −41 mix.lock
+3 −1 packaging/suse/container/Dockerfile
+0 −0 packaging/suse/container/_constraints
+0 −0 packaging/suse/container/_service
+18 −0 packaging/suse/rpm/_service
+5 −0 packaging/suse/rpm/systemd/trento-wanda.example
+14 −0 packaging/suse/rpm/systemd/trento-wanda.service
+84 −0 packaging/suse/rpm/trento-wanda.spec
+6 −3 priv/catalog/0B6DB2.yaml
+3 −2 priv/catalog/222A57.yaml
+5 −5 priv/catalog/373DB8.yaml
+3 −3 priv/catalog/816815.yaml
+1 −3 priv/catalog/9FAAD0.yaml
+2 −2 priv/catalog/9FEFB0.yaml
+1 −4 priv/catalog/C3166E.yaml
+1 −1 priv/catalog/D028B9.yaml
+4 −3 priv/catalog/DC5429.yaml
+1 −2 priv/catalog/F50AF5.yaml
+1 −1 test/support/factory.ex
+311 −0 test/wanda/executions/evaluation_test.exs
+6 −0 test/wanda/executions/server_test.exs
Loading