Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions e2e/tests.bats
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,33 @@ EOF
assert_success
assert_output "No resources found."

# Test complex selectors with matchExpressions (regression test for server-side over-filtering)
# Use 'sa' exporter since 'oidc' is already leased above. The '!nonexistent' is a matchExpression
# that will always be true (label doesn't exist on exporters), allowing the lease to match.
jmp create lease --selector 'example.com/board=sa,!nonexistent' --duration 1d

# Partial match: filter with just matchLabels (subset) → expecting a match
run jmp get leases --selector 'example.com/board=sa' -o yaml
assert_success
assert_output --partial "example.com/board=sa"

# Partial match: filter with just matchExpressions (subset) → expecting a match
# This specifically tests client-side filtering of matchExpressions
run jmp get leases --selector '!nonexistent' -o yaml
assert_success
assert_output --partial "!nonexistent"

# Non-matching matchExpressions → expecting no match with current implementation
# where we're filtering against the original lease request
run jmp get leases --selector 'example.com/board=sa,!production'
assert_success
assert_output "No resources found."

# Filter asks for more than lease has → expecting no match
run jmp get leases --selector 'example.com/board=sa,!nonexistent,region=us'
assert_success
assert_output "No resources found."

jmp delete leases --all
}

Expand Down
2 changes: 2 additions & 0 deletions python/packages/jumpstarter-cli/jumpstarter_cli/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ def delete_leases(config, name: str, selector: str | None, all: bool, output: Ou
names.append(name)
elif selector:
leases = config.list_leases(filter=selector)
# Client-side filtering for matchExpressions (server only filters matchLabels)
leases = leases.filter_by_selector(selector)
for lease in leases.leases:
if lease.client == config.metadata.name:
names.append(lease.name)
Expand Down
4 changes: 2 additions & 2 deletions python/packages/jumpstarter/jumpstarter/client/grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from jumpstarter_protocol import client_pb2, client_pb2_grpc, jumpstarter_pb2_grpc, kubernetes_pb2, router_pb2_grpc
from pydantic import BaseModel, ConfigDict, Field, field_serializer

from jumpstarter.client.selectors import selector_contains
from jumpstarter.client.selectors import extract_match_labels_filter, selector_contains
from jumpstarter.common.grpc import translate_grpc_exceptions


Expand Down Expand Up @@ -383,7 +383,7 @@ async def ListLeases(
parent="namespaces/{}".format(self.namespace),
page_size=page_size,
page_token=page_token,
filter=filter,
filter=extract_match_labels_filter(filter),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
only_active=only_active,
)
)
Expand Down
16 changes: 16 additions & 0 deletions python/packages/jumpstarter/jumpstarter/client/selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ def parse_label_selector(selector: str) -> tuple[dict[str, str], list[tuple[str,
return match_labels, match_expressions


def extract_match_labels_filter(selector: str | None) -> str | None:
"""Extract only the matchLabels portion from a selector string.

This is used to send only the server-filterable portion to the server,
since matchExpressions can't be matched against metadata.labels.
"""
if not selector:
return None
match_labels, _ = parse_label_selector(selector)
if not match_labels:
return None
# Format matchLabels dict back to a selector string.
# Example: {"board": "rpi", "env": "test"} -> "board=rpi,env=test"
return ",".join(f"{k}={v}" for k, v in match_labels.items())


def selector_contains(selector: str, requirements: str) -> bool:
"""Check if selector contains all criteria from requirements.

Expand Down
Loading