Skip to content

cli: add lease transfer option#338

Open
bennyz wants to merge 4 commits intojumpstarter-dev:mainfrom
bennyz:lease-transfer
Open

cli: add lease transfer option#338
bennyz wants to merge 4 commits intojumpstarter-dev:mainfrom
bennyz:lease-transfer

Conversation

@bennyz
Copy link
Member

@bennyz bennyz commented Mar 18, 2026

Allow users to transfer the lease with:

 jmp update lease <lease name> --to-client <client name>

jmp update lease <lease name> --to-client majopela

Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
@netlify
Copy link

netlify bot commented Mar 18, 2026

Deploy Preview for jumpstarter-docs ready!

Name Link
🔨 Latest commit 9e96efd
🔍 Latest deploy log https://app.netlify.com/projects/jumpstarter-docs/deploys/69baca500bc10600088d90bd
😎 Deploy Preview https://deploy-preview-338--jumpstarter-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@bennyz bennyz requested a review from mangelajo March 18, 2026 14:02
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f411d38d-0141-4316-82bf-bfda8cbe9e80

📥 Commits

Reviewing files that changed from the base of the PR and between 4372c0c and 9e96efd.

📒 Files selected for processing (1)
  • e2e/tests.bats
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/tests.bats

📝 Walkthrough

Walkthrough

Adds lease transfer support: CLI option to transfer a lease to another client, CLI validation and tests, config and gRPC plumbing to accept a client field on UpdateLease, lease-side detection of transferred leases, and shell error handling for transferred leases.

Changes

Cohort / File(s) Summary
E2E test
e2e/tests.bats
New end-to-end test "can transfer lease to another client" that creates a lease, transfers ownership, verifies the transfer, and cleans up.
CLI: update command & tests
python/packages/jumpstarter-cli/jumpstarter_cli/update.py, python/packages/jumpstarter-cli/jumpstarter_cli/update_test.py
Adds --to-client option and to_client parameter to update_lease; validates at least one update option (duration
Shell error handling
python/packages/jumpstarter-cli/jumpstarter_cli/shell.py
Detects lease_transferred in BaseExceptionGroup path and raises ExporterOfflineError with a transfer-specific message.
Client lease logic
python/packages/jumpstarter/jumpstarter/client/lease.py
Adds lease_transferred: bool field; marks it true and logs a transfer-specific warning when AioRpcError indicates permission denied.
Config client API
python/packages/jumpstarter/jumpstarter/config/client.py
update_lease gains optional client parameter and forwards it to the underlying UpdateLease call.
gRPC client
python/packages/jumpstarter/jumpstarter/client/grpc.py
ClientService.UpdateLease accepts optional client and includes it in the update_mask when provided.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as "CLI (--to-client)"
    participant Config as "ClientConfig"
    participant gRPC as "ClientService gRPC"
    participant Lease as "Lease object / runtime"

    User->>CLI: run "jumpstarter update --to-client client-b"
    CLI->>CLI: validate (duration | begin_time | to_client)
    CLI->>Config: update_lease(name, client=namespaces/.../clients/client-b)
    Config->>gRPC: UpdateLease(name, client=namespaces/.../clients/client-b)
    gRPC->>gRPC: include "client" in update_mask
    gRPC-->>Config: return updated lease
    Config-->>CLI: lease returned
    CLI-->>User: print updated lease

    Note over Lease,CLI: Later, operations using transferred lease
    User->>CLI: perform action using session
    CLI->>Lease: perform RPC -> AioRpcError("permission denied")
    Lease->>Lease: set lease_transferred = true
    Lease-->>CLI: raise ExporterOfflineError("Lease has been transferred to another client. Session is no longer valid.")
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • mangelajo

Poem

🐰 I nudged a lease across the sod,
A tiny hop from client A to God,
A flag, a log, a transfer bell,
If permissions fail, I warn and tell,
Hooray — new owner, time for a nod!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% 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
Title check ✅ Passed The title 'cli: add lease transfer option' accurately summarizes the main change: adding a CLI option for users to transfer leases to another client.
Description check ✅ Passed The description provides a clear example of the new CLI feature, showing users how to transfer a lease with the --to-client option.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

continue
# Exporter went offline or lease ended - log and exit gracefully
logger.warning("Connection to exporter lost: %s", e.details())
if "permission denied" in str(e.details()).lower():
Copy link
Member Author

Choose a reason for hiding this comment

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

@mangelajo I think I can add a more specific error in the controller so we don't have to check like this

Copy link
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: 3

🧹 Nitpick comments (2)
e2e/tests.bats (1)

345-363: Make this e2e transfer test self-contained.

This test currently depends on suite-wide state (pre-created clients/exporters and selected client), so it won’t run reliably in isolation. Please add explicit preconditions/cleanup inside this test (or setup/teardown hooks scoped for it).

Based on learnings: E2E tests in e2e/tests.bats should be runnable in isolation and not rely on prior suite setup or shared state across tests.

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

In `@e2e/tests.bats` around lines 345 - 363, The e2e "can transfer lease to
another client" test is not self-contained; modify the test block so it creates
the required preconditions (create both clients and any exporter/selector
resources used by the selector `example.com/board=oidc`, and explicitly select
the source client via `jmp config client use test-client-oidc`) before creating
the lease, and add teardown steps at the end to delete the created clients,
exporter and leases (e.g., `jmp delete client test-client-oidc`,
`test-client-legacy`, delete the exporter and the lease) so the test can run in
isolation; update the test block name and use the existing commands shown in the
diff (e.g., the `jmp create client`, `jmp create exporter`, `jmp config client
use`, `jmp create lease`, `jmp update lease`, and the `jmp delete` commands) to
perform setup and cleanup.
python/packages/jumpstarter-cli/jumpstarter_cli/update.py (1)

56-56: Validate --to-client as a client name before path construction.

Line 56 accepts raw text and interpolates it directly into a resource path. A slash-containing value (or already-qualified path) yields malformed identifiers and confusing server errors.

Suggested fix
+    if to_client is not None and "/" in to_client:
+        raise click.UsageError("--to-client expects a client name, not a resource path")
     client_path = f"namespaces/{config.metadata.namespace}/clients/{to_client}" if to_client else None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/update.py` at line 56, The
code builds client_path by directly interpolating the --to-client value into
"namespaces/.../clients/{to_client}" (variable client_path); validate and reject
or sanitize the to_client input before constructing the path: ensure to_client
is not an already-qualified path (no leading "namespaces/" and no "/"
characters) and matches an allowed client-name pattern (e.g. alphanumerics,
dots, underscores, hyphens) — if invalid, raise a clear error (use
click.BadParameter if this is a Click command handler) referencing the to_client
parameter in the update command handler so malformed values never get
interpolated into client_path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/tests.bats`:
- Line 355: The yq extraction for LEASE_NAME is using the wrong YAML path;
update the LEASE_NAME assignment (the line setting LEASE_NAME=$(echo "$output" |
go run ... '.metadata.name')) to use the actual path in the Jumpstarter lease
CLI output (e.g., change to the correct field such as '.lease.metadata.name' or
'.lease.name' depending on the model) — inspect the CLI JSON/YAML shape and
replace '.metadata.name' with the exact path to the lease name so subsequent
transfer steps receive a non-null value.

In `@python/packages/jumpstarter/jumpstarter/client/grpc.py`:
- Around line 472-474: The code is writing to the proto field Lease.client
(marked OUTPUT_ONLY) which creates an API mismatch; change the transfer flow to
use a dedicated input field instead: add a new_client (or transfer_to_client)
field on UpdateLeaseRequest in the proto, update the server handler that
currently reads Lease.client
(controller/internal/service/client/v1/client_service.go) to read the new
UpdateLeaseRequest.new_client field for transfers, and update the Python client
(jumpstarter/client/grpc.py) to set update_fields and request.new_client instead
of assigning lease_pb.client; alternatively, if you intend the server to accept
client as input, remove the OUTPUT_ONLY annotation from Lease.client in the
proto and regenerate stubs so server and Python client remain consistent.

---

Nitpick comments:
In `@e2e/tests.bats`:
- Around line 345-363: The e2e "can transfer lease to another client" test is
not self-contained; modify the test block so it creates the required
preconditions (create both clients and any exporter/selector resources used by
the selector `example.com/board=oidc`, and explicitly select the source client
via `jmp config client use test-client-oidc`) before creating the lease, and add
teardown steps at the end to delete the created clients, exporter and leases
(e.g., `jmp delete client test-client-oidc`, `test-client-legacy`, delete the
exporter and the lease) so the test can run in isolation; update the test block
name and use the existing commands shown in the diff (e.g., the `jmp create
client`, `jmp create exporter`, `jmp config client use`, `jmp create lease`,
`jmp update lease`, and the `jmp delete` commands) to perform setup and cleanup.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/update.py`:
- Line 56: The code builds client_path by directly interpolating the --to-client
value into "namespaces/.../clients/{to_client}" (variable client_path); validate
and reject or sanitize the to_client input before constructing the path: ensure
to_client is not an already-qualified path (no leading "namespaces/" and no "/"
characters) and matches an allowed client-name pattern (e.g. alphanumerics,
dots, underscores, hyphens) — if invalid, raise a clear error (use
click.BadParameter if this is a Click command handler) referencing the to_client
parameter in the update command handler so malformed values never get
interpolated into client_path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9ec00ea-e969-4522-a1b4-6fa126419e19

📥 Commits

Reviewing files that changed from the base of the PR and between c6cf780 and 20e216a.

📒 Files selected for processing (7)
  • e2e/tests.bats
  • python/packages/jumpstarter-cli/jumpstarter_cli/shell.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/update.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/update_test.py
  • python/packages/jumpstarter/jumpstarter/client/grpc.py
  • python/packages/jumpstarter/jumpstarter/client/lease.py
  • python/packages/jumpstarter/jumpstarter/config/client.py

Comment on lines +472 to +474
if client is not None:
lease_pb.client = client
update_fields.append("client")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Verify Lease.client field behavior in proto:"
rg -n -C3 'optional string client = 9|field_behavior' protocol/proto/jumpstarter/client/v1/client.proto

echo
echo "Verify server consumes req.Lease.Client as update input:"
rg -n -C6 'req\.Lease\.Client|ParseClientIdentifier|cannot transfer lease' controller/internal/service/client/v1/client_service.go

Repository: jumpstarter-dev/jumpstarter

Length of output: 5289


Lease.client is marked OUTPUT_ONLY in the proto but is being written and used as a transfer input by both the server and Python client.

The proto definition (lines 96-99 of protocol/proto/jumpstarter/client/v1/client.proto) declares Lease.client with (google.api.field_behavior) = OUTPUT_ONLY, yet the server implementation (controller/internal/service/client/v1/client_service.go:292-310) reads it as an input field to transfer a lease to a new client. Line 473 writes to this field, creating an API/schema mismatch that will cause inconsistency in generated API documentation and client libraries.

Resolve by either removing the OUTPUT_ONLY constraint from the proto or adding a dedicated new_client field to UpdateLeaseRequest for the transfer operation.

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

In `@python/packages/jumpstarter/jumpstarter/client/grpc.py` around lines 472 -
474, The code is writing to the proto field Lease.client (marked OUTPUT_ONLY)
which creates an API mismatch; change the transfer flow to use a dedicated input
field instead: add a new_client (or transfer_to_client) field on
UpdateLeaseRequest in the proto, update the server handler that currently reads
Lease.client (controller/internal/service/client/v1/client_service.go) to read
the new UpdateLeaseRequest.new_client field for transfers, and update the Python
client (jumpstarter/client/grpc.py) to set update_fields and request.new_client
instead of assigning lease_pb.client; alternatively, if you intend the server to
accept client as input, remove the OUTPUT_ONLY annotation from Lease.client in
the proto and regenerate stubs so server and Python client remain consistent.

bennyz added 2 commits March 18, 2026 16:13
- e2e
- pytest

Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
Assisted-by: claude-sonnet-4.6
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
Assisted-by: claude-sonnet-4.6
Copy link
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.

🧹 Nitpick comments (1)
python/packages/jumpstarter-cli/jumpstarter_cli/update.py (1)

56-56: Consider validating to_client for invalid characters.

If to_client contains a / (e.g., --to-client "foo/bar"), the constructed path would have more than 4 segments, causing parse_identifier in grpc.py to fail with a somewhat cryptic error. Adding early validation could provide a clearer error message.

♻️ Optional: Add input validation
+    if to_client and "/" in to_client:
+        raise click.UsageError("Client name cannot contain '/' characters")
+
     client_path = f"namespaces/{config.metadata.namespace}/clients/{to_client}" if to_client else None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/update.py` at line 56, The
code builds client_path using to_client without validating it, which allows
values like "foo/bar" to produce invalid identifiers and trigger a cryptic
parse_identifier error; add an explicit validation in jumpstarter_cli.update
(before constructing client_path) that rejects values containing '/' (and
optionally other disallowed chars, e.g., only allow alphanumerics, hyphen,
underscore) and raise a clear ValueError or click.BadParameter explaining that
to_client must not contain '/' and must match the allowed pattern so
parse_identifier in grpc.py won't fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@python/packages/jumpstarter-cli/jumpstarter_cli/update.py`:
- Line 56: The code builds client_path using to_client without validating it,
which allows values like "foo/bar" to produce invalid identifiers and trigger a
cryptic parse_identifier error; add an explicit validation in
jumpstarter_cli.update (before constructing client_path) that rejects values
containing '/' (and optionally other disallowed chars, e.g., only allow
alphanumerics, hyphen, underscore) and raise a clear ValueError or
click.BadParameter explaining that to_client must not contain '/' and must match
the allowed pattern so parse_identifier in grpc.py won't fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 906584b1-5e20-4cbd-bb63-a91b9eb24f61

📥 Commits

Reviewing files that changed from the base of the PR and between 20e216a and 4372c0c.

📒 Files selected for processing (5)
  • e2e/tests.bats
  • python/packages/jumpstarter-cli/jumpstarter_cli/shell.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/update.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/update_test.py
  • python/packages/jumpstarter/jumpstarter/client/lease.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • python/packages/jumpstarter-cli/jumpstarter_cli/update_test.py
  • e2e/tests.bats
  • python/packages/jumpstarter-cli/jumpstarter_cli/shell.py

Copy link
Member

@mangelajo mangelajo left a comment

Choose a reason for hiding this comment

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

awesome, E2E and all! :D

@mangelajo mangelajo enabled auto-merge (squash) March 18, 2026 14:31
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
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