Skip to content

Commit 371a56b

Browse files
authored
Merge pull request #174 from evakhoni/lease-labels-fix
Fix lease labels over-filtering
2 parents ae2fef4 + 71c91a3 commit 371a56b

4 files changed

Lines changed: 47 additions & 2 deletions

File tree

e2e/tests.bats

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,33 @@ EOF
185185
assert_success
186186
assert_output "No resources found."
187187

188+
# Test complex selectors with matchExpressions (regression test for server-side over-filtering)
189+
# Use 'sa' exporter since 'oidc' is already leased above. The '!nonexistent' is a matchExpression
190+
# that will always be true (label doesn't exist on exporters), allowing the lease to match.
191+
jmp create lease --selector 'example.com/board=sa,!nonexistent' --duration 1d
192+
193+
# Partial match: filter with just matchLabels (subset) → expecting a match
194+
run jmp get leases --selector 'example.com/board=sa' -o yaml
195+
assert_success
196+
assert_output --partial "example.com/board=sa"
197+
198+
# Partial match: filter with just matchExpressions (subset) → expecting a match
199+
# This specifically tests client-side filtering of matchExpressions
200+
run jmp get leases --selector '!nonexistent' -o yaml
201+
assert_success
202+
assert_output --partial "!nonexistent"
203+
204+
# Non-matching matchExpressions → expecting no match with current implementation
205+
# where we're filtering against the original lease request
206+
run jmp get leases --selector 'example.com/board=sa,!production'
207+
assert_success
208+
assert_output "No resources found."
209+
210+
# Filter asks for more than lease has → expecting no match
211+
run jmp get leases --selector 'example.com/board=sa,!nonexistent,region=us'
212+
assert_success
213+
assert_output "No resources found."
214+
188215
jmp delete leases --all
189216
}
190217

python/packages/jumpstarter-cli/jumpstarter_cli/delete.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ def delete_leases(config, name: str, selector: str | None, all: bool, output: Ou
3232
names.append(name)
3333
elif selector:
3434
leases = config.list_leases(filter=selector)
35+
# Client-side filtering for matchExpressions (server only filters matchLabels)
36+
leases = leases.filter_by_selector(selector)
3537
for lease in leases.leases:
3638
if lease.client == config.metadata.name:
3739
names.append(lease.name)

python/packages/jumpstarter/jumpstarter/client/grpc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from jumpstarter_protocol import client_pb2, client_pb2_grpc, jumpstarter_pb2_grpc, kubernetes_pb2, router_pb2_grpc
1414
from pydantic import BaseModel, ConfigDict, Field, field_serializer
1515

16-
from jumpstarter.client.selectors import selector_contains
16+
from jumpstarter.client.selectors import extract_match_labels_filter, selector_contains
1717
from jumpstarter.common.grpc import translate_grpc_exceptions
1818

1919

@@ -383,7 +383,7 @@ async def ListLeases(
383383
parent="namespaces/{}".format(self.namespace),
384384
page_size=page_size,
385385
page_token=page_token,
386-
filter=filter,
386+
filter=extract_match_labels_filter(filter),
387387
only_active=only_active,
388388
)
389389
)

python/packages/jumpstarter/jumpstarter/client/selectors.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ def parse_label_selector(selector: str) -> tuple[dict[str, str], list[tuple[str,
5151
return match_labels, match_expressions
5252

5353

54+
def extract_match_labels_filter(selector: str | None) -> str | None:
55+
"""Extract only the matchLabels portion from a selector string.
56+
57+
This is used to send only the server-filterable portion to the server,
58+
since matchExpressions can't be matched against metadata.labels.
59+
"""
60+
if not selector:
61+
return None
62+
match_labels, _ = parse_label_selector(selector)
63+
if not match_labels:
64+
return None
65+
# Format matchLabels dict back to a selector string.
66+
# Example: {"board": "rpi", "env": "test"} -> "board=rpi,env=test"
67+
return ",".join(f"{k}={v}" for k, v in match_labels.items())
68+
69+
5470
def selector_contains(selector: str, requirements: str) -> bool:
5571
"""Check if selector contains all criteria from requirements.
5672

0 commit comments

Comments
 (0)