Skip to content

fix: prevent transfer warning for non-owned leases#570

Open
bennyz wants to merge 3 commits intojumpstarter-dev:mainfrom
bennyz:fix-lease-transfer-error
Open

fix: prevent transfer warning for non-owned leases#570
bennyz wants to merge 3 commits intojumpstarter-dev:mainfrom
bennyz:fix-lease-transfer-error

Conversation

@bennyz
Copy link
Copy Markdown
Member

@bennyz bennyz commented Apr 16, 2026

A user might receive a warning about a lease being transfer, even if the lease was never theirs to begin with:
[04/16/26 13:00:06] INFO [jumpstarter.client.lease] Waiting for ready connection at /var/folders/4m/llxcfbf1431fz8bjyr5cxtgr0000gn/T/jumpstarter-yk94a7bt/socket
WARNING Lease 019d95ac-b0d2-7155-9ee1-60c034b959f9 has been transferred to another client. Your session is no longer valid.
WARNING Lease 019d95ac-b0d2-7155-9ee1-60c034b959f9 has been transferred to another client. Your session is no longer valid.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

Added a client_name field to Lease and enforce ownership validation when reusing existing leases; client config now passes the client name into leases. Minor formatting/log changes and new unit tests; workflow and pytest coverage config adjusted.

Changes

Cohort / File(s) Summary
Lease implementation
python/packages/jumpstarter/jumpstarter/client/lease.py
Added `client_name: str
Client config
python/packages/jumpstarter/jumpstarter/config/client.py
ClientConfigV1Alpha1.lease_async now passes client_name=self.metadata.name when constructing Lease.
Tests for lease behavior
python/packages/jumpstarter/jumpstarter/client/lease_test.py
Added tests covering ownership validation in request_async(), _acquire() retry for NoExporter, and handle_async() dial retry behavior; updated imports and test helpers.
CI workflow
.github/workflows/python-tests.yaml
Removed --cov from PYTEST_ADDOPTS, leaving --cov-report=xml (coverage collection target changed).
Pytest config
python/pyproject.toml
Changed pytest coverage flag to --cov=. to explicitly set coverage target directory.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • mangelajo

Poem

🐇 I nibble code beneath the moon,
A client name now hums in tune.
No mixtaped leases in the lair,
Each hop finds home with tidy care.
🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly relates to the changeset by explaining the problem (confusing warning about lease transfers even when the lease wasn't owned by the user) that the changes address through lease ownership validation.
Title check ✅ Passed The title accurately describes the main change: preventing transfer warnings for non-owned leases by adding ownership validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bennyz bennyz force-pushed the fix-lease-transfer-error branch from 208de76 to 94ab62b Compare April 16, 2026 10:42
bennyz added 3 commits April 16, 2026 14:21
A user might receive a warning about a lease being transfer, even if the lease was never theirs to begin with:
[04/16/26 13:00:06] INFO     [jumpstarter.client.lease] Waiting for ready connection at /var/folders/4m/llxcfbf1431fz8bjyr5cxtgr0000gn/T/jumpstarter-yk94a7bt/socket
                    WARNING  Lease 019d95ac-b0d2-7155-9ee1-60c034b959f9 has been transferred to another client. Your session is no longer valid.
                    WARNING  Lease 019d95ac-b0d2-7155-9ee1-60c034b959f9 has been transferred to another client. Your session is no longer valid.

Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
Assisted-by: claude-opus-4.6
Signed-off by: Benny Zlotnik <bzlotnik@redhat.com>
Assisted-by: claude-opus-4.6
The bare --cov flag produces coverage.xml with an empty <source>
element, making diff-cover unable to match coverage data to git diff
paths. Change to --cov=. so each package's coverage.xml includes the
correct source root, and remove the redundant bare --cov from
PYTEST_ADDOPTS.
@bennyz bennyz force-pushed the fix-lease-transfer-error branch from 94ab62b to e4b080a Compare April 20, 2026 10:43
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
python/packages/jumpstarter/jumpstarter/client/lease_test.py (1)

343-371: Good coverage of the ownership check; consider adding the empty-string case.

The two tests nicely cover "different client" and "client_name is None". Given the production guard uses if self.client_name and ... (truthy check), an additional case where client_name="" would document/lock behavior for a potentially-common misconfiguration (empty metadata.name) and catch any future change to is not None.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter/jumpstarter/client/lease_test.py` around lines
343 - 371, Add a new test in TestRequestAsyncOwnership that covers the
empty-string client_name case: create a lease via _make_lease(client_name=""),
stub lease.get.return_value = Mock(client="other-client", selector=None) and
lease._acquire = AsyncMock(return_value=lease), then await lease.request_async()
and assert it returns the lease (i.e., ownership check is skipped). Name the
test something like test_skips_check_when_client_name_is_empty_string and mark
it with `@pytest.mark.anyio` so it mirrors
test_skips_check_when_client_name_is_none and guards against future changes to
the truthy check in Lease.request_async.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/packages/jumpstarter/jumpstarter/config/client.py`:
- Line 332: The ownership check in Lease.request_async() currently uses a truthy
test on self.client_name which allows an empty string to bypass validation;
update the logic to explicitly reject empty client names by checking that
self.client_name is not None and not an empty string (e.g., self.client_name !=
"") or raise a validation error when metadata.name is empty before passing it to
client_name (where client_name=self.metadata.name is set); ensure the comparison
against existing_lease.client uses the explicit non-empty check so empty
metadata.name cannot reuse a lease.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/client/lease_test.py`:
- Around line 343-371: Add a new test in TestRequestAsyncOwnership that covers
the empty-string client_name case: create a lease via
_make_lease(client_name=""), stub lease.get.return_value =
Mock(client="other-client", selector=None) and lease._acquire =
AsyncMock(return_value=lease), then await lease.request_async() and assert it
returns the lease (i.e., ownership check is skipped). Name the test something
like test_skips_check_when_client_name_is_empty_string and mark it with
`@pytest.mark.anyio` so it mirrors test_skips_check_when_client_name_is_none and
guards against future changes to the truthy check in Lease.request_async.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4dcc6453-6214-4839-ac18-7480728fe0bc

📥 Commits

Reviewing files that changed from the base of the PR and between 208de76 and e4b080a.

📒 Files selected for processing (5)
  • .github/workflows/python-tests.yaml
  • python/packages/jumpstarter/jumpstarter/client/lease.py
  • python/packages/jumpstarter/jumpstarter/client/lease_test.py
  • python/packages/jumpstarter/jumpstarter/config/client.py
  • python/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/packages/jumpstarter/jumpstarter/client/lease.py

release=release_lease,
tls_config=self.tls,
grpc_options=self.grpcOptions,
client_name=self.metadata.name,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm ObjectMeta.name default and any callers that could leave it empty.
rg -nP -C3 '\bclass\s+ObjectMeta\b'
rg -nP -C2 'ObjectMeta\('

Repository: jumpstarter-dev/jumpstarter

Length of output: 36922


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/config/common.py | head -30

Repository: jumpstarter-dev/jumpstarter

Length of output: 650


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/config/client.py | sed -n '320,350p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 1454


🏁 Script executed:

rg -nP 'def request_async|def lease_async' python/packages/jumpstarter/jumpstarter/client/

Repository: jumpstarter-dev/jumpstarter

Length of output: 165


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/client/lease.py | sed -n '166,220p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 2951


🏁 Script executed:

rg -nP 'if.*client_name.*and.*existing_lease' python/packages/jumpstarter/

Repository: jumpstarter-dev/jumpstarter

Length of output: 209


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/config/common.py | sed -n '13,40p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 235


🏁 Script executed:

rg -nP 'class ObjectMeta|name.*=' python/packages/jumpstarter/jumpstarter/config/common.py | head -20

Repository: jumpstarter-dev/jumpstarter

Length of output: 105


🏁 Script executed:

rg -nP 'model_validate|from_file' python/packages/jumpstarter/jumpstarter/config/client.py | head -5

Repository: jumpstarter-dev/jumpstarter

Length of output: 257


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/config/common.py

Repository: jumpstarter-dev/jumpstarter

Length of output: 650


🏁 Script executed:

rg -nP 'Field|validator|field_validator' python/packages/jumpstarter/jumpstarter/config/common.py

Repository: jumpstarter-dev/jumpstarter

Length of output: 53


🏁 Script executed:

rg -nP 'name.*=""' python/packages/jumpstarter/ | head -10

Repository: jumpstarter-dev/jumpstarter

Length of output: 604


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/exporter/lease_context.py | sed -n '40,60p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 1219


🏁 Script executed:

rg -nP 'class Lease' python/packages/jumpstarter/jumpstarter/client/lease.py | head -5

Repository: jumpstarter-dev/jumpstarter

Length of output: 168


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/client/lease.py | sed -n '1,80p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 3324


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/client/lease.py | sed -n '78,130p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 3103


🏁 Script executed:

rg -nP 'class ClientConfigV1Alpha1|class ExporterConfigV1Alpha1' python/packages/jumpstarter/jumpstarter/config/

Repository: jumpstarter-dev/jumpstarter

Length of output: 979


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/config/client.py | sed -n '110,160p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 2421


🏁 Script executed:

rg -nP 'metadata.*=' python/packages/jumpstarter/jumpstarter/config/client.py | head -20

Repository: jumpstarter-dev/jumpstarter

Length of output: 135


🏁 Script executed:

rg -nP 'ObjectMeta\(' python/packages/jumpstarter/jumpstarter/config/client.py -B2 -A2

Repository: jumpstarter-dev/jumpstarter

Length of output: 53


🏁 Script executed:

python3 << 'EOF'
from pydantic_settings import BaseSettings, SettingsConfigDict

class ObjectMeta(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="JMP_")
    namespace: str | None
    name: str

try:
    obj = ObjectMeta()
    print(f"ObjectMeta created with defaults: name={repr(obj.name)}, namespace={repr(obj.namespace)}")
except Exception as e:
    print(f"Error creating ObjectMeta(): {type(e).__name__}: {e}")

try:
    obj = ObjectMeta(name="test")
    print(f"ObjectMeta with name: name={repr(obj.name)}, namespace={repr(obj.namespace)}")
except Exception as e:
    print(f"Error: {e}")
EOF

Repository: jumpstarter-dev/jumpstarter

Length of output: 657


🏁 Script executed:

rg -nP 'class ObjectMeta|Field|validator' python/packages/jumpstarter/jumpstarter/config/common.py -A15

Repository: jumpstarter-dev/jumpstarter

Length of output: 215


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/config/client.py | sed -n '315,345p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 1492


🏁 Script executed:

rg -nP 'ObjectMeta\(' python/packages/jumpstarter/ --type py | grep -v test | head -10

Repository: jumpstarter-dev/jumpstarter

Length of output: 162


🏁 Script executed:

rg -nP 'Field.*name|validator.*name' python/packages/jumpstarter/jumpstarter/config/ | grep -i meta

Repository: jumpstarter-dev/jumpstarter

Length of output: 53


🏁 Script executed:

python3 << 'EOF'
from pydantic_settings import BaseSettings, SettingsConfigDict

class ObjectMeta(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="JMP_")
    namespace: str | None
    name: str

try:
    obj = ObjectMeta(name="", namespace=None)
    print(f"ObjectMeta created with empty name: name={repr(obj.name)}, namespace={repr(obj.namespace)}")
except Exception as e:
    print(f"Error: {type(e).__name__}: {e}")
EOF

Repository: jumpstarter-dev/jumpstarter

Length of output: 130


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/client/lease.py | sed -n '175,200p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 1452


🏁 Script executed:

python3 << 'EOF'
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field

class ObjectMeta(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="JMP_")
    namespace: str | None
    name: str

class TestConfig(BaseSettings):
    metadata: ObjectMeta = Field(default_factory=ObjectMeta)

# Test 1: Can we create TestConfig with default ObjectMeta?
try:
    config = TestConfig()
    print(f"TestConfig() succeeded with metadata: {config.metadata}")
except Exception as e:
    print(f"TestConfig() failed: {type(e).__name__}: {str(e)[:100]}")

# Test 2: Can we create with empty name via YAML-like dict?
try:
    config = TestConfig.model_validate({"metadata": {"name": "", "namespace": None}})
    print(f"TestConfig with empty name succeeded: name={repr(config.metadata.name)}")
except Exception as e:
    print(f"TestConfig with empty name failed: {e}")
EOF

Repository: jumpstarter-dev/jumpstarter

Length of output: 255


🏁 Script executed:

rg -nP 'from_file|model_validate' python/packages/jumpstarter/jumpstarter/config/client.py -B2 -A5 | head -40

Repository: jumpstarter-dev/jumpstarter

Length of output: 1147


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/config/client_config_test.py | grep -A10 "ObjectMeta" | head -30

Repository: jumpstarter-dev/jumpstarter

Length of output: 1705


🏁 Script executed:

rg -nP 'metadata:.*name:|name:' python/packages/jumpstarter/jumpstarter/config/ --type yaml -l

Repository: jumpstarter-dev/jumpstarter

Length of output: 53


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/config/client.py | sed -n '300,340p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 2061


🏁 Script executed:

rg -nP 'metadata' python/packages/jumpstarter/jumpstarter/config/client.py | grep -i "field\|default"

Repository: jumpstarter-dev/jumpstarter

Length of output: 135


🏁 Script executed:

cat -n python/packages/jumpstarter/jumpstarter/config/client.py | sed -n '110,125p'

Repository: jumpstarter-dev/jumpstarter

Length of output: 822


🏁 Script executed:

python3 << 'EOF'
# Test if ObjectMeta can be instantiated via Field(default_factory=ObjectMeta)
from pydantic import Field, BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict

class ObjectMeta(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="JMP_")
    namespace: str | None
    name: str

class ClientConfig(BaseSettings):
    metadata: ObjectMeta = Field(default_factory=ObjectMeta)

# Can we instantiate without metadata?
try:
    config = ClientConfig()
    print(f"✗ ClientConfig() succeeded: {config.metadata}")
except Exception as e:
    print(f"✓ ClientConfig() failed (expected): name and namespace required")

# Can we provide empty name via YAML-like input?
try:
    config = ClientConfig.model_validate({"metadata": {"name": "", "namespace": "default"}})
    print(f"✓ Empty name allowed: name={repr(config.metadata.name)}")
    print(f"  This would pass empty string to client_name parameter")
except Exception as e:
    print(f"✗ Failed: {e}")
EOF

Repository: jumpstarter-dev/jumpstarter

Length of output: 220


Tighten the ownership check to reject empty client_name.

The ownership validation in Lease.request_async() at line 179 uses a truthy check: if self.client_name and existing_lease.client != self.client_name:. Since ObjectMeta.name is typed as str (not str | None), it can be instantiated with an empty string via YAML or config input. When self.metadata.name is empty and passed as client_name at line 332, the ownership check silently no-ops (empty string is falsy), allowing a lease to be reused without validation. Change the check to explicitly reject empty client names, either by using is not None/!= "" or raising a validation error on empty metadata.name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter/jumpstarter/config/client.py` at line 332, The
ownership check in Lease.request_async() currently uses a truthy test on
self.client_name which allows an empty string to bypass validation; update the
logic to explicitly reject empty client names by checking that self.client_name
is not None and not an empty string (e.g., self.client_name != "") or raise a
validation error when metadata.name is empty before passing it to client_name
(where client_name=self.metadata.name is set); ensure the comparison against
existing_lease.client uses the explicit non-empty check so empty metadata.name
cannot reuse a lease.

@bennyz bennyz changed the title fix: confusing error message fix: prevent transfer warning for non-owned leases Apr 20, 2026
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.

2 participants