public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH datacenter-manager 0/3] views: preparations for regex/glob, include-all param
@ 2025-11-17 14:11 Lukas Wagner
  2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 1/3] pdm-api-types: views: preparations for future glob/regex support Lukas Wagner
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Lukas Wagner @ 2025-11-17 14:11 UTC (permalink / raw)
  To: pdm-devel

As brought up in:

https://lore.proxmox.com/pdm-devel/20251113121644.236005-1-l.wagner@proxmox.com/T/#m1a2463844e8d833457135cbdd87bb0de707ffc08

Lukas Wagner (3):
  pdm-api-types: views: preparations for future glob/regex support
  views: add 'include-all' param; change semantics when there are no
    includes
  views: tests: use full section-config format for test cases

 lib/pdm-api-types/src/views.rs | 189 ++++++++++++------
 server/src/views/mod.rs        |  82 ++++----
 server/src/views/tests.rs      | 339 +++++++++++++++++++--------------
 3 files changed, 377 insertions(+), 233 deletions(-)

-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pdm-devel] [PATCH datacenter-manager 1/3] pdm-api-types: views: preparations for future glob/regex support
  2025-11-17 14:11 [pdm-devel] [PATCH datacenter-manager 0/3] views: preparations for regex/glob, include-all param Lukas Wagner
@ 2025-11-17 14:11 ` Lukas Wagner
  2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 2/3] views: add 'include-all' param; change semantics when there are no includes Lukas Wagner
  2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 3/3] views: tests: use full section-config format for test cases Lukas Wagner
  2 siblings, 0 replies; 4+ messages in thread
From: Lukas Wagner @ 2025-11-17 14:11 UTC (permalink / raw)
  To: pdm-devel

Change the config format slightly in a way similar to how the
'match-field' statements work for notification matchers.

The new format is, in pseudo-regex:

  (include|exclude) (exact:)?(resource-pool|...|tag)=.*

The 'exact:' part is optional. Later we can add "regex" or "glob" types
to add support for globbing or regex without any changes to the config
format.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 lib/pdm-api-types/src/views.rs | 185 +++++++++++++++++++++++----------
 server/src/views/mod.rs        |  24 +++--
 server/src/views/tests.rs      | 110 ++++++++++++--------
 3 files changed, 215 insertions(+), 104 deletions(-)

diff --git a/lib/pdm-api-types/src/views.rs b/lib/pdm-api-types/src/views.rs
index ef39cc62..950a3a04 100644
--- a/lib/pdm-api-types/src/views.rs
+++ b/lib/pdm-api-types/src/views.rs
@@ -1,6 +1,6 @@
-use std::{fmt::Display, str::FromStr, sync::OnceLock};
+use std::{fmt::Debug, fmt::Display, str::FromStr, sync::OnceLock};
 
-use anyhow::bail;
+use anyhow::{bail, Error};
 use const_format::concatcp;
 use serde::{Deserialize, Serialize};
 
@@ -23,11 +23,11 @@ const_regex! {
 pub const FILTER_RULE_SCHEMA: Schema = StringSchema::new("Filter rule for resources.")
     .format(&ApiStringFormat::VerifyFn(verify_filter_rule))
     .type_text(
-        "resource-type:<storage|qemu|lxc|sdn-zone|datastore|node>\
-            |resource-pool:<pool-name>\
-            |tag:<tag-name>\
-            |remote:<remote-name>\
-            |resource-id:<resource-id>",
+        "[exact:]resource-type=<storage|qemu|lxc|sdn-zone|datastore|node>\
+            |[exact:]resource-pool=<pool-name>\
+            |[exact:]tag=<tag-name>\
+            |[exact:]remote=<remote-name>\
+            |[exact:]resource=id:<resource-id>",
     )
     .schema();
 
@@ -102,65 +102,112 @@ impl ApiSectionDataEntry for ViewConfigEntry {
     }
 }
 
+#[derive(Clone, Debug, PartialEq)]
+/// Matcher for string-based values.
+pub enum StringMatcher {
+    Exact(String),
+}
+
+impl StringMatcher {
+    /// Check if a given string matches.
+    pub fn matches(&self, value: &str) -> bool {
+        match self {
+            StringMatcher::Exact(matched_value) => value == matched_value,
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+/// Matcher for enum-based values.
+pub struct EnumMatcher<T: PartialEq + Clone + Debug>(pub T);
+
+impl<T: PartialEq + Debug + Clone> EnumMatcher<T> {
+    /// Check if a given value matches.
+    pub fn matches(&self, value: &T) -> bool {
+        self.0 == *value
+    }
+}
+
 #[derive(Clone, Debug, PartialEq)]
 /// Filter rule for includes/excludes.
 pub enum FilterRule {
     /// Match a resource type.
-    ResourceType(ResourceType),
+    ResourceType(EnumMatcher<ResourceType>),
     /// Match a resource pools (for PVE guests).
-    ResourcePool(String),
+    ResourcePool(StringMatcher),
     /// Match a (global) resource ID, e.g. 'remote/<remote>/guest/<vmid>'.
-    ResourceId(String),
+    ResourceId(StringMatcher),
     /// Match a tag (for PVE guests).
-    Tag(String),
+    Tag(StringMatcher),
     /// Match a remote.
-    Remote(String),
+    Remote(StringMatcher),
 }
 
 impl FromStr for FilterRule {
     type Err = anyhow::Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s.split_once(':') {
-            Some(("resource-type", value)) => FilterRule::ResourceType(value.parse()?),
-            Some(("resource-pool", value)) => {
-                if !PROXMOX_SAFE_ID_REGEX.is_match(value) {
-                    bail!("invalid resource-pool value: {value}");
-                }
-                FilterRule::ResourcePool(value.to_string())
-            }
-            Some(("resource-id", value)) => {
-                if !GLOBAL_RESOURCE_ID_REGEX.is_match(value) {
-                    bail!("invalid resource-id value: {value}");
-                }
-
-                FilterRule::ResourceId(value.to_string())
-            }
-            Some(("tag", value)) => {
-                if !PROXMOX_SAFE_ID_REGEX.is_match(value) {
-                    bail!("invalid tag value: {value}");
-                }
-                FilterRule::Tag(value.to_string())
-            }
-            Some(("remote", value)) => {
-                let _ = REMOTE_ID_SCHEMA.parse_simple_value(value)?;
-                FilterRule::Remote(value.to_string())
-            }
-            Some((ty, _)) => bail!("invalid type: {ty}"),
-            None => bail!("invalid filter rule: {s}"),
-        })
+        if let Some(s) = s.strip_prefix("exact:") {
+            parse_filter_rule(s)
+        } else {
+            parse_filter_rule(s)
+        }
     }
 }
 
+fn parse_filter_rule(s: &str) -> Result<FilterRule, Error> {
+    Ok(match s.split_once('=') {
+        Some(("resource-type", value)) => FilterRule::ResourceType(EnumMatcher(value.parse()?)),
+        Some(("resource-pool", value)) => {
+            if !PROXMOX_SAFE_ID_REGEX.is_match(value) {
+                bail!("invalid resource-pool value: {value}");
+            }
+
+            let val = StringMatcher::Exact(value.into());
+            FilterRule::ResourcePool(val)
+        }
+        Some(("resource-id", value)) => {
+            if !GLOBAL_RESOURCE_ID_REGEX.is_match(value) {
+                bail!("invalid resource-id value: {value}");
+            }
+
+            let val = StringMatcher::Exact(value.into());
+            FilterRule::ResourceId(val)
+        }
+        Some(("tag", value)) => {
+            if !PROXMOX_SAFE_ID_REGEX.is_match(value) {
+                bail!("invalid tag value: {value}");
+            }
+            let val = StringMatcher::Exact(value.into());
+            FilterRule::Tag(val)
+        }
+        Some(("remote", value)) => {
+            if !PROXMOX_SAFE_ID_REGEX.is_match(value) {
+                let _ = REMOTE_ID_SCHEMA.parse_simple_value(value)?;
+            }
+            let val = StringMatcher::Exact(value.into());
+            FilterRule::Remote(val)
+        }
+        Some((ty, _)) => bail!("invalid type: {ty}"),
+        None => bail!("invalid filter rule: {s}"),
+    })
+}
+
 // used for serializing below, caution!
 impl Display for FilterRule {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
-            FilterRule::ResourceType(resource_type) => write!(f, "resource-type:{resource_type}"),
-            FilterRule::ResourcePool(pool) => write!(f, "resource-pool:{pool}"),
-            FilterRule::ResourceId(id) => write!(f, "resource-id:{id}"),
-            FilterRule::Tag(tag) => write!(f, "tag:{tag}"),
-            FilterRule::Remote(remote) => write!(f, "remote:{remote}"),
+            FilterRule::ResourceType(EnumMatcher(resource_type)) => {
+                write!(f, "exact:resource-type={resource_type}")
+            }
+            FilterRule::ResourceId(StringMatcher::Exact(value)) => {
+                write!(f, "exact:resource-id={value}")
+            }
+            FilterRule::Tag(StringMatcher::Exact(value)) => write!(f, "exact:tag={value}"),
+            FilterRule::Remote(StringMatcher::Exact(value)) => write!(f, "exact:remote={value}"),
+            FilterRule::ResourcePool(StringMatcher::Exact(value)) => {
+                write!(f, "exact:resource-pool={value}")
+            }
         }
     }
 }
@@ -176,7 +223,9 @@ fn verify_filter_rule(input: &str) -> Result<(), anyhow::Error> {
 mod test {
     use anyhow::Error;
 
-    use crate::views::FilterRule;
+    use proxmox_section_config::typed::ApiSectionDataEntry;
+
+    use super::*;
 
     fn parse_and_check_display(input: &str) -> Result<bool, Error> {
         let rule: FilterRule = input.parse()?;
@@ -190,21 +239,49 @@ mod test {
         assert!(parse_and_check_display("abc:").is_err());
 
         assert!(parse_and_check_display("resource-type:").is_err());
-        assert!(parse_and_check_display("resource-type:lxc").unwrap());
-        assert!(parse_and_check_display("resource-type:qemu").unwrap());
-        assert!(parse_and_check_display("resource-type:abc").is_err());
+        assert!(parse_and_check_display("exact:resource-type=lxc").unwrap());
+        assert!(parse_and_check_display("exact:resource-type=qemu").unwrap());
+        assert!(parse_and_check_display("exact:resource-type=abc").is_err());
 
         assert!(parse_and_check_display("resource-pool:").is_err());
-        assert!(parse_and_check_display("resource-pool:somepool").unwrap());
+        assert!(parse_and_check_display("exact:resource-pool=somepool").unwrap());
 
         assert!(parse_and_check_display("resource-id:").is_err());
-        assert!(parse_and_check_display("resource-id:remote/someremote/guest/100").unwrap());
-        assert!(parse_and_check_display("resource-id:remote").is_err());
+        assert!(parse_and_check_display("exact:resource-id=remote/someremote/guest/100").unwrap());
+        assert!(parse_and_check_display("exact:resource-id=remote").is_err());
 
         assert!(parse_and_check_display("tag:").is_err());
-        assert!(parse_and_check_display("tag:sometag").unwrap());
+        assert!(parse_and_check_display("exact:tag=sometag").unwrap());
 
-        assert!(parse_and_check_display("remote:someremote").unwrap());
+        assert!(parse_and_check_display("exact:remote=someremote").unwrap());
         assert!(parse_and_check_display("remote:a").is_err());
     }
+
+    #[test]
+    fn config_smoke_test() {
+        let config = "
+view: some-view
+    include exact:remote=someremote
+    include remote=someremote
+    include resource-type=qemu
+    include exact:resource-type=qemu
+    include resource-id=remote/someremote/guest/100
+    include exact:resource-id=remote/someremote/guest/100
+    include tag=sometag
+    include exact:tag=sometag
+    include resource-pool=somepool
+    include exact:resource-pool=somepool
+    exclude remote=someremote
+    exclude exact:remote=someremote
+    exclude resource-type=qemu
+    exclude exact:resource-type=qemu
+    exclude resource-id=remote/someremote/guest/100
+    exclude exact:resource-id=remote/someremote/guest/100
+    exclude tag=sometag
+    exclude exact:tag=sometag
+    exclude resource-pool=somepool
+    exclude exact:resource-pool=somepool
+";
+        ViewConfigEntry::parse_section_config("views.cfg", config).unwrap();
+    }
 }
diff --git a/server/src/views/mod.rs b/server/src/views/mod.rs
index 80f8425c..8b4c70a4 100644
--- a/server/src/views/mod.rs
+++ b/server/src/views/mod.rs
@@ -72,7 +72,7 @@ impl View {
         for include in &self.config.include {
             if let FilterRule::Remote(r) = include {
                 has_any_include_remote = true;
-                if r == remote {
+                if r.matches(remote) {
                     matches_any_include_remote = true;
                     break;
                 }
@@ -145,7 +145,7 @@ impl View {
 
     fn matches_remote_rule(remote: &str, rule: &FilterRule) -> bool {
         if let FilterRule::Remote(r) = rule {
-            r == remote
+            r.matches(remote)
         } else {
             false
         }
@@ -154,17 +154,23 @@ impl View {
 
 fn check_rules(rules: &[FilterRule], remote: &str, resource: &ResourceData) -> bool {
     rules.iter().any(|rule| match rule {
-        FilterRule::ResourceType(resource_type) => resource.resource_type == *resource_type,
-        FilterRule::ResourcePool(pool) => resource.resource_pool == Some(pool),
-        FilterRule::ResourceId(resource_id) => resource.resource_id == resource_id,
-        FilterRule::Tag(tag) => {
-            if let Some(resource_tags) = resource.tags {
-                resource_tags.contains(tag)
+        FilterRule::ResourceType(resource_type) => resource_type.matches(&resource.resource_type),
+        FilterRule::ResourcePool(pool) => {
+            if let Some(resource_pool) = resource.resource_pool {
+                pool.matches(resource_pool)
             } else {
                 false
             }
         }
-        FilterRule::Remote(included_remote) => included_remote == remote,
+        FilterRule::ResourceId(resource_id) => resource_id.matches(resource.resource_id),
+        FilterRule::Tag(tag) => {
+            if let Some(resource_tags) = resource.tags {
+                resource_tags.iter().any(|t| tag.matches(t))
+            } else {
+                false
+            }
+        }
+        FilterRule::Remote(included_remote) => included_remote.matches(remote),
     })
 }
 
diff --git a/server/src/views/tests.rs b/server/src/views/tests.rs
index 030b7994..14d94ac9 100644
--- a/server/src/views/tests.rs
+++ b/server/src/views/tests.rs
@@ -1,6 +1,6 @@
 use pdm_api_types::{
     resource::{PveLxcResource, PveQemuResource, PveStorageResource, Resource, ResourceType},
-    views::{FilterRule, ViewConfig},
+    views::{EnumMatcher, FilterRule, StringMatcher, ViewConfig},
 };
 
 use super::View;
@@ -88,8 +88,8 @@ fn include_remotes() {
     let config = ViewConfig {
         id: "only-includes".into(),
         include: vec![
-            FilterRule::Remote("remote-a".into()),
-            FilterRule::Remote("remote-b".into()),
+            FilterRule::Remote(StringMatcher::Exact("remote-a".into())),
+            FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
         ],
         ..Default::default()
     };
@@ -132,8 +132,8 @@ fn exclude_remotes() {
     let config = ViewConfig {
         id: "only-excludes".into(),
         exclude: vec![
-            FilterRule::Remote("remote-a".into()),
-            FilterRule::Remote("remote-b".into()),
+            FilterRule::Remote(StringMatcher::Exact("remote-a".into())),
+            FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
         ],
         ..Default::default()
     };
@@ -177,12 +177,12 @@ fn include_exclude_remotes() {
     let config = ViewConfig {
         id: "both".into(),
         include: vec![
-            FilterRule::Remote("remote-a".into()),
-            FilterRule::Remote("remote-b".into()),
+            FilterRule::Remote(StringMatcher::Exact("remote-a".into())),
+            FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
         ],
         exclude: vec![
-            FilterRule::Remote("remote-b".into()),
-            FilterRule::Remote("remote-c".into()),
+            FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
+            FilterRule::Remote(StringMatcher::Exact("remote-c".into())),
         ],
     };
     run_test(
@@ -270,8 +270,8 @@ fn include_type() {
         ViewConfig {
             id: "include-resource-type".into(),
             include: vec![
-                FilterRule::ResourceType(ResourceType::PveStorage),
-                FilterRule::ResourceType(ResourceType::PveQemu),
+                FilterRule::ResourceType(EnumMatcher(ResourceType::PveStorage)),
+                FilterRule::ResourceType(EnumMatcher(ResourceType::PveQemu)),
             ],
             ..Default::default()
         },
@@ -298,8 +298,8 @@ fn exclude_type() {
         ViewConfig {
             id: "exclude-resource-type".into(),
             exclude: vec![
-                FilterRule::ResourceType(ResourceType::PveStorage),
-                FilterRule::ResourceType(ResourceType::PveQemu),
+                FilterRule::ResourceType(EnumMatcher(ResourceType::PveStorage)),
+                FilterRule::ResourceType(EnumMatcher(ResourceType::PveQemu)),
             ],
             ..Default::default()
         },
@@ -325,8 +325,10 @@ fn include_exclude_type() {
     run_test(
         ViewConfig {
             id: "exclude-resource-type".into(),
-            include: vec![FilterRule::ResourceType(ResourceType::PveQemu)],
-            exclude: vec![FilterRule::ResourceType(ResourceType::PveStorage)],
+            include: vec![FilterRule::ResourceType(EnumMatcher(ResourceType::PveQemu))],
+            exclude: vec![FilterRule::ResourceType(EnumMatcher(
+                ResourceType::PveStorage,
+            ))],
         },
         &[
             (
@@ -351,10 +353,10 @@ fn include_exclude_tags() {
         ViewConfig {
             id: "include-tags".into(),
             include: vec![
-                FilterRule::Tag("tag1".to_string()),
-                FilterRule::Tag("tag2".to_string()),
+                FilterRule::Tag(StringMatcher::Exact("tag1".to_string())),
+                FilterRule::Tag(StringMatcher::Exact("tag2".to_string())),
             ],
-            exclude: vec![FilterRule::Tag("tag3".to_string())],
+            exclude: vec![FilterRule::Tag(StringMatcher::Exact("tag3".to_string()))],
         },
         &[
             (
@@ -396,10 +398,12 @@ fn include_exclude_resource_pool() {
         ViewConfig {
             id: "pools".into(),
             include: vec![
-                FilterRule::ResourcePool("pool1".to_string()),
-                FilterRule::ResourcePool("pool2".to_string()),
+                FilterRule::ResourcePool(StringMatcher::Exact("pool1".to_string())),
+                FilterRule::ResourcePool(StringMatcher::Exact("pool2".to_string())),
             ],
-            exclude: vec![FilterRule::ResourcePool("pool2".to_string())],
+            exclude: vec![FilterRule::ResourcePool(StringMatcher::Exact(
+                "pool2".to_string(),
+            ))],
         },
         &[
             (
@@ -441,13 +445,19 @@ fn include_exclude_resource_id() {
         ViewConfig {
             id: "resource-id".into(),
             include: vec![
-                FilterRule::ResourceId(format!("remote/{REMOTE}/guest/100")),
-                FilterRule::ResourceId(format!("remote/{REMOTE}/storage/{NODE}/{STORAGE}")),
+                FilterRule::ResourceId(StringMatcher::Exact(format!("remote/{REMOTE}/guest/100"))),
+                FilterRule::ResourceId(StringMatcher::Exact(format!(
+                    "remote/{REMOTE}/storage/{NODE}/{STORAGE}"
+                ))),
             ],
             exclude: vec![
-                FilterRule::ResourceId(format!("remote/{REMOTE}/guest/101")),
-                FilterRule::ResourceId("remote/otherremote/guest/101".to_string()),
-                FilterRule::ResourceId(format!("remote/{REMOTE}/storage/{NODE}/otherstorage")),
+                FilterRule::ResourceId(StringMatcher::Exact(format!("remote/{REMOTE}/guest/101"))),
+                FilterRule::ResourceId(StringMatcher::Exact(
+                    "remote/otherremote/guest/101".to_string(),
+                )),
+                FilterRule::ResourceId(StringMatcher::Exact(format!(
+                    "remote/{REMOTE}/storage/{NODE}/otherstorage"
+                ))),
             ],
         },
         &[
@@ -491,10 +501,14 @@ fn node_included() {
         id: "both".into(),
 
         include: vec![
-            FilterRule::Remote("remote-a".to_string()),
-            FilterRule::ResourceId("remote/someremote/node/test".to_string()),
+            FilterRule::Remote(StringMatcher::Exact("remote-a".to_string())),
+            FilterRule::ResourceId(StringMatcher::Exact(
+                "remote/someremote/node/test".to_string(),
+            )),
         ],
-        exclude: vec![FilterRule::Remote("remote-b".to_string())],
+        exclude: vec![FilterRule::Remote(StringMatcher::Exact(
+            "remote-b".to_string(),
+        ))],
     });
 
     assert!(view.is_node_included("remote-a", "somenode"));
@@ -511,7 +525,9 @@ fn can_skip_remote_if_excluded() {
     let view = View::new(ViewConfig {
         id: "abc".into(),
         include: vec![],
-        exclude: vec![FilterRule::Remote("remote-b".to_string())],
+        exclude: vec![FilterRule::Remote(StringMatcher::Exact(
+            "remote-b".to_string(),
+        ))],
     });
 
     assert!(!view.can_skip_remote("remote-a"));
@@ -522,7 +538,9 @@ fn can_skip_remote_if_excluded() {
 fn can_skip_remote_if_included() {
     let view = View::new(ViewConfig {
         id: "abc".into(),
-        include: vec![FilterRule::Remote("remote-b".to_string())],
+        include: vec![FilterRule::Remote(StringMatcher::Exact(
+            "remote-b".to_string(),
+        ))],
         exclude: vec![],
     });
 
@@ -535,8 +553,10 @@ fn can_skip_remote_cannot_skip_if_any_other_include() {
     let view = View::new(ViewConfig {
         id: "abc".into(),
         include: vec![
-            FilterRule::Remote("remote-b".to_string()),
-            FilterRule::ResourceId("resource/remote-a/guest/100".to_string()),
+            FilterRule::Remote(StringMatcher::Exact("remote-b".to_string())),
+            FilterRule::ResourceId(StringMatcher::Exact(
+                "resource/remote-a/guest/100".to_string(),
+            )),
         ],
         exclude: vec![],
     });
@@ -549,10 +569,12 @@ fn can_skip_remote_cannot_skip_if_any_other_include() {
 fn can_skip_remote_explicit_remote_exclude() {
     let view = View::new(ViewConfig {
         id: "abc".into(),
-        include: vec![FilterRule::ResourceId(
+        include: vec![FilterRule::ResourceId(StringMatcher::Exact(
             "resource/remote-a/guest/100".to_string(),
-        )],
-        exclude: vec![FilterRule::Remote("remote-a".to_string())],
+        ))],
+        exclude: vec![FilterRule::Remote(StringMatcher::Exact(
+            "remote-a".to_string(),
+        ))],
     });
 
     assert!(view.can_skip_remote("remote-a"));
@@ -574,9 +596,9 @@ fn can_skip_remote_with_empty_config() {
 fn can_skip_remote_with_no_remote_includes() {
     let view = View::new(ViewConfig {
         id: "abc".into(),
-        include: vec![FilterRule::ResourceId(
+        include: vec![FilterRule::ResourceId(StringMatcher::Exact(
             "resource/remote-a/guest/100".to_string(),
-        )],
+        ))],
         exclude: vec![],
     });
 
@@ -588,7 +610,9 @@ fn can_skip_remote_with_no_remote_includes() {
 fn explicitly_included_remote() {
     let view = View::new(ViewConfig {
         id: "abc".into(),
-        include: vec![FilterRule::Remote("remote-b".to_string())],
+        include: vec![FilterRule::Remote(StringMatcher::Exact(
+            "remote-b".to_string(),
+        ))],
         exclude: vec![],
     });
 
@@ -599,8 +623,12 @@ fn explicitly_included_remote() {
 fn included_and_excluded_same_remote() {
     let view = View::new(ViewConfig {
         id: "abc".into(),
-        include: vec![FilterRule::Remote("remote-b".to_string())],
-        exclude: vec![FilterRule::Remote("remote-b".to_string())],
+        include: vec![FilterRule::Remote(StringMatcher::Exact(
+            "remote-b".to_string(),
+        ))],
+        exclude: vec![FilterRule::Remote(StringMatcher::Exact(
+            "remote-b".to_string(),
+        ))],
     });
 
     assert!(!view.is_remote_explicitly_included("remote-b"));
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pdm-devel] [PATCH datacenter-manager 2/3] views: add 'include-all' param; change semantics when there are no includes
  2025-11-17 14:11 [pdm-devel] [PATCH datacenter-manager 0/3] views: preparations for regex/glob, include-all param Lukas Wagner
  2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 1/3] pdm-api-types: views: preparations for future glob/regex support Lukas Wagner
@ 2025-11-17 14:11 ` Lukas Wagner
  2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 3/3] views: tests: use full section-config format for test cases Lukas Wagner
  2 siblings, 0 replies; 4+ messages in thread
From: Lukas Wagner @ 2025-11-17 14:11 UTC (permalink / raw)
  To: pdm-devel

Previously, having no 'include' rules would mean that *all* resources
are included. Due view permissions being transitively applied to all
included resources, this also has some security consequences.
For instance, if there is only a single include rule which is then
removed by accident, suddenly any AuthID having privileges on the view
ACL object would then be granted these privileges on *all* resources.

To somewhat mitigate this, the semantics are slightly changed. If there
are no include rules, then no resources will be contained in the view.
A new 'include-all' parameter is added, which aims to emphasize the
intent of *really* including everything.

Suggested-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 lib/pdm-api-types/src/views.rs |  4 +++
 server/src/views/mod.rs        | 60 +++++++++++++++++++---------------
 server/src/views/tests.rs      | 35 ++++++++++++++++++--
 3 files changed, 69 insertions(+), 30 deletions(-)

diff --git a/lib/pdm-api-types/src/views.rs b/lib/pdm-api-types/src/views.rs
index 950a3a04..82ab8781 100644
--- a/lib/pdm-api-types/src/views.rs
+++ b/lib/pdm-api-types/src/views.rs
@@ -58,6 +58,9 @@ pub struct ViewConfig {
     #[updater(skip)]
     pub id: String,
 
+    /// Include all resources by default.
+    pub include_all: Option<bool>,
+
     /// List of includes.
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     #[updater(serde(skip_serializing_if = "Option::is_none"))]
@@ -261,6 +264,7 @@ mod test {
     fn config_smoke_test() {
         let config = "
 view: some-view
+    include-all true
     include exact:remote=someremote
     include remote=someremote
     include resource-type=qemu
diff --git a/server/src/views/mod.rs b/server/src/views/mod.rs
index 8b4c70a4..3669ace4 100644
--- a/server/src/views/mod.rs
+++ b/server/src/views/mod.rs
@@ -64,31 +64,34 @@ impl View {
     /// When there are `include remote:<...>` or `exclude remote:<...>` rules, we can use these to
     /// check if a remote needs to be considered at all.
     pub fn can_skip_remote(&self, remote: &str) -> bool {
-        let mut has_any_include_remote = false;
-        let mut matches_any_include_remote = false;
-
-        let mut any_other = false;
-
-        for include in &self.config.include {
-            if let FilterRule::Remote(r) = include {
-                has_any_include_remote = true;
-                if r.matches(remote) {
-                    matches_any_include_remote = true;
-                    break;
-                }
-            } else {
-                any_other = true;
-            }
-        }
-
         let matches_any_exclude_remote = self
             .config
             .exclude
             .iter()
             .any(|rule| Self::matches_remote_rule(remote, rule));
 
-        (has_any_include_remote && !matches_any_include_remote && !any_other)
-            || matches_any_exclude_remote
+        if matches_any_exclude_remote {
+            return true;
+        }
+
+        if self.config.include_all.unwrap_or_default() {
+            return false;
+        }
+
+        for include in &self.config.include {
+            if let FilterRule::Remote(r) = include {
+                if r.matches(remote) {
+                    return false;
+                }
+            } else {
+                // If there is any other type of rule, we cannot safely infer whether we can skip
+                // the remote (e.g. for 'tag' matches, we have to check *all* remotes for resources
+                // with a given tag)
+                return false;
+            }
+        }
+
+        true
     }
 
     /// Check if a remote is *explicitly* included (and not excluded).
@@ -96,18 +99,22 @@ impl View {
     /// A subset of the resources of a remote might still be pulled in by other rules,
     /// but this function check if the remote as a whole is matched.
     pub fn is_remote_explicitly_included(&self, remote: &str) -> bool {
-        let matches_include_remote = self
-            .config
-            .include
-            .iter()
-            .any(|rule| Self::matches_remote_rule(remote, rule));
+        let included = if self.config.include_all.unwrap_or_default() {
+            true
+        } else {
+            self.config
+                .include
+                .iter()
+                .any(|rule| Self::matches_remote_rule(remote, rule))
+        };
+
         let matches_exclude_remote = self
             .config
             .exclude
             .iter()
             .any(|rule| Self::matches_remote_rule(remote, rule));
 
-        matches_include_remote && !matches_exclude_remote
+        included && !matches_exclude_remote
     }
 
     /// Check if a node is matched by the filter rules.
@@ -131,8 +138,7 @@ impl View {
     }
 
     fn check_if_included(&self, remote: &str, resource: &ResourceData) -> bool {
-        if self.config.include.is_empty() {
-            // If there are no include rules, any resource is included (unless excluded)
+        if self.config.include_all.unwrap_or_default() {
             return true;
         }
 
diff --git a/server/src/views/tests.rs b/server/src/views/tests.rs
index 14d94ac9..0d83ae70 100644
--- a/server/src/views/tests.rs
+++ b/server/src/views/tests.rs
@@ -135,6 +135,7 @@ fn exclude_remotes() {
             FilterRule::Remote(StringMatcher::Exact("remote-a".into())),
             FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
         ],
+        include_all: Some(true),
         ..Default::default()
     };
 
@@ -184,6 +185,8 @@ fn include_exclude_remotes() {
             FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
             FilterRule::Remote(StringMatcher::Exact("remote-c".into())),
         ],
+
+        ..Default::default()
     };
     run_test(
         config.clone(),
@@ -224,6 +227,7 @@ fn include_exclude_remotes() {
 fn empty_config() {
     let config = ViewConfig {
         id: "empty".into(),
+        include_all: Some(true),
         ..Default::default()
     };
     run_test(
@@ -301,6 +305,7 @@ fn exclude_type() {
                 FilterRule::ResourceType(EnumMatcher(ResourceType::PveStorage)),
                 FilterRule::ResourceType(EnumMatcher(ResourceType::PveQemu)),
             ],
+            include_all: Some(true),
             ..Default::default()
         },
         &[
@@ -329,6 +334,7 @@ fn include_exclude_type() {
             exclude: vec![FilterRule::ResourceType(EnumMatcher(
                 ResourceType::PveStorage,
             ))],
+            ..Default::default()
         },
         &[
             (
@@ -357,6 +363,7 @@ fn include_exclude_tags() {
                 FilterRule::Tag(StringMatcher::Exact("tag2".to_string())),
             ],
             exclude: vec![FilterRule::Tag(StringMatcher::Exact("tag3".to_string()))],
+            ..Default::default()
         },
         &[
             (
@@ -404,6 +411,7 @@ fn include_exclude_resource_pool() {
             exclude: vec![FilterRule::ResourcePool(StringMatcher::Exact(
                 "pool2".to_string(),
             ))],
+            ..Default::default()
         },
         &[
             (
@@ -459,6 +467,7 @@ fn include_exclude_resource_id() {
                     "remote/{REMOTE}/storage/{NODE}/otherstorage"
                 ))),
             ],
+            ..Default::default()
         },
         &[
             (
@@ -509,6 +518,7 @@ fn node_included() {
         exclude: vec![FilterRule::Remote(StringMatcher::Exact(
             "remote-b".to_string(),
         ))],
+        ..Default::default()
     });
 
     assert!(view.is_node_included("remote-a", "somenode"));
@@ -528,6 +538,7 @@ fn can_skip_remote_if_excluded() {
         exclude: vec![FilterRule::Remote(StringMatcher::Exact(
             "remote-b".to_string(),
         ))],
+        include_all: Some(true),
     });
 
     assert!(!view.can_skip_remote("remote-a"));
@@ -542,6 +553,7 @@ fn can_skip_remote_if_included() {
             "remote-b".to_string(),
         ))],
         exclude: vec![],
+        ..Default::default()
     });
 
     assert!(!view.can_skip_remote("remote-b"));
@@ -559,6 +571,7 @@ fn can_skip_remote_cannot_skip_if_any_other_include() {
             )),
         ],
         exclude: vec![],
+        ..Default::default()
     });
 
     assert!(!view.can_skip_remote("remote-b"));
@@ -575,6 +588,7 @@ fn can_skip_remote_explicit_remote_exclude() {
         exclude: vec![FilterRule::Remote(StringMatcher::Exact(
             "remote-a".to_string(),
         ))],
+        ..Default::default()
     });
 
     assert!(view.can_skip_remote("remote-a"));
@@ -584,8 +598,19 @@ fn can_skip_remote_explicit_remote_exclude() {
 fn can_skip_remote_with_empty_config() {
     let view = View::new(ViewConfig {
         id: "abc".into(),
-        include: vec![],
-        exclude: vec![],
+        ..Default::default()
+    });
+
+    assert!(view.can_skip_remote("remote-a"));
+    assert!(view.can_skip_remote("remote-b"));
+}
+
+#[test]
+fn can_skip_remote_cannot_skip_if_all_included() {
+    let view = View::new(ViewConfig {
+        id: "abc".into(),
+        include_all: Some(true),
+        ..Default::default()
     });
 
     assert!(!view.can_skip_remote("remote-a"));
@@ -600,6 +625,7 @@ fn can_skip_remote_with_no_remote_includes() {
             "resource/remote-a/guest/100".to_string(),
         ))],
         exclude: vec![],
+        ..Default::default()
     });
 
     assert!(!view.can_skip_remote("remote-a"));
@@ -614,6 +640,7 @@ fn explicitly_included_remote() {
             "remote-b".to_string(),
         ))],
         exclude: vec![],
+        ..Default::default()
     });
 
     assert!(view.is_remote_explicitly_included("remote-b"));
@@ -629,6 +656,7 @@ fn included_and_excluded_same_remote() {
         exclude: vec![FilterRule::Remote(StringMatcher::Exact(
             "remote-b".to_string(),
         ))],
+        ..Default::default()
     });
 
     assert!(!view.is_remote_explicitly_included("remote-b"));
@@ -640,8 +668,9 @@ fn not_explicitly_included_remote() {
         id: "abc".into(),
         include: vec![],
         exclude: vec![],
+        include_all: Some(true),
     });
 
     // Assert that is not *explicitly* included
-    assert!(!view.is_remote_explicitly_included("remote-b"));
+    assert!(view.is_remote_explicitly_included("remote-b"));
 }
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pdm-devel] [PATCH datacenter-manager 3/3] views: tests: use full section-config format for test cases
  2025-11-17 14:11 [pdm-devel] [PATCH datacenter-manager 0/3] views: preparations for regex/glob, include-all param Lukas Wagner
  2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 1/3] pdm-api-types: views: preparations for future glob/regex support Lukas Wagner
  2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 2/3] views: add 'include-all' param; change semantics when there are no includes Lukas Wagner
@ 2025-11-17 14:11 ` Lukas Wagner
  2 siblings, 0 replies; 4+ messages in thread
From: Lukas Wagner @ 2025-11-17 14:11 UTC (permalink / raw)
  To: pdm-devel

The config structure should be more stable than the actual rust types,
so this should lead to less maintenance burden over time. Also, the
tests are a bit easier to read this way.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 server/src/views/tests.rs | 378 +++++++++++++++++++-------------------
 1 file changed, 186 insertions(+), 192 deletions(-)

diff --git a/server/src/views/tests.rs b/server/src/views/tests.rs
index 0d83ae70..9f496207 100644
--- a/server/src/views/tests.rs
+++ b/server/src/views/tests.rs
@@ -1,7 +1,8 @@
 use pdm_api_types::{
-    resource::{PveLxcResource, PveQemuResource, PveStorageResource, Resource, ResourceType},
-    views::{EnumMatcher, FilterRule, StringMatcher, ViewConfig},
+    resource::{PveLxcResource, PveQemuResource, PveStorageResource, Resource},
+    views::{ViewConfig, ViewConfigEntry},
 };
+use proxmox_section_config::typed::ApiSectionDataEntry;
 
 use super::View;
 
@@ -79,20 +80,26 @@ fn run_test(config: ViewConfig, tests: &[((&str, &Resource), bool)]) {
     }
 }
 
+fn parse_config(config: &str) -> ViewConfig {
+    let config = ViewConfigEntry::parse_section_config("views.cfg", config).unwrap();
+    let ViewConfigEntry::View(config) = config.get("test").unwrap();
+    config.clone()
+}
+
 const NODE: &str = "somenode";
 const STORAGE: &str = "somestorage";
 const REMOTE: &str = "someremote";
 
 #[test]
 fn include_remotes() {
-    let config = ViewConfig {
-        id: "only-includes".into(),
-        include: vec![
-            FilterRule::Remote(StringMatcher::Exact("remote-a".into())),
-            FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
-        ],
-        ..Default::default()
-    };
+    let config = parse_config(
+        "
+view: test
+    include remote=remote-a
+    include remote=remote-b
+",
+    );
+
     run_test(
         config.clone(),
         &[
@@ -129,15 +136,14 @@ fn include_remotes() {
 
 #[test]
 fn exclude_remotes() {
-    let config = ViewConfig {
-        id: "only-excludes".into(),
-        exclude: vec![
-            FilterRule::Remote(StringMatcher::Exact("remote-a".into())),
-            FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
-        ],
-        include_all: Some(true),
-        ..Default::default()
-    };
+    let config = parse_config(
+        "
+view: test
+    include-all true
+    exclude remote=remote-a
+    exclude remote=remote-b
+",
+    );
 
     run_test(
         config.clone(),
@@ -175,19 +181,16 @@ fn exclude_remotes() {
 
 #[test]
 fn include_exclude_remotes() {
-    let config = ViewConfig {
-        id: "both".into(),
-        include: vec![
-            FilterRule::Remote(StringMatcher::Exact("remote-a".into())),
-            FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
-        ],
-        exclude: vec![
-            FilterRule::Remote(StringMatcher::Exact("remote-b".into())),
-            FilterRule::Remote(StringMatcher::Exact("remote-c".into())),
-        ],
+    let config = parse_config(
+        "
+view: test
+    include remote=remote-a
+    include remote=remote-b
+    exclude remote=remote-b
+    exclude remote=remote-c
+",
+    );
 
-        ..Default::default()
-    };
     run_test(
         config.clone(),
         &[
@@ -225,11 +228,12 @@ fn include_exclude_remotes() {
 
 #[test]
 fn empty_config() {
-    let config = ViewConfig {
-        id: "empty".into(),
-        include_all: Some(true),
-        ..Default::default()
-    };
+    let config = parse_config(
+        "
+view: test
+    include-all true
+",
+    );
     run_test(
         config.clone(),
         &[
@@ -270,15 +274,15 @@ fn empty_config() {
 
 #[test]
 fn include_type() {
+    let config = parse_config(
+        "
+view: test
+    include resource-type=storage
+    include resource-type=qemu
+",
+    );
     run_test(
-        ViewConfig {
-            id: "include-resource-type".into(),
-            include: vec![
-                FilterRule::ResourceType(EnumMatcher(ResourceType::PveStorage)),
-                FilterRule::ResourceType(EnumMatcher(ResourceType::PveQemu)),
-            ],
-            ..Default::default()
-        },
+        config,
         &[
             (
                 (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
@@ -298,16 +302,16 @@ fn include_type() {
 
 #[test]
 fn exclude_type() {
+    let config = parse_config(
+        "
+view: test
+    include-all true
+    exclude resource-type=storage
+    exclude resource-type=qemu
+",
+    );
     run_test(
-        ViewConfig {
-            id: "exclude-resource-type".into(),
-            exclude: vec![
-                FilterRule::ResourceType(EnumMatcher(ResourceType::PveStorage)),
-                FilterRule::ResourceType(EnumMatcher(ResourceType::PveQemu)),
-            ],
-            include_all: Some(true),
-            ..Default::default()
-        },
+        config,
         &[
             (
                 (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
@@ -327,15 +331,16 @@ fn exclude_type() {
 
 #[test]
 fn include_exclude_type() {
+    let config = parse_config(
+        "
+view: test
+    include resource-type=qemu
+    exclude resource-type=storage
+",
+    );
+
     run_test(
-        ViewConfig {
-            id: "exclude-resource-type".into(),
-            include: vec![FilterRule::ResourceType(EnumMatcher(ResourceType::PveQemu))],
-            exclude: vec![FilterRule::ResourceType(EnumMatcher(
-                ResourceType::PveStorage,
-            ))],
-            ..Default::default()
-        },
+        config,
         &[
             (
                 (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
@@ -355,16 +360,16 @@ fn include_exclude_type() {
 
 #[test]
 fn include_exclude_tags() {
+    let config = parse_config(
+        "
+view: test
+    include tag=tag1
+    include tag=tag2
+    exclude tag=tag3
+",
+    );
     run_test(
-        ViewConfig {
-            id: "include-tags".into(),
-            include: vec![
-                FilterRule::Tag(StringMatcher::Exact("tag1".to_string())),
-                FilterRule::Tag(StringMatcher::Exact("tag2".to_string())),
-            ],
-            exclude: vec![FilterRule::Tag(StringMatcher::Exact("tag3".to_string()))],
-            ..Default::default()
-        },
+        config,
         &[
             (
                 (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
@@ -401,18 +406,16 @@ fn include_exclude_tags() {
 
 #[test]
 fn include_exclude_resource_pool() {
+    let config = parse_config(
+        "
+view: test
+    include resource-pool=pool1
+    include resource-pool=pool2
+    exclude resource-pool=pool2
+",
+    );
     run_test(
-        ViewConfig {
-            id: "pools".into(),
-            include: vec![
-                FilterRule::ResourcePool(StringMatcher::Exact("pool1".to_string())),
-                FilterRule::ResourcePool(StringMatcher::Exact("pool2".to_string())),
-            ],
-            exclude: vec![FilterRule::ResourcePool(StringMatcher::Exact(
-                "pool2".to_string(),
-            ))],
-            ..Default::default()
-        },
+        config,
         &[
             (
                 (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
@@ -449,26 +452,18 @@ fn include_exclude_resource_pool() {
 
 #[test]
 fn include_exclude_resource_id() {
+    let config = parse_config(
+        "
+view: test
+    include resource-id=remote/someremote/guest/100
+    include resource-id=remote/someremote/storage/somenode/somestorage
+    exclude resource-id=remote/someremote/guest/101
+    exclude resource-id=remote/otherremote/guest/101
+    exclude resource-id=remote/someremote/storage/somenode/otherstorage
+",
+    );
     run_test(
-        ViewConfig {
-            id: "resource-id".into(),
-            include: vec![
-                FilterRule::ResourceId(StringMatcher::Exact(format!("remote/{REMOTE}/guest/100"))),
-                FilterRule::ResourceId(StringMatcher::Exact(format!(
-                    "remote/{REMOTE}/storage/{NODE}/{STORAGE}"
-                ))),
-            ],
-            exclude: vec![
-                FilterRule::ResourceId(StringMatcher::Exact(format!("remote/{REMOTE}/guest/101"))),
-                FilterRule::ResourceId(StringMatcher::Exact(
-                    "remote/otherremote/guest/101".to_string(),
-                )),
-                FilterRule::ResourceId(StringMatcher::Exact(format!(
-                    "remote/{REMOTE}/storage/{NODE}/otherstorage"
-                ))),
-            ],
-            ..Default::default()
-        },
+        config,
         &[
             (
                 (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
@@ -506,20 +501,16 @@ fn include_exclude_resource_id() {
 
 #[test]
 fn node_included() {
-    let view = View::new(ViewConfig {
-        id: "both".into(),
+    let config = parse_config(
+        "
+view: test
+    include remote=remote-a
+    include resource-id=remote/someremote/node/test
+    exclude remote=remote-b
+",
+    );
 
-        include: vec![
-            FilterRule::Remote(StringMatcher::Exact("remote-a".to_string())),
-            FilterRule::ResourceId(StringMatcher::Exact(
-                "remote/someremote/node/test".to_string(),
-            )),
-        ],
-        exclude: vec![FilterRule::Remote(StringMatcher::Exact(
-            "remote-b".to_string(),
-        ))],
-        ..Default::default()
-    });
+    let view = View::new(config);
 
     assert!(view.is_node_included("remote-a", "somenode"));
     assert!(view.is_node_included("remote-a", "somenode2"));
@@ -527,19 +518,19 @@ fn node_included() {
     assert!(!view.is_node_included("remote-b", "somenode2"));
     assert!(view.is_node_included("someremote", "test"));
 
-    assert_eq!(view.name(), "both");
+    assert_eq!(view.name(), "test");
 }
 
 #[test]
 fn can_skip_remote_if_excluded() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        include: vec![],
-        exclude: vec![FilterRule::Remote(StringMatcher::Exact(
-            "remote-b".to_string(),
-        ))],
-        include_all: Some(true),
-    });
+    let config = parse_config(
+        "
+view: test
+    include-all true
+    exclude remote=remote-b
+",
+    );
+    let view = View::new(config);
 
     assert!(!view.can_skip_remote("remote-a"));
     assert!(view.can_skip_remote("remote-b"));
@@ -547,14 +538,14 @@ fn can_skip_remote_if_excluded() {
 
 #[test]
 fn can_skip_remote_if_included() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        include: vec![FilterRule::Remote(StringMatcher::Exact(
-            "remote-b".to_string(),
-        ))],
-        exclude: vec![],
-        ..Default::default()
-    });
+    let config = parse_config(
+        "
+view: test
+    include remote=remote-b
+",
+    );
+
+    let view = View::new(config);
 
     assert!(!view.can_skip_remote("remote-b"));
     assert!(view.can_skip_remote("remote-a"));
@@ -562,17 +553,15 @@ fn can_skip_remote_if_included() {
 
 #[test]
 fn can_skip_remote_cannot_skip_if_any_other_include() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        include: vec![
-            FilterRule::Remote(StringMatcher::Exact("remote-b".to_string())),
-            FilterRule::ResourceId(StringMatcher::Exact(
-                "resource/remote-a/guest/100".to_string(),
-            )),
-        ],
-        exclude: vec![],
-        ..Default::default()
-    });
+    let config = parse_config(
+        "
+view: test
+    include remote=remote-b
+    include resource-id=remote/remote-a/guest/100
+",
+    );
+
+    let view = View::new(config);
 
     assert!(!view.can_skip_remote("remote-b"));
     assert!(!view.can_skip_remote("remote-a"));
@@ -580,26 +569,28 @@ fn can_skip_remote_cannot_skip_if_any_other_include() {
 
 #[test]
 fn can_skip_remote_explicit_remote_exclude() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        include: vec![FilterRule::ResourceId(StringMatcher::Exact(
-            "resource/remote-a/guest/100".to_string(),
-        ))],
-        exclude: vec![FilterRule::Remote(StringMatcher::Exact(
-            "remote-a".to_string(),
-        ))],
-        ..Default::default()
-    });
+    let config = parse_config(
+        "
+view: test
+    exclude remote=remote-a
+    include resource-id=remote/remote-a/guest/100
+",
+    );
+
+    let view = View::new(config);
 
     assert!(view.can_skip_remote("remote-a"));
 }
 
 #[test]
 fn can_skip_remote_with_empty_config() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        ..Default::default()
-    });
+    let config = parse_config(
+        "
+view: test
+",
+    );
+
+    let view = View::new(config);
 
     assert!(view.can_skip_remote("remote-a"));
     assert!(view.can_skip_remote("remote-b"));
@@ -607,11 +598,14 @@ fn can_skip_remote_with_empty_config() {
 
 #[test]
 fn can_skip_remote_cannot_skip_if_all_included() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        include_all: Some(true),
-        ..Default::default()
-    });
+    let config = parse_config(
+        "
+view: test
+    include-all true
+",
+    );
+
+    let view = View::new(config);
 
     assert!(!view.can_skip_remote("remote-a"));
     assert!(!view.can_skip_remote("remote-b"));
@@ -619,14 +613,14 @@ fn can_skip_remote_cannot_skip_if_all_included() {
 
 #[test]
 fn can_skip_remote_with_no_remote_includes() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        include: vec![FilterRule::ResourceId(StringMatcher::Exact(
-            "resource/remote-a/guest/100".to_string(),
-        ))],
-        exclude: vec![],
-        ..Default::default()
-    });
+    let config = parse_config(
+        "
+view: test
+    include resource-id=remote/remote-a/guest/100
+",
+    );
+
+    let view = View::new(config);
 
     assert!(!view.can_skip_remote("remote-a"));
     assert!(!view.can_skip_remote("remote-b"));
@@ -634,42 +628,42 @@ fn can_skip_remote_with_no_remote_includes() {
 
 #[test]
 fn explicitly_included_remote() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        include: vec![FilterRule::Remote(StringMatcher::Exact(
-            "remote-b".to_string(),
-        ))],
-        exclude: vec![],
-        ..Default::default()
-    });
+    let config = parse_config(
+        "
+view: test
+    include remote=remote-b
+",
+    );
+
+    let view = View::new(config);
 
     assert!(view.is_remote_explicitly_included("remote-b"));
 }
 
 #[test]
 fn included_and_excluded_same_remote() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        include: vec![FilterRule::Remote(StringMatcher::Exact(
-            "remote-b".to_string(),
-        ))],
-        exclude: vec![FilterRule::Remote(StringMatcher::Exact(
-            "remote-b".to_string(),
-        ))],
-        ..Default::default()
-    });
+    let config = parse_config(
+        "
+view: test
+    include remote=remote-b
+    exclude remote=remote-b
+",
+    );
+
+    let view = View::new(config);
 
     assert!(!view.is_remote_explicitly_included("remote-b"));
 }
 
 #[test]
 fn not_explicitly_included_remote() {
-    let view = View::new(ViewConfig {
-        id: "abc".into(),
-        include: vec![],
-        exclude: vec![],
-        include_all: Some(true),
-    });
+    let config = parse_config(
+        "
+view: test
+    include-all true
+",
+    );
+    let view = View::new(config);
 
     // Assert that is not *explicitly* included
     assert!(view.is_remote_explicitly_included("remote-b"));
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2025-11-17 14:12 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-11-17 14:11 [pdm-devel] [PATCH datacenter-manager 0/3] views: preparations for regex/glob, include-all param Lukas Wagner
2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 1/3] pdm-api-types: views: preparations for future glob/regex support Lukas Wagner
2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 2/3] views: add 'include-all' param; change semantics when there are no includes Lukas Wagner
2025-11-17 14:11 ` [pdm-devel] [PATCH datacenter-manager 3/3] views: tests: use full section-config format for test cases Lukas Wagner

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal