public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 1/3] pdm-api-types: views: preparations for future glob/regex support
Date: Mon, 17 Nov 2025 15:11:20 +0100	[thread overview]
Message-ID: <20251117141122.328559-2-l.wagner@proxmox.com> (raw)
In-Reply-To: <20251117141122.328559-1-l.wagner@proxmox.com>

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


  reply	other threads:[~2025-11-17 14:11 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251117141122.328559-2-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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