all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: Proxmox Datacenter Manager development discussion
	<pdm-devel@lists.proxmox.com>,
	Lukas Wagner <l.wagner@proxmox.com>
Subject: Re: [pdm-devel] [PATCH datacenter-manager v2 04/12] views: add implementation for view filters
Date: Wed, 5 Nov 2025 11:08:00 +0100	[thread overview]
Message-ID: <a9e4fe9a-3ecd-4a11-ad3e-e72c846b72b7@proxmox.com> (raw)
In-Reply-To: <20251103123521.266258-5-l.wagner@proxmox.com>

some comments inline

On 11/3/25 1:36 PM, Lukas Wagner wrote:
> This commit adds the filter implementation for the previously defined
> ViewFilterConfig 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
> 
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
>   server/src/lib.rs               |   1 +
>   server/src/views/mod.rs         |   1 +
>   server/src/views/view_filter.rs | 182 ++++++++++++++++++++++++++++++++
>   3 files changed, 184 insertions(+)
>   create mode 100644 server/src/views/mod.rs
>   create mode 100644 server/src/views/view_filter.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..9a2856a4
> --- /dev/null
> +++ b/server/src/views/mod.rs
> @@ -0,0 +1 @@
> +pub mod view_filter;
> diff --git a/server/src/views/view_filter.rs b/server/src/views/view_filter.rs
> new file mode 100644
> index 00000000..4f77e7bf
> --- /dev/null
> +++ b/server/src/views/view_filter.rs
> @@ -0,0 +1,182 @@
> +use anyhow::Error;
> +
> +use pdm_api_types::{
> +    resource::{Resource, ResourceType},
> +    views::{FilterRule, ViewFilterConfig},
> +};
> +
> +/// Get view filter with a given ID.
> +///
> +/// Returns an error if the view filter configuration file could not be read, or
> +/// if the view filter with the provided ID does not exist.
> +pub fn get_view_filter(filter_id: &str) -> Result<ViewFilter, Error> {
> +    pdm_config::views::get_view_filter_config(filter_id).map(ViewFilter::new)
> +}
> +
> +/// View filter implementation.
> +///
> +/// Given a [`ViewFilterConfig`], this struct can be used to check if a resource/remote/node
> +/// matches the filter rules.
> +#[derive(Clone)]
> +pub struct ViewFilter {
> +    config: ViewFilterConfig,
> +}

wouldn't a newtype suffice here too?

pub struct ViewFilter(ViewFilterConfig)

? alternatively, what about having freestanding functions that
take a `&ViewFilterConfig` as parameter ?

If we're doing it this way though, I'd rather implement a
From<ViewFilterConfig> for ViewFilter than a `new` method
(or maybe both)

> +
> +impl ViewFilter {
> +    /// Create a new [`ViewFiler`].
> +    pub fn new(config: ViewFilterConfig) -> 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.
> +        //
> +        // Some experiments were performed with a cache that works roughly as following:
> +        //   - HashMap<ViewId, HashMap<ResourceId, bool>> in a mutex
> +        //   - Cache invalidated if view-filter 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
> +        // ViewFilterConfig 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* filter config, we should be good with direct evaluation, as long
> +        // as we don't add any filter rules which are very expensive to evaluate.

isn't that (full) info more suited for the commit message than a  comment?

e.g. a single line comment with 'caching here is currently not worth it'
and the full text in the commit message should also be ok?

(no hard feelings though)

> +
> +        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(|e| {
> +            if let FilterRule::Remote(r) = e {
> +                r == remote
> +            } else {
> +                false
> +            }
> +        });
> +
> +        (has_any_include_remote && !matches_any_include_remote && !any_other)
> +            || matches_any_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 filter.
> +    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 check_rules(rules: &[FilterRule], remote: &str, resource: &ResourceData) -> bool {
> +    for rule in rules {
> +        let verdict = 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,
> +        };
> +
> +        if verdict {
> +            return true;
> +        }
> +    }
> +
> +    false

wouldn't this boil down to:

return rules.any(|rule| match rule { ... } ) ?

instead of looping and doing an early return manually?

> +}
> +
> +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(),
> +            },
> +        }
> +    }
> +}

Is it really worht it, to define a seperate type that you only use 
internally?
couldn't you simple use the &Resource type directly?
or maybe just having 2 helper methods to extract the relevant info?
(the type and global_id is already abstracted, so it's only relevant
for the tags and the resource_pool ?)

but i guess i'd have to see it to determine what is better...



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


  reply	other threads:[~2025-11-05 10:07 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-03 12:35 [pdm-devel] [PATCH datacenter-manager v2 00/12] backend " Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 01/12] pdm-api-types: views: add ViewFilterConfig type Lukas Wagner
2025-11-05 10:07   ` Dominik Csapak
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 02/12] pdm-config: views: add support for view-filters Lukas Wagner
2025-11-05 10:07   ` Dominik Csapak
2025-11-05 10:37     ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 03/12] acl: add '/view' and '/view/{view-id}' as allowed ACL paths Lukas Wagner
2025-11-05 10:07   ` Dominik Csapak
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 04/12] views: add implementation for view filters Lukas Wagner
2025-11-05 10:08   ` Dominik Csapak [this message]
2025-11-05 10:58     ` Lukas Wagner
2025-11-05 11:48       ` Dominik Csapak
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 05/12] views: add tests for view filter implementation Lukas Wagner
2025-11-05 10:08   ` Dominik Csapak
2025-11-05 10:58     ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 06/12] api: resources: list: add support for view-filter parameter Lukas Wagner
2025-11-05 10:08   ` Dominik Csapak
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 07/12] api: resources: top entities: " Lukas Wagner
2025-11-05 10:08   ` Dominik Csapak
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 08/12] api: resources: status: " Lukas Wagner
2025-11-05 10:08   ` Dominik Csapak
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 09/12] api: subscription " Lukas Wagner
2025-11-05 10:08   ` Dominik Csapak
2025-11-05 14:11     ` Lukas Wagner
2025-11-05 14:28       ` Dominik Csapak
2025-11-05 14:35         ` Lukas Wagner
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 10/12] api: remote-tasks: " Lukas Wagner
2025-11-05 10:09   ` Dominik Csapak
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 11/12] pdm-client: resource list: add " Lukas Wagner
2025-11-05 10:11   ` Dominik Csapak
2025-11-03 12:35 ` [pdm-devel] [PATCH datacenter-manager v2 12/12] pdm-client: top entities: " Lukas Wagner
2025-11-05 10:12   ` Dominik Csapak

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=a9e4fe9a-3ecd-4a11-ad3e-e72c846b72b7@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal