Skip to content

Fix: Empty selector default to match nothing (#37)#76

Merged
damsallem merged 1 commit intocriteo:mainfrom
ayoub3bidi:fix/empty-selector-default-to-match-all
Jan 12, 2026
Merged

Fix: Empty selector default to match nothing (#37)#76
damsallem merged 1 commit intocriteo:mainfrom
ayoub3bidi:fix/empty-selector-default-to-match-all

Conversation

@ayoub3bidi
Copy link
Contributor

Fixes #37

Problem

When using label selectors in disruption budgets, the current implementation treats empty selectors as matching all resources. This behavior is unintuitive and potentially dangerous, as users would expect an empty selector to match nothing rather than everything.

The issue occurs because the code was checking selector.Empty() after converting the label selector, which in Kubernetes semantics means "match all resources" rather than "match no resources".

Root Cause

The problematic pattern was present in multiple locations:

selector, err := metav1.LabelSelectorAsSelector(&podSelector)
if err != nil || selector.Empty() {
    return nodeSet, err
}

When a LabelSelector has no MatchLabels and no MatchExpressions, the converted selector would match all resources when passed to Kubernetes API calls, leading to unintended behavior.

Solution

This PR implements explicit checks before selector conversion to ensure empty selectors return empty result sets, preventing them from matching any resources. The check pattern applied is:

if len(selector.MatchLabels) == 0 && len(selector.MatchExpressions) == 0 {
    return nodeSet, nil  // Return empty set
}
selector, err := metav1.LabelSelectorAsSelector(&selector)
if err != nil {
    return nodeSet, err
}

Changes

Core Logic Changes

  1. pkg/resolver/resolver.go

    • GetNodesFromNamespacedPodSelector(): Added empty selector check
    • GetNodesFromNamespacedPVCSelector(): Added empty selector check
    • GetNodeFromNodeSelector(): Added empty selector check
  2. api/v1alpha1/applicationdisruptionbudget_types.go

    • SelectorMatchesObject(): Added empty selector checks for both PodSelector and PVCSelector cases
  3. api/v1alpha1/nodedisruptionbudget_types.go

    • SelectorMatchesObject(): Added empty selector check for NodeSelector case

Test Coverage

Added comprehensive test cases to verify the fix:

  1. ApplicationDisruptionBudget tests

    • Test with both PodSelector and PVCSelector empty
    • Test with only PodSelector empty (partial empty selector)
  2. NodeDisruptionBudget tests

    • Test with empty NodeSelector

All tests verify that:

  • Empty selectors result in zero watched nodes
  • Disruption calculations work correctly with no nodes
  • Budgets with empty selectors do not impact disruption requests

Testing

  • All existing tests continue to pass (24/24 specs)
  • Added 3 new test cases specifically for empty selector scenarios
  • Test coverage maintained at 89.2% for controller logic
  • Verified with go vet and go build

Impact

This change modifies the default behavior of empty selectors from "match all" to "match nothing". While this is technically a breaking change in behavior, it fixes an unintuitive and potentially dangerous default.

The impact is limited to:

  • Disruption budgets with explicitly empty selectors (no labels or expressions defined)
  • Such budgets will now correctly watch no nodes instead of all nodes

Existing budgets with properly defined selectors are unaffected.

Backward Compatibility

Users who relied on empty selectors matching all resources will need to update their configurations. However, this scenario is unlikely as:

  1. The previous behavior was undocumented
  2. It is counter-intuitive (empty selector matching everything)
  3. It poses a safety risk for a controller designed to protect resources

The new behavior aligns with the principle of least surprise and is safer by default.

Checklist

  • Code compiles without errors
  • All tests pass
  • New tests added for edge cases
  • Code formatted with go fmt
  • Code passes go vet
  • No breaking changes to existing functionality
  • Documentation updated (inline comments added)

Copy link
Contributor

@geobeau geobeau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thank you for contributing.
I just have the feeling that relying on Empty() is nicer so let me know if there is a specific reason for it, otherwise I would just use selector.Empty() when possible

@ayoub3bidi ayoub3bidi force-pushed the fix/empty-selector-default-to-match-all branch from 1c426b2 to 0b1a3df Compare December 24, 2025 09:16
@geobeau
Copy link
Contributor

geobeau commented Dec 30, 2025

LGTM, Just waiting for @damsallem validation to merge it

@damsallem
Copy link
Contributor

Thanks a lot for your contribution. Before validating I took some times to validate all our production in term of usage of these selector to make sure nothing was broken.

@damsallem damsallem merged commit da58e9b into criteo:main Jan 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Empty selector default to match all

3 participants

Comments