From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v3 04/11] views: add implementation for view resource filtering
Date: Thu, 6 Nov 2025 14:43:46 +0100 [thread overview]
Message-ID: <20251106134353.263598-5-l.wagner@proxmox.com> (raw)
In-Reply-To: <20251106134353.263598-1-l.wagner@proxmox.com>
This commit adds the resource filter implementation for the previously
defined ViewConfig type.
There are include/exclude rules for the following properties:
- (global) resource-id
- resource pool
- resource type
- remote
- tags
The rules are interpreted as follows:
- no rules: everything matches
- only includes: included resources match
- only excluded: everything *but* the excluded resources match
- include and exclude: excludes are applied *after* includes, meaning if
one has a `include-remote foo` and `exclude-remote foo` at the same
time, the remote `foo` will never match
Some experiments were performed with a cache that works roughly as following:
- HashMap<ViewId, HashMap<ResourceId, bool>> in a mutex
- Cache invalidated if view config digest changed
- Cache invalidated if certain resource fields such as tags or resource pools change
from the last time (also with a digest-based implementation)
Experimented with the `fake-remote` feature and and 15000 guests showed
that caching was only faster than direct evaluation if the number of
rules in the ViewConfig is *huge* (e.g. >1000 `include-resource-id`
entries). But even for those, direct evaluation was always plenty fast,
with evaluation times ~20ms for *all* resources.
-> for any *realistic* view config, we should be good with direct
evaluation, as long as we don't add any filter rules which are very
expensive to evaluate.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
Notes:
Changes since v2:
- add is_remote_explicitly_included helper
- merge commit that added tests into this one
- simply `check_rules` using .any on the rules vec
- add get_optional_view helper
- rename ViewFilter* -> View*
server/src/lib.rs | 1 +
server/src/views/mod.rs | 205 +++++++++++++
server/src/views/tests.rs | 619 ++++++++++++++++++++++++++++++++++++++
3 files changed, 825 insertions(+)
create mode 100644 server/src/views/mod.rs
create mode 100644 server/src/views/tests.rs
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 964807eb..0f25aa71 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -12,6 +12,7 @@ pub mod remote_tasks;
pub mod remote_updates;
pub mod resource_cache;
pub mod task_utils;
+pub mod views;
pub mod connection;
pub mod pbs_client;
diff --git a/server/src/views/mod.rs b/server/src/views/mod.rs
new file mode 100644
index 00000000..b5d79e0e
--- /dev/null
+++ b/server/src/views/mod.rs
@@ -0,0 +1,205 @@
+use anyhow::{format_err, Error};
+
+use pdm_api_types::{
+ resource::{Resource, ResourceType},
+ views::{FilterRule, ViewConfig, ViewConfigEntry},
+};
+
+#[cfg(test)]
+mod tests;
+
+/// Get view with a given ID.
+///
+/// Returns an error if the view configuration file could not be read, or
+/// if the view with the provided ID does not exist.
+pub fn get_view(view_id: &str) -> Result<View, Error> {
+ let config = pdm_config::views::config()?;
+
+ let entry = config
+ .get(view_id)
+ .cloned()
+ .ok_or_else(|| format_err!("unknown view: {view_id}"))?;
+
+ match entry {
+ ViewConfigEntry::View(view_config) => Ok(View::new(view_config)),
+ }
+}
+
+/// Get (optional) view with a given ID.
+///
+/// Returns an error if the view configuration file could not be read, or
+/// if the view with the provided ID does not exist.
+pub fn get_optional_view(view_id: Option<&str>) -> Result<Option<View>, Error> {
+ view_id.map(get_view).transpose()
+}
+
+/// View implementation.
+///
+/// Given a [`ViewConfig`], this struct can be used to check if a resource/remote/node
+/// matches the include/exclude rules.
+#[derive(Clone)]
+pub struct View {
+ config: ViewConfig,
+}
+
+impl View {
+ /// Create a new [`View`].
+ pub fn new(config: ViewConfig) -> Self {
+ Self { config }
+ }
+
+ /// Check if a [`Resource`] matches the filter rules.
+ pub fn resource_matches(&self, remote: &str, resource: &Resource) -> bool {
+ // NOTE: Establishing a cache here is not worth the effort at the moment, evaluation of
+ // rules is *very* fast.
+
+ let resource_data = resource.into();
+
+ self.check_if_included(remote, &resource_data)
+ && !self.check_if_excluded(remote, &resource_data)
+ }
+
+ /// Check if a remote can be safely skipped based on the filter rule definition.
+ ///
+ /// 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 == 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
+ }
+
+ /// Check if a remote is *explicitly* included (and not excluded).
+ ///
+ /// 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 matches_exclude_remote = self
+ .config
+ .exclude
+ .iter()
+ .any(|rule| Self::matches_remote_rule(remote, rule));
+
+ matches_include_remote && !matches_exclude_remote
+ }
+
+ /// Check if a node is matched by the filter rules.
+ ///
+ /// This is equivalent to checking an actual node resource.
+ pub fn is_node_included(&self, remote: &str, node: &str) -> bool {
+ let resource_data = ResourceData {
+ resource_type: ResourceType::Node,
+ tags: None,
+ resource_pool: None,
+ resource_id: &format!("remote/{remote}/node/{node}"),
+ };
+
+ self.check_if_included(remote, &resource_data)
+ && !self.check_if_excluded(remote, &resource_data)
+ }
+
+ /// Returns the name of the view.
+ pub fn name(&self) -> &str {
+ &self.config.id
+ }
+
+ 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)
+ return true;
+ }
+
+ check_rules(&self.config.include, remote, resource)
+ }
+
+ fn check_if_excluded(&self, remote: &str, resource: &ResourceData) -> bool {
+ check_rules(&self.config.exclude, remote, resource)
+ }
+
+ fn matches_remote_rule(remote: &str, rule: &FilterRule) -> bool {
+ if let FilterRule::Remote(r) = rule {
+ r == remote
+ } else {
+ false
+ }
+ }
+}
+
+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)
+ } else {
+ false
+ }
+ }
+ FilterRule::Remote(included_remote) => included_remote == remote,
+ })
+}
+
+struct ResourceData<'a> {
+ resource_type: ResourceType,
+ tags: Option<&'a [String]>,
+ resource_pool: Option<&'a String>,
+ resource_id: &'a str,
+}
+
+impl<'a> From<&'a Resource> for ResourceData<'a> {
+ fn from(value: &'a Resource) -> Self {
+ match value {
+ Resource::PveQemu(resource) => ResourceData {
+ resource_type: value.resource_type(),
+ tags: Some(&resource.tags),
+ resource_pool: Some(&resource.pool),
+ resource_id: value.global_id(),
+ },
+ Resource::PveLxc(resource) => ResourceData {
+ resource_type: value.resource_type(),
+ tags: Some(&resource.tags),
+ resource_pool: Some(&resource.pool),
+ resource_id: value.global_id(),
+ },
+ Resource::PveNode(_)
+ | Resource::PveSdn(_)
+ | Resource::PbsNode(_)
+ | Resource::PbsDatastore(_)
+ | Resource::PveStorage(_) => ResourceData {
+ resource_type: value.resource_type(),
+ tags: None,
+ resource_pool: None,
+ resource_id: value.global_id(),
+ },
+ }
+ }
+}
diff --git a/server/src/views/tests.rs b/server/src/views/tests.rs
new file mode 100644
index 00000000..030b7994
--- /dev/null
+++ b/server/src/views/tests.rs
@@ -0,0 +1,619 @@
+use pdm_api_types::{
+ resource::{PveLxcResource, PveQemuResource, PveStorageResource, Resource, ResourceType},
+ views::{FilterRule, ViewConfig},
+};
+
+use super::View;
+
+fn make_storage_resource(remote: &str, node: &str, storage_name: &str) -> Resource {
+ Resource::PveStorage(PveStorageResource {
+ disk: 1000,
+ maxdisk: 2000,
+ id: format!("remote/{remote}/storage/{node}/{storage_name}"),
+ storage: storage_name.into(),
+ node: node.into(),
+ status: "available".into(),
+ })
+}
+
+fn make_qemu_resource(
+ remote: &str,
+ node: &str,
+ vmid: u32,
+ pool: Option<&str>,
+ tags: &[&str],
+) -> Resource {
+ Resource::PveQemu(PveQemuResource {
+ disk: 1000,
+ maxdisk: 2000,
+ id: format!("remote/{remote}/guest/{vmid}"),
+ node: node.into(),
+ status: "available".into(),
+ cpu: 0.0,
+ maxcpu: 0.0,
+ maxmem: 1024,
+ mem: 512,
+ name: format!("vm-{vmid}"),
+ // TODO: Check the API type - i guess it should be an option?
+ pool: pool.map_or_else(String::new, |a| a.into()),
+ tags: tags.iter().map(|tag| String::from(*tag)).collect(),
+ template: false,
+ uptime: 1337,
+ vmid,
+ })
+}
+
+fn make_lxc_resource(
+ remote: &str,
+ node: &str,
+ vmid: u32,
+ pool: Option<&str>,
+ tags: &[&str],
+) -> Resource {
+ Resource::PveLxc(PveLxcResource {
+ disk: 1000,
+ maxdisk: 2000,
+ id: format!("remote/{remote}/guest/{vmid}"),
+ node: node.into(),
+ status: "available".into(),
+ cpu: 0.0,
+ maxcpu: 0.0,
+ maxmem: 1024,
+ mem: 512,
+ name: format!("vm-{vmid}"),
+ // TODO: Check the API type - i guess it should be an option?
+ pool: pool.map_or_else(String::new, |a| a.into()),
+ tags: tags.iter().map(|tag| String::from(*tag)).collect(),
+ template: false,
+ uptime: 1337,
+ vmid,
+ })
+}
+
+fn run_test(config: ViewConfig, tests: &[((&str, &Resource), bool)]) {
+ let filter = View::new(config);
+
+ for ((remote_name, resource), expected) in tests {
+ eprintln!("remote: {remote_name}, resource: {resource:?}");
+ assert_eq!(filter.resource_matches(remote_name, resource), *expected);
+ }
+}
+
+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("remote-a".into()),
+ FilterRule::Remote("remote-b".into()),
+ ],
+ ..Default::default()
+ };
+ run_test(
+ config.clone(),
+ &[
+ (
+ (
+ "remote-a",
+ &make_storage_resource("remote-a", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-b",
+ &make_storage_resource("remote-b", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-c",
+ &make_storage_resource("remote-c", NODE, STORAGE),
+ ),
+ false,
+ ),
+ ],
+ );
+
+ let view = View::new(config);
+
+ assert!(!view.can_skip_remote("remote-a"));
+ assert!(!view.can_skip_remote("remote-b"));
+ assert!(view.can_skip_remote("remote-c"));
+}
+
+#[test]
+fn exclude_remotes() {
+ let config = ViewConfig {
+ id: "only-excludes".into(),
+ exclude: vec![
+ FilterRule::Remote("remote-a".into()),
+ FilterRule::Remote("remote-b".into()),
+ ],
+ ..Default::default()
+ };
+
+ run_test(
+ config.clone(),
+ &[
+ (
+ (
+ "remote-a",
+ &make_storage_resource("remote-a", NODE, STORAGE),
+ ),
+ false,
+ ),
+ (
+ (
+ "remote-b",
+ &make_storage_resource("remote-b", NODE, STORAGE),
+ ),
+ false,
+ ),
+ (
+ (
+ "remote-c",
+ &make_storage_resource("remote-c", NODE, STORAGE),
+ ),
+ true,
+ ),
+ ],
+ );
+
+ let view = View::new(config);
+
+ assert!(view.can_skip_remote("remote-a"));
+ assert!(view.can_skip_remote("remote-b"));
+ assert!(!view.can_skip_remote("remote-c"));
+}
+
+#[test]
+fn include_exclude_remotes() {
+ let config = ViewConfig {
+ id: "both".into(),
+ include: vec![
+ FilterRule::Remote("remote-a".into()),
+ FilterRule::Remote("remote-b".into()),
+ ],
+ exclude: vec![
+ FilterRule::Remote("remote-b".into()),
+ FilterRule::Remote("remote-c".into()),
+ ],
+ };
+ run_test(
+ config.clone(),
+ &[
+ (
+ (
+ "remote-a",
+ &make_storage_resource("remote-a", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-b",
+ &make_storage_resource("remote-b", NODE, STORAGE),
+ ),
+ false,
+ ),
+ (
+ (
+ "remote-c",
+ &make_storage_resource("remote-c", NODE, STORAGE),
+ ),
+ false,
+ ),
+ ],
+ );
+
+ let view = View::new(config);
+
+ assert!(!view.can_skip_remote("remote-a"));
+ assert!(view.can_skip_remote("remote-b"));
+ assert!(view.can_skip_remote("remote-c"));
+ assert!(view.can_skip_remote("remote-d"));
+}
+
+#[test]
+fn empty_config() {
+ let config = ViewConfig {
+ id: "empty".into(),
+ ..Default::default()
+ };
+ run_test(
+ config.clone(),
+ &[
+ (
+ (
+ "remote-a",
+ &make_storage_resource("remote-a", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-b",
+ &make_storage_resource("remote-b", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (
+ "remote-c",
+ &make_storage_resource("remote-c", NODE, STORAGE),
+ ),
+ true,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ true,
+ ),
+ ],
+ );
+
+ let view = View::new(config);
+
+ assert!(!view.can_skip_remote("remote-a"));
+ assert!(!view.can_skip_remote("remote-b"));
+ assert!(!view.can_skip_remote("remote-c"));
+}
+
+#[test]
+fn include_type() {
+ run_test(
+ ViewConfig {
+ id: "include-resource-type".into(),
+ include: vec![
+ FilterRule::ResourceType(ResourceType::PveStorage),
+ FilterRule::ResourceType(ResourceType::PveQemu),
+ ],
+ ..Default::default()
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ true,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ true,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])),
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn exclude_type() {
+ run_test(
+ ViewConfig {
+ id: "exclude-resource-type".into(),
+ exclude: vec![
+ FilterRule::ResourceType(ResourceType::PveStorage),
+ FilterRule::ResourceType(ResourceType::PveQemu),
+ ],
+ ..Default::default()
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ false,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ false,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])),
+ true,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn include_exclude_type() {
+ run_test(
+ ViewConfig {
+ id: "exclude-resource-type".into(),
+ include: vec![FilterRule::ResourceType(ResourceType::PveQemu)],
+ exclude: vec![FilterRule::ResourceType(ResourceType::PveStorage)],
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ false,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ true,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])),
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn include_exclude_tags() {
+ run_test(
+ ViewConfig {
+ id: "include-tags".into(),
+ include: vec![
+ FilterRule::Tag("tag1".to_string()),
+ FilterRule::Tag("tag2".to_string()),
+ ],
+ exclude: vec![FilterRule::Tag("tag3".to_string())],
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ // only qemu/lxc can match tags for now
+ false,
+ ),
+ (
+ (
+ REMOTE,
+ &make_qemu_resource(REMOTE, NODE, 100, None, &["tag1", "tag3"]),
+ ),
+ // because tag3 is excluded
+ false,
+ ),
+ (
+ (
+ REMOTE,
+ &make_lxc_resource(REMOTE, NODE, 101, None, &["tag1"]),
+ ),
+ // matches since it's in the includes
+ true,
+ ),
+ (
+ (
+ REMOTE,
+ &make_lxc_resource(REMOTE, NODE, 102, None, &["tag4"]),
+ ),
+ // Not in includes, can never match
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn include_exclude_resource_pool() {
+ run_test(
+ ViewConfig {
+ id: "pools".into(),
+ include: vec![
+ FilterRule::ResourcePool("pool1".to_string()),
+ FilterRule::ResourcePool("pool2".to_string()),
+ ],
+ exclude: vec![FilterRule::ResourcePool("pool2".to_string())],
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ // only qemu/lxc can match pools for now
+ false,
+ ),
+ (
+ (
+ REMOTE,
+ &make_qemu_resource(REMOTE, NODE, 100, Some("pool2"), &[]),
+ ),
+ // because pool2 is excluded (takes precedence over includes)
+ false,
+ ),
+ (
+ (
+ REMOTE,
+ &make_lxc_resource(REMOTE, NODE, 101, Some("pool1"), &[]),
+ ),
+ // matches since it's in the includes
+ true,
+ ),
+ (
+ (
+ REMOTE,
+ &make_lxc_resource(REMOTE, NODE, 102, Some("pool4"), &[]),
+ ),
+ // Not in includes, can never match
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn include_exclude_resource_id() {
+ run_test(
+ ViewConfig {
+ id: "resource-id".into(),
+ include: vec![
+ FilterRule::ResourceId(format!("remote/{REMOTE}/guest/100")),
+ FilterRule::ResourceId(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")),
+ ],
+ },
+ &[
+ (
+ (REMOTE, &make_storage_resource(REMOTE, NODE, STORAGE)),
+ true,
+ ),
+ (
+ (REMOTE, &make_qemu_resource(REMOTE, NODE, 100, None, &[])),
+ true,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 101, None, &[])),
+ false,
+ ),
+ (
+ (REMOTE, &make_lxc_resource(REMOTE, NODE, 102, None, &[])),
+ false,
+ ),
+ (
+ (
+ "otherremote",
+ &make_lxc_resource("otherremote", NODE, 101, None, &[]),
+ ),
+ false,
+ ),
+ (
+ (
+ "yetanoterremote",
+ &make_lxc_resource("yetanotherremote", NODE, 104, None, &[]),
+ ),
+ false,
+ ),
+ ],
+ );
+}
+
+#[test]
+fn node_included() {
+ let view = View::new(ViewConfig {
+ id: "both".into(),
+
+ include: vec![
+ FilterRule::Remote("remote-a".to_string()),
+ FilterRule::ResourceId("remote/someremote/node/test".to_string()),
+ ],
+ exclude: vec![FilterRule::Remote("remote-b".to_string())],
+ });
+
+ assert!(view.is_node_included("remote-a", "somenode"));
+ assert!(view.is_node_included("remote-a", "somenode2"));
+ assert!(!view.is_node_included("remote-b", "somenode"));
+ assert!(!view.is_node_included("remote-b", "somenode2"));
+ assert!(view.is_node_included("someremote", "test"));
+
+ assert_eq!(view.name(), "both");
+}
+
+#[test]
+fn can_skip_remote_if_excluded() {
+ let view = View::new(ViewConfig {
+ id: "abc".into(),
+ include: vec![],
+ exclude: vec![FilterRule::Remote("remote-b".to_string())],
+ });
+
+ assert!(!view.can_skip_remote("remote-a"));
+ assert!(view.can_skip_remote("remote-b"));
+}
+
+#[test]
+fn can_skip_remote_if_included() {
+ let view = View::new(ViewConfig {
+ id: "abc".into(),
+ include: vec![FilterRule::Remote("remote-b".to_string())],
+ exclude: vec![],
+ });
+
+ assert!(!view.can_skip_remote("remote-b"));
+ assert!(view.can_skip_remote("remote-a"));
+}
+
+#[test]
+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()),
+ ],
+ exclude: vec![],
+ });
+
+ assert!(!view.can_skip_remote("remote-b"));
+ assert!(!view.can_skip_remote("remote-a"));
+}
+
+#[test]
+fn can_skip_remote_explicit_remote_exclude() {
+ let view = View::new(ViewConfig {
+ id: "abc".into(),
+ include: vec![FilterRule::ResourceId(
+ "resource/remote-a/guest/100".to_string(),
+ )],
+ exclude: vec![FilterRule::Remote("remote-a".to_string())],
+ });
+
+ assert!(view.can_skip_remote("remote-a"));
+}
+
+#[test]
+fn can_skip_remote_with_empty_config() {
+ let view = View::new(ViewConfig {
+ id: "abc".into(),
+ include: vec![],
+ exclude: vec![],
+ });
+
+ assert!(!view.can_skip_remote("remote-a"));
+ assert!(!view.can_skip_remote("remote-b"));
+}
+
+#[test]
+fn can_skip_remote_with_no_remote_includes() {
+ let view = View::new(ViewConfig {
+ id: "abc".into(),
+ include: vec![FilterRule::ResourceId(
+ "resource/remote-a/guest/100".to_string(),
+ )],
+ exclude: vec![],
+ });
+
+ assert!(!view.can_skip_remote("remote-a"));
+ assert!(!view.can_skip_remote("remote-b"));
+}
+
+#[test]
+fn explicitly_included_remote() {
+ let view = View::new(ViewConfig {
+ id: "abc".into(),
+ include: vec![FilterRule::Remote("remote-b".to_string())],
+ exclude: vec![],
+ });
+
+ 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("remote-b".to_string())],
+ exclude: vec![FilterRule::Remote("remote-b".to_string())],
+ });
+
+ 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![],
+ });
+
+ // 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
next prev parent reply other threads:[~2025-11-06 13:43 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-11-06 13:43 [pdm-devel] [PATCH datacenter-manager v3 00/11] backend implementation for view filters Lukas Wagner
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 01/11] pdm-api-types: views: add ViewConfig type Lukas Wagner
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 02/11] pdm-config: views: add support for views Lukas Wagner
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 03/11] acl: add '/view' and '/view/{view-id}' as allowed ACL paths Lukas Wagner
2025-11-06 13:43 ` Lukas Wagner [this message]
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 05/11] api: resources: list: add support for view parameter Lukas Wagner
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 06/11] api: resources: top entities: " Lukas Wagner
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 07/11] api: resources: status: " Lukas Wagner
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 08/11] api: subscription " Lukas Wagner
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 09/11] api: remote-tasks: " Lukas Wagner
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 10/11] pdm-client: resource list: add view-filter parameter Lukas Wagner
2025-11-06 13:43 ` [pdm-devel] [PATCH datacenter-manager v3 11/11] pdm-client: top entities: " 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=20251106134353.263598-5-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