all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: "Lukas Wagner" <l.wagner@proxmox.com>
To: "Dominik Csapak" <d.csapak@proxmox.com>,
	"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, 05 Nov 2025 11:58:05 +0100	[thread overview]
Message-ID: <DE0PHQIDCL1P.3VR35J8P02V05@proxmox.com> (raw)
In-Reply-To: <a9e4fe9a-3ecd-4a11-ad3e-e72c846b72b7@proxmox.com>

On Wed Nov 5, 2025 at 11:08 AM CET, Dominik Csapak wrote:
>> +
>> +/// 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)

Personally I'm not the biggest fan of newtypes unless I'm 100%
convinced that the type is only ever going to contain this single
member (simple example: pub struct UnixTimestamp(i64)), just to avoid
having to change any code when I have to introduce another member (e.g.
changing self.0 to self.config). Since it's only a very minor syntactic
thing, I'd be tempted to leave it as is - unless you insist I change it.

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

I want to shield callers from the ViewFilterConfig itself (also the
reason why the `get_view_filter` fn exist, instead of having the caller
call directly into pdm_config) - so I'd prefer to keep this struct.

> 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)
>

I'll move it to the commit message and leave a short summary in the
comment - thanks!

>> +
>> +        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?
>

You are absolutely right - I'll change it as suggested.

>> +}
>> +
>> +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...

The sole reason why there is this 'itermediary' type is the
'is_node_included' function. It is used when we don't handle resources,
but only really care about the node (e.g. task API). I didn't want to
'synthesize' some fake PveNode resource in the 'is_node_included'
function; it felt kind of wrong.


_______________________________________________
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:57 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
2025-11-05 10:58     ` Lukas Wagner [this message]
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=DE0PHQIDCL1P.3VR35J8P02V05@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=d.csapak@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