Skip to content

Add exporter context env vars to jmp shell and env_with_metadata() helper#291

Open
mangelajo wants to merge 1 commit intomainfrom
feat/exporter-metadata-env
Open

Add exporter context env vars to jmp shell and env_with_metadata() helper#291
mangelajo wants to merge 1 commit intomainfrom
feat/exporter-metadata-env

Conversation

@mangelajo
Copy link
Member

@mangelajo mangelajo commented Mar 5, 2026

Summary

  • Sets JMP_EXPORTER, JMP_LEASE, and JMP_EXPORTER_LABELS environment variables when launching a jmp shell with a remote lease, so scripts and commands running inside the shell can discover the exporter context.
  • Adds env_with_metadata() / env_with_metadata_async() context managers and ExporterMetadata dataclass that let Python code retrieve the exporter name, labels, and lease ID from within a shell session.
  • Fetches exporter labels from the controller during lease acquisition and stores them on the Lease object.

Usage

from jumpstarter.common.utils import env_with_metadata

with env_with_metadata() as (client, metadata):
    print(metadata.name)    # e.g. "my-board-exporter"
    print(metadata.labels)  # e.g. {"board": "rpi4", "location": "lab-1"}
    print(metadata.lease)   # e.g. "lease-abc123"

Or directly from the shell environment:

echo $JMP_EXPORTER        # my-board-exporter
echo $JMP_LEASE           # lease-abc123
echo $JMP_EXPORTER_LABELS # board=rpi4,location=lab-1

Closes #53

Test plan

  • Existing tests pass (162 jumpstarter + 39 jumpstarter-cli)
  • New test: test_launch_shell_sets_lease_env verifies env vars are set when a lease is provided
  • New test: test_exporter_metadata_from_env verifies parsing with all env vars set
  • New test: test_exporter_metadata_from_env_empty verifies graceful behavior with no env vars
  • New test: test_exporter_metadata_from_env_labels_with_equals_in_value verifies label values containing =
  • Lint passes cleanly

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Exporter metadata (name, labels, lease id) is captured for acquired leases; labels are fetched after acquisition and applied to the environment.
    • New sync/async context managers return client plus exporter metadata.
    • Environment variables now support exporter name, labels, and lease id; shell launches apply lease-derived vars.
  • Tests

    • Added tests for lease-based environment setup and parsing exporter metadata (including edge-case labels).

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Walkthrough

Adds exporter metadata propagation: new ExporterMetadata and env_with_metadata helpers; new JMP_EXPORTER and JMP_EXPORTER_LABELS env constants; Lease stores exporter_labels and fetches them after acquisition; launch_shell merges lease-derived env vars; tests added for parsing and shell behavior.

Changes

Cohort / File(s) Summary
Configuration & Constants
python/packages/jumpstarter/jumpstarter/config/env.py
Added JMP_EXPORTER and JMP_EXPORTER_LABELS constants.
Exporter Metadata & Environment Helpers
python/packages/jumpstarter/jumpstarter/utils/env.py
Added frozen ExporterMetadata dataclass with from_env(); added env_with_metadata_async() and env_with_metadata() context managers that yield (client, ExporterMetadata); imports updated to expose new env constants.
Lease Acquisition
python/packages/jumpstarter/jumpstarter/client/lease.py
Added exporter_labels: dict[str, str] field and async _fetch_exporter_labels(); on lease acquire, sets exporter_name and fetches labels (on failure resets labels and logs a warning).
Shell Integration & Utils
python/packages/jumpstarter/jumpstarter/common/utils.py
Added _lease_env_vars(lease) helper and updated launch_shell() to merge lease-derived env vars (JMP_EXPORTER, optional JMP_LEASE, JMP_EXPORTER_LABELS).
Tests
python/packages/jumpstarter/jumpstarter/common/utils_test.py
Added tests for launch_shell lease env propagation and ExporterMetadata.from_env() parsing (including edge cases with '=' in label values).

Sequence Diagram

sequenceDiagram
    participant User
    participant EnvSync as env_with_metadata()
    participant Portal as BlockingPortal
    participant EnvAsync as env_with_metadata_async()
    participant Client
    participant Lease
    participant OS as Environment
    participant Meta as ExporterMetadata

    User->>EnvSync: enter context
    EnvSync->>Portal: create blocking portal
    EnvSync->>EnvAsync: call async helper with portal
    EnvAsync->>Client: create client via env_async()
    EnvAsync->>OS: read JMP_EXPORTER, JMP_EXPORTER_LABELS, JMP_LEASE
    OS-->>Meta: provide raw vars
    Meta->>Meta: ExporterMetadata.from_env()
    EnvAsync-->>EnvSync: yield (Client, ExporterMetadata)
    EnvSync-->>User: provide (client, metadata)
    User->>Client: use client (may request lease)
    Client->>Lease: acquire()
    Lease->>Lease: set exporter_name from acquisition result
    Lease->>Lease: call _fetch_exporter_labels()
    Lease->>OS: provide labels for shell env (JMP_EXPORTER, JMP_LEASE, JMP_EXPORTER_LABELS)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • jumpstarter-dev/jumpstarter#671: Modifies Lease acquisition and exporter_name handling; overlaps with exporter label logic.
  • jumpstarter-dev/jumpstarter#592: Also changes the Lease implementation and context-manager behavior; likely to touch similar code paths.

Suggested reviewers

  • bennyz

Poem

🐇 I hop through code with tiny paws,
Exporter names and labels in my claws,
Leases fetched, the shell now knows,
Metadata seeds where curiosity grows,
A rabbit cheers — deploys and applauds!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding exporter context environment variables to jmp shell and introducing the env_with_metadata() helper function.
Linked Issues check ✅ Passed The PR fully implements all requirements from issue #53: exposes exporter name and labels via environment variables in shell sessions, provides env_with_metadata() helper with ExporterMetadata dataclass, and allows code to access exporter context.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #53 requirements: environment variable exposure, metadata context managers, dataclass implementation, and supporting tests with no unrelated modifications.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/exporter-metadata-env

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.

@mangelajo mangelajo requested review from NickCao, evakhoni and kirkbrauer and removed request for kirkbrauer March 5, 2026 20:10
@mangelajo mangelajo force-pushed the feat/exporter-metadata-env branch from 555b95a to 3f784d1 Compare March 5, 2026 20:13
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: 2

🧹 Nitpick comments (1)
python/packages/jumpstarter/jumpstarter/common/utils_test.py (1)

29-45: test_launch_shell_sets_lease_env currently doesn’t validate env propagation.

It only asserts process success; it should also assert that the three lease/exporter env vars are actually present.

Suggested test strengthening
-def test_launch_shell_sets_lease_env(tmp_path, monkeypatch):
+def test_launch_shell_sets_lease_env(tmp_path, monkeypatch, capsys):
     monkeypatch.setenv("SHELL", shutil.which("env"))
     lease = SimpleNamespace(
         exporter_name="my-exporter",
         name="lease-123",
         exporter_labels={"board": "rpi4", "location": "lab-1"},
         lease_ending_callback=None,
     )
     exit_code = launch_shell(
         host=str(tmp_path / "test.sock"),
         context="my-exporter",
         allow=["*"],
         unsafe=False,
         use_profiles=False,
         lease=lease,
     )
     assert exit_code == 0
+    out = capsys.readouterr().out
+    assert "JMP_EXPORTER=my-exporter" in out
+    assert "JMP_LEASE=lease-123" in out
+    assert "JMP_EXPORTER_LABELS=board=rpi4,location=lab-1" in out
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter/jumpstarter/common/utils_test.py` around lines 29
- 45, The test only checks exit code but must also verify the launched shell
received lease-related env vars: update test_launch_shell_sets_lease_env to
capture the launched process stdout (e.g., use capsys or patch
subprocess.run/capture_output) when SHELL is set to the env binary and assert
the output contains the lease.exporter_name, lease.name, and each key=value from
lease.exporter_labels so you verify the three lease/exporter env vars are
actually propagated by launch_shell.
🤖 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/client/lease.py`:
- Around line 142-149: The _fetch_exporter_labels method can leave stale
self.exporter_labels when svc.GetExporter fails; modify the exception path in
_fetch_exporter_labels (the async method) to clear or reset self.exporter_labels
(e.g., set to {} or None) when an exception occurs and include the exporter name
in the debug log (use logger.debug("Could not fetch labels for exporter %s: %s",
self.exporter_name, e) or similar) so reused lease instances do not retain
previous labels.

In `@python/packages/jumpstarter/jumpstarter/common/utils_test.py`:
- Around line 70-75: The test
test_exporter_metadata_from_env_labels_with_equals_in_value triggers typo-linter
noise because of the sample value; update the fixture to use a less typo-like
sample while preserving the edge case (an equals sign inside a label value),
e.g. replace "key=val=ue,other=ok" with something like "key=a=b,other=ok", keep
the monkeypatch calls and the assertion that ExporterMetadata.from_env() yields
meta.labels == {"key": "a=b", "other": "ok"}.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/common/utils_test.py`:
- Around line 29-45: The test only checks exit code but must also verify the
launched shell received lease-related env vars: update
test_launch_shell_sets_lease_env to capture the launched process stdout (e.g.,
use capsys or patch subprocess.run/capture_output) when SHELL is set to the env
binary and assert the output contains the lease.exporter_name, lease.name, and
each key=value from lease.exporter_labels so you verify the three lease/exporter
env vars are actually propagated by launch_shell.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 250a74dd-bc0f-4834-8909-f4326472e1c6

📥 Commits

Reviewing files that changed from the base of the PR and between 5bfa067 and 555b95a.

📒 Files selected for processing (5)
  • python/packages/jumpstarter/jumpstarter/client/lease.py
  • python/packages/jumpstarter/jumpstarter/common/utils.py
  • python/packages/jumpstarter/jumpstarter/common/utils_test.py
  • python/packages/jumpstarter/jumpstarter/config/env.py
  • python/packages/jumpstarter/jumpstarter/utils/env.py

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/jumpstarter/utils/env.py (1)

13-35: Consider using immutable mapping for labels field to strengthen frozen dataclass contract.

ExporterMetadata is marked frozen=True to indicate immutable semantics, but labels: dict[str, str] can still be mutated via direct assignment (e.g., metadata.labels['key'] = 'value'). Since ExporterMetadata is exported as public API, wrapping it with MappingProxyType would prevent accidental mutation and clarify intent.

♻️ Suggested refactor
 import os
 from contextlib import ExitStack, asynccontextmanager, contextmanager
 from dataclasses import dataclass, field
+from types import MappingProxyType
+from typing import Mapping
@@
 `@dataclass`(frozen=True)
 class ExporterMetadata:
@@
-    labels: dict[str, str] = field(default_factory=dict)
+    labels: Mapping[str, str] = field(default_factory=lambda: MappingProxyType({}))
@@
-        return cls(name=name, labels=labels, lease=lease)
+        return cls(name=name, labels=MappingProxyType(labels), lease=lease)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter/jumpstarter/utils/env.py` around lines 13 - 35,
ExporterMetadata is frozen but exposes a mutable dict via the labels field;
change labels to an immutable mapping and wrap any dicts produced in from_env
with types.MappingProxyType (or change the annotation to Mapping[str,str] and
return MappingProxyType(labels)) so callers cannot mutate the labels after
construction; update the dataclass field default to produce an immutable empty
mapping via MappingProxyType({}) (and add the import for MappingProxyType and
typing.Mapping if needed) while keeping the class name ExporterMetadata and the
factory method from_env unchanged.
🤖 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/jumpstarter/utils/env.py`:
- Around line 13-35: ExporterMetadata is frozen but exposes a mutable dict via
the labels field; change labels to an immutable mapping and wrap any dicts
produced in from_env with types.MappingProxyType (or change the annotation to
Mapping[str,str] and return MappingProxyType(labels)) so callers cannot mutate
the labels after construction; update the dataclass field default to produce an
immutable empty mapping via MappingProxyType({}) (and add the import for
MappingProxyType and typing.Mapping if needed) while keeping the class name
ExporterMetadata and the factory method from_env unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 96ff34ba-0502-43d4-b638-ac73844f7698

📥 Commits

Reviewing files that changed from the base of the PR and between 555b95a and 3f784d1.

📒 Files selected for processing (5)
  • python/packages/jumpstarter/jumpstarter/client/lease.py
  • python/packages/jumpstarter/jumpstarter/common/utils.py
  • python/packages/jumpstarter/jumpstarter/common/utils_test.py
  • python/packages/jumpstarter/jumpstarter/config/env.py
  • python/packages/jumpstarter/jumpstarter/utils/env.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • python/packages/jumpstarter/jumpstarter/common/utils_test.py
  • python/packages/jumpstarter/jumpstarter/client/lease.py
  • python/packages/jumpstarter/jumpstarter/common/utils.py

@evakhoni
Copy link
Contributor

Or directly from the shell environment:
echo $JMP_EXPORTER # my-board-exporter
echo $JMP_LEASE # lease-abc123
echo $JMP_EXPORTER_LABELS # board=rpi4,location=lab-1

I wonder what would happen with complex selectors. are we fetching the selector we leased with, or the labels of the exporter that ended up matching that selector?

@mangelajo mangelajo force-pushed the feat/exporter-metadata-env branch from 3f784d1 to 39b3926 Compare March 12, 2026 12:35
@netlify
Copy link

netlify bot commented Mar 12, 2026

Deploy Preview for jumpstarter-docs ready!

Name Link
🔨 Latest commit 8b28485
🔍 Latest deploy log https://app.netlify.com/projects/jumpstarter-docs/deploys/69b2b3b88fb27600084d427e
😎 Deploy Preview https://deploy-preview-291--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.

…lper

Set JMP_EXPORTER, JMP_LEASE, and JMP_EXPORTER_LABELS environment variables
when launching a jmp shell with a remote lease. Add env_with_metadata()
context manager that yields (client, ExporterMetadata) so scripts can
access the exporter name, labels, and lease ID from within a shell session.

Closes #53

Made-with: Cursor
@mangelajo mangelajo force-pushed the feat/exporter-metadata-env branch from 39b3926 to 8b28485 Compare March 12, 2026 12:38
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/jumpstarter/common/utils_test.py (1)

29-45: Consider verifying the environment variables are actually set.

The test confirms launch_shell accepts a lease and exits successfully, but doesn't verify that JMP_EXPORTER, JMP_LEASE, and JMP_EXPORTER_LABELS are actually propagated to the subprocess environment.

🧪 Optional enhancement to capture and verify env vars
 def test_launch_shell_sets_lease_env(tmp_path, monkeypatch):
-    monkeypatch.setenv("SHELL", shutil.which("env"))
+    # Use a script that outputs specific env vars to a file
+    env_output = tmp_path / "env_output.txt"
+    script = tmp_path / "capture_env.sh"
+    script.write_text(f"""#!/bin/sh
+echo "JMP_EXPORTER=$JMP_EXPORTER" >> {env_output}
+echo "JMP_LEASE=$JMP_LEASE" >> {env_output}
+echo "JMP_EXPORTER_LABELS=$JMP_EXPORTER_LABELS" >> {env_output}
+""")
+    script.chmod(0o755)
+    monkeypatch.setenv("SHELL", str(script))
     lease = SimpleNamespace(
         exporter_name="my-exporter",
         name="lease-123",
         exporter_labels={"board": "rpi4", "location": "lab-1"},
         lease_ending_callback=None,
     )
     exit_code = launch_shell(
         host=str(tmp_path / "test.sock"),
         context="my-exporter",
         allow=["*"],
         unsafe=False,
         use_profiles=False,
         lease=lease,
     )
     assert exit_code == 0
+    output = env_output.read_text()
+    assert "JMP_EXPORTER=my-exporter" in output
+    assert "JMP_LEASE=lease-123" in output
+    assert "board=rpi4" in output
+    assert "location=lab-1" in output
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter/jumpstarter/common/utils_test.py` around lines 29
- 45, Update test_launch_shell_sets_lease_env to verify the env vars passed to
the child process by mocking the subprocess invocation used inside launch_shell
(e.g., subprocess.run or subprocess.Popen depending on implementation) so you
can capture the env dict passed to it; call launch_shell with the same lease
fixture and then assert the captured env contains keys/values for JMP_EXPORTER
== lease.exporter_name, JMP_LEASE == lease.name, and JMP_EXPORTER_LABELS ==
json.dumps(lease.exporter_labels) (or the exact serialization used by
launch_shell). Ensure you restore/cleanup the mock so other tests are
unaffected.
🤖 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/jumpstarter/common/utils_test.py`:
- Around line 29-45: Update test_launch_shell_sets_lease_env to verify the env
vars passed to the child process by mocking the subprocess invocation used
inside launch_shell (e.g., subprocess.run or subprocess.Popen depending on
implementation) so you can capture the env dict passed to it; call launch_shell
with the same lease fixture and then assert the captured env contains
keys/values for JMP_EXPORTER == lease.exporter_name, JMP_LEASE == lease.name,
and JMP_EXPORTER_LABELS == json.dumps(lease.exporter_labels) (or the exact
serialization used by launch_shell). Ensure you restore/cleanup the mock so
other tests are unaffected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59d1ab79-7ea0-470d-844b-24ea3e260ccd

📥 Commits

Reviewing files that changed from the base of the PR and between 3f784d1 and 39b3926.

📒 Files selected for processing (5)
  • python/packages/jumpstarter/jumpstarter/client/lease.py
  • python/packages/jumpstarter/jumpstarter/common/utils.py
  • python/packages/jumpstarter/jumpstarter/common/utils_test.py
  • python/packages/jumpstarter/jumpstarter/config/env.py
  • python/packages/jumpstarter/jumpstarter/utils/env.py

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/jumpstarter/common/utils.py (1)

87-97: Consider adding type hint for the lease parameter.

The function works correctly but lacks a type annotation for lease. Adding a type hint would improve IDE support and documentation.

📝 Suggested type annotation
-def _lease_env_vars(lease) -> dict[str, str]:
+def _lease_env_vars(lease: "Lease") -> dict[str, str]:

You would need to add the import under TYPE_CHECKING:

 if TYPE_CHECKING:
     from jumpstarter.driver import Driver
+    from jumpstarter.client.lease import Lease
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter/jumpstarter/common/utils.py` around lines 87 -
97, The _lease_env_vars function is missing a type annotation for the lease
parameter; update its signature to include the lease type (e.g., lease: Lease)
and add a conditional import for that type under TYPE_CHECKING (from typing
import TYPE_CHECKING; if TYPE_CHECKING: from <module> import Lease) so you don't
create runtime import cycles; update any forward references if needed and keep
existing logic using JMP_EXPORTER, JMP_LEASE, and JMP_EXPORTER_LABELS unchanged.
🤖 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/jumpstarter/common/utils.py`:
- Around line 87-97: The _lease_env_vars function is missing a type annotation
for the lease parameter; update its signature to include the lease type (e.g.,
lease: Lease) and add a conditional import for that type under TYPE_CHECKING
(from typing import TYPE_CHECKING; if TYPE_CHECKING: from <module> import Lease)
so you don't create runtime import cycles; update any forward references if
needed and keep existing logic using JMP_EXPORTER, JMP_LEASE, and
JMP_EXPORTER_LABELS unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1fea5fdb-d1aa-4ef1-be79-9551a75efed5

📥 Commits

Reviewing files that changed from the base of the PR and between 39b3926 and 8b28485.

📒 Files selected for processing (5)
  • python/packages/jumpstarter/jumpstarter/client/lease.py
  • python/packages/jumpstarter/jumpstarter/common/utils.py
  • python/packages/jumpstarter/jumpstarter/common/utils_test.py
  • python/packages/jumpstarter/jumpstarter/config/env.py
  • python/packages/jumpstarter/jumpstarter/utils/env.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/packages/jumpstarter/jumpstarter/client/lease.py
  • python/packages/jumpstarter/jumpstarter/common/utils_test.py

Copy link
Contributor

@raballew raballew left a comment

Choose a reason for hiding this comment

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

The label serialization (key1=val1,key2=val2) will break on Kubernetes label values that contain commas -- e.g. env=dev,staging would get parsed as two separate labels. JSON would handle this cleanly, or at least reject/document the limitation.

Also the test test_launch_shell_sets_lease_env only checks exit_code == 0 but doesn't actually verify the env vars show up in the output. And env_with_metadata() / env_with_metadata_async() and _fetch_exporter_labels don't have tests yet.

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.

exporter context under a shell

3 participants