Skip to content

Commit

Permalink
Merge pull request #101 from projectsyn/feat/ignore_class_notfound_re…
Browse files Browse the repository at this point in the history
…gexp

Add support for reclass option `ignore_class_notfound_regexp`
  • Loading branch information
simu authored Feb 23, 2024
2 parents cbedceb + d2d8ba4 commit ad977e3
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ indexmap = "2.2.3"
nom = "7.1.3"
pyo3 = { version = "0.20.2", features = ["chrono"] }
rayon = "1.8.1"
regex = "1.10.3"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
serde_yaml = "0.9.32"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The implementation currently supports the following features of Kapicorp Reclass

* The Reclass options `nodes_path` and `classes_path`
* The Reclass option `ignore_class_notfound`
* The Reclass option `ignore_class_notfound_regexp`
* Escaped parameter references
* Merging referenced lists and dictionaries
* Constant parameters
Expand All @@ -27,7 +28,6 @@ The following Kapicorp Reclass features aren't supported:

* Ignoring overwritten missing references
* Inventory Queries
* The Reclass option `ignore_class_notfound_regexp`
* The Reclass option `allow_none_override` can't be set to `False`
* The Reclass `yaml_git` and `mixed` storage types
* Any Reclass option which is not mentioned explicitly here or above
Expand Down
58 changes: 58 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::{anyhow, Result};
use pyo3::prelude::*;
use regex::RegexSet;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
Expand Down Expand Up @@ -63,6 +64,9 @@ pub struct Config {
pub compose_node_name: bool,
/// Python Reclass compatibility flags. See `CompatFlag` for available flags.
#[pyo3(get)]
ignore_class_notfound_regexp: Vec<String>,
ignore_class_notfound_regexset: RegexSet,
#[pyo3(get)]
pub compatflags: HashSet<CompatFlag>,
}

Expand Down Expand Up @@ -114,6 +118,8 @@ impl Config {
classes_path: to_lexical_normal(&cpath, true).display().to_string(),
ignore_class_notfound: ignore_class_notfound.unwrap_or(false),
compose_node_name: false,
ignore_class_notfound_regexp: vec![".*".to_string()],
ignore_class_notfound_regexset: RegexSet::new([".*"])?,
compatflags: HashSet::new(),
})
}
Expand Down Expand Up @@ -158,6 +164,22 @@ impl Config {
"Expected value of config key 'ignore_class_notfound' to be a boolean"
))?;
}
"ignore_class_notfound_regexp" => {
let list = v.as_sequence().ok_or(anyhow!(
"Expected value of config key 'ignore_class_notfound_regexp' to be a list"
))?;
self.ignore_class_notfound_regexp.clear();
for val in list {
self.ignore_class_notfound_regexp.push(
val.as_str()
.ok_or(anyhow!(
"Expected entry of 'ignore_class_notfound_regexp' to be a string"
))?
.to_string(),
);
}
self.ignore_class_notfound_regexp.shrink_to_fit();
}
"compose_node_name" => {
self.compose_node_name = v.as_bool().ok_or(anyhow!(
"Expected value of config key 'compose_node_name' to be a boolean"
Expand Down Expand Up @@ -185,6 +207,29 @@ impl Config {
}
}
}
self.compile_ignore_class_notfound_patterns()?;
Ok(())
}

/// Returns the currently configured `ignore_class_notfound_regexp` pattern list.
pub fn get_ignore_class_notfound_regexp(&self) -> &Vec<String> {
&self.ignore_class_notfound_regexp
}

/// Updates the saved ignore_class_notfound_regexp pattern list with the provided list and
/// ensures that the precompiled RegexSet is updated to match the new pattern list.
pub fn set_ignore_class_notfound_regexp(&mut self, patterns: Vec<String>) -> Result<()> {
self.ignore_class_notfound_regexp = patterns;
self.compile_ignore_class_notfound_patterns()
}

pub(crate) fn is_class_ignored(&self, cls: &str) -> bool {
self.ignore_class_notfound && self.ignore_class_notfound_regexset.is_match(cls)
}

fn compile_ignore_class_notfound_patterns(&mut self) -> Result<()> {
self.ignore_class_notfound_regexset = RegexSet::new(&self.ignore_class_notfound_regexp)
.map_err(|e| anyhow!("while compiling ignore_class_notfound regex patterns: {e}"))?;
Ok(())
}

Expand Down Expand Up @@ -286,4 +331,17 @@ mod tests {
assert_eq!(cfg.classes_path, "./inventory/classes");
assert_eq!(cfg.ignore_class_notfound, false);
}

#[test]
fn test_config_update_ignore_class_notfound_patterns() {
let mut cfg = Config::new(Some("./inventory"), None, None, None).unwrap();
assert_eq!(cfg.ignore_class_notfound_regexp, vec![".*"]);

cfg.set_ignore_class_notfound_regexp(vec![".*foo".into(), "bar.*".into()])
.unwrap();

assert!(cfg.ignore_class_notfound_regexset.is_match("thefooer"));
assert!(cfg.ignore_class_notfound_regexset.is_match("baring"));
assert!(!cfg.ignore_class_notfound_regexset.is_match("bazzer"));
}
}
12 changes: 12 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,18 @@ impl Reclass {
.collect::<HashMap<String, PathBuf>>();
Ok(res)
}

/// Update the current Reclass instance's config object with the provided
/// `ignore_class_notfound_regexp` patterns
pub fn set_ignore_class_notfound_regexp(&mut self, patterns: Vec<String>) -> PyResult<()> {
self.config
.set_ignore_class_notfound_regexp(patterns)
.map_err(|e| {
PyValueError::new_err(format!(
"Error while setting ignore_class_notfound_regexp: {e}"
))
})
}
}

impl Default for Reclass {
Expand Down
21 changes: 20 additions & 1 deletion src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,26 @@ impl Node {

// Lookup path for provided class in r.classes, handling ignore_class_notfound
let Some(classinfo) = r.classes.get(&cls) else {
if r.config.ignore_class_notfound {
// ignore_class_notfound_regexp is only applied if ignore_class_notfound == true.
// By default the regexset has a single pattern for .* so that all missing classes are
// ignored.
if r.config.is_class_ignored(&cls) {
return Ok(None);
}

if r.config.ignore_class_notfound {
// return an error informing the user that we didn't ignore the missing class
// based on the configured regex patterns.
eprintln!(
"Missing class '{cls}' not ignored due to configured regex patterns: [{}]",
r.config
.get_ignore_class_notfound_regexp()
.iter()
.map(|s| format!("'{s}'"))
.collect::<Vec<_>>()
.join(", ")
);
}
return Err(anyhow!("Class {cls} not found"));
};

Expand Down Expand Up @@ -545,3 +562,5 @@ mod node_tests {

#[cfg(test)]
mod node_render_tests;
#[cfg(test)]
mod node_render_tests_ignore_class_notfound_regexp;
41 changes: 41 additions & 0 deletions src/node/node_render_tests_ignore_class_notfound_regexp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use crate::types::Value;
use crate::{Config, Reclass};

#[test]
fn test_render_n1() {
let mut c = Config::new(
Some("./tests/inventory-class-notfound-regexp"),
None,
None,
None,
)
.unwrap();
c.load_from_file("reclass-config.yml").unwrap();
let r = Reclass::new_from_config(c).unwrap();

let n1 = r.render_node("n1").unwrap();
assert_eq!(
n1.classes,
vec!["service.foo", "service.bar", "missing", "a", "amissing"]
);
assert_eq!(
n1.parameters.get(&"a".into()),
Some(&Value::Literal("a".into()))
);
}

#[test]
fn test_render_n2() {
let mut c = Config::new(
Some("./tests/inventory-class-notfound-regexp"),
None,
None,
None,
)
.unwrap();
c.load_from_file("reclass-config.yml").unwrap();
let r = Reclass::new_from_config(c).unwrap();

let n2 = r.render_node("n2");
assert!(n2.is_err());
}
2 changes: 2 additions & 0 deletions tests/inventory-class-notfound-regexp/classes/a.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
a: a
2 changes: 2 additions & 0 deletions tests/inventory-class-notfound-regexp/classes/b.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
b: b
2 changes: 2 additions & 0 deletions tests/inventory-class-notfound-regexp/classes/c.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
c: c
2 changes: 2 additions & 0 deletions tests/inventory-class-notfound-regexp/classes/d.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
d: d
8 changes: 8 additions & 0 deletions tests/inventory-class-notfound-regexp/nodes/n1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
classes:
- service.foo
- service.bar
- missing
- a
- amissing

parameters: {}
2 changes: 2 additions & 0 deletions tests/inventory-class-notfound-regexp/nodes/n2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
classes:
- foo
9 changes: 9 additions & 0 deletions tests/inventory-class-notfound-regexp/reclass-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Add reclass-config.yml for Kapitan/Python reclass
nodes_uri: nodes
classes_uri: classes
compose_node_name: false
allow_none_override: true
ignore_class_notfound: true
ignore_class_notfound_regexp:
- service\..*
- .*missing.*
37 changes: 37 additions & 0 deletions tests/test_ignore_class_notfound_regexp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pytest

import reclass_rs


def test_ignore_regexp_render_n1():
r = reclass_rs.Reclass.from_config(
"./tests/inventory-class-notfound-regexp", "reclass-config.yml"
)
assert r.config.ignore_class_notfound_regexp == ["service\\..*", ".*missing.*"]

n1 = r.nodeinfo("n1")

assert n1 is not None


def test_ignore_regexp_render_n2():
r = reclass_rs.Reclass.from_config(
"./tests/inventory-class-notfound-regexp", "reclass-config.yml"
)
assert r.config.ignore_class_notfound_regexp == ["service\\..*", ".*missing.*"]

with pytest.raises(
ValueError, match="Error while rendering n2: Class foo not found"
):
n2 = r.nodeinfo("n2")


def test_ignore_regexp_update_config_render_n2():
r = reclass_rs.Reclass.from_config(
"./tests/inventory-class-notfound-regexp", "reclass-config.yml"
)
r.set_ignore_class_notfound_regexp([".*"])
assert r.config.ignore_class_notfound_regexp == [".*"]

n2 = r.nodeinfo("n2")
assert n2 is not None

0 comments on commit ad977e3

Please sign in to comment.