Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
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
11 changes: 10 additions & 1 deletion packages/jumpstarter-cli-common/jumpstarter_cli_common/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from contextlib import suppress
from functools import partial, reduce, wraps
from pathlib import Path

import click
from pydantic import ValidationError

from jumpstarter.config.client import ClientConfigV1Alpha1
from jumpstarter.config.exporter import ExporterConfigV1Alpha1
Expand Down Expand Up @@ -52,7 +54,14 @@ def wrapper(*args, **kwds): # noqa: C901
match len(params):
case 0:
if client:
config = UserConfigV1Alpha1.load_or_create().config.current_client
config = None

with suppress(ValidationError):
config = ClientConfigV1Alpha1()

if config is None:
config = UserConfigV1Alpha1.load_or_create().config.current_client

if config is None:
raise click.ClickException(
f"none of {', '.join(options_names)} is specified, and default config is not set"
Expand Down
63 changes: 30 additions & 33 deletions packages/jumpstarter/jumpstarter/config/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,23 @@
from datetime import timedelta
from functools import wraps
from pathlib import Path
from typing import ClassVar, Literal, Optional, Self
from typing import Annotated, ClassVar, Literal, Optional, Self

import grpc
import yaml
from anyio.from_thread import BlockingPortal, start_blocking_portal
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict

from .common import CONFIG_PATH, ObjectMeta
from .env import JMP_DRIVERS_ALLOW, JMP_ENDPOINT, JMP_LEASE, JMP_NAME, JMP_NAMESPACE, JMP_TOKEN
from .env import JMP_LEASE
from .grpc import call_credentials
from .tls import TLSConfigV1Alpha1
from jumpstarter.client.grpc import ClientService
from jumpstarter.common.exceptions import FileNotFoundError
from jumpstarter.common.grpc import aio_secure_channel, ssl_channel_credentials


def _allow_from_env():
allow = os.environ.get(JMP_DRIVERS_ALLOW)
match allow:
case None:
return [], False
case "UNSAFE":
return [], True
case _:
return allow.split(","), False


def _blocking_compat(f):
@wraps(f)
def wrapper(*args, **kwargs):
Expand All @@ -44,28 +34,47 @@ def wrapper(*args, **kwargs):
return wrapper


class ClientConfigV1Alpha1Drivers(BaseModel):
allow: list[str] = Field(default_factory=[])
class ClientConfigV1Alpha1Drivers(BaseSettings):
model_config = SettingsConfigDict(env_prefix="JMP_DRIVERS_")

allow: Annotated[list[str], NoDecode] = Field(default_factory=list)
unsafe: bool = Field(default=False)

@field_validator("allow", mode="before")
@classmethod
def decode_allow(cls, v: str | list[str]) -> list[str]:
if not isinstance(v, list):
return list(v.split(","))
else:
return v

class ClientConfigV1Alpha1(BaseModel):
@model_validator(mode="after")
def decode_unsafe(self) -> Self:
if "UNSAFE" in self.allow:
self.unsafe = True

return self


class ClientConfigV1Alpha1(BaseSettings):
CLIENT_CONFIGS_PATH: ClassVar[Path] = CONFIG_PATH / "clients"

model_config = SettingsConfigDict(env_prefix="JMP_")

alias: str = Field(default="default")
path: Path | None = Field(default=None)

apiVersion: Literal["jumpstarter.dev/v1alpha1"] = Field(default="jumpstarter.dev/v1alpha1")
kind: Literal["ClientConfig"] = Field(default="ClientConfig")

metadata: ObjectMeta
metadata: ObjectMeta = Field(default_factory=ObjectMeta)

endpoint: str
tls: TLSConfigV1Alpha1 = Field(default_factory=TLSConfigV1Alpha1)
token: str
grpcOptions: dict[str, str | int] | None = Field(default_factory=dict)

drivers: ClientConfigV1Alpha1Drivers
drivers: ClientConfigV1Alpha1Drivers = Field(default_factory=ClientConfigV1Alpha1Drivers)

async def channel(self):
credentials = grpc.composite_channel_credentials(
Expand Down Expand Up @@ -201,19 +210,7 @@ def try_from_env(cls):

@classmethod
def from_env(cls):
allow, unsafe = _allow_from_env()
return cls(
metadata=ObjectMeta(
namespace=os.environ.get(JMP_NAMESPACE),
name=os.environ.get(JMP_NAME),
),
endpoint=os.environ.get(JMP_ENDPOINT),
token=os.environ.get(JMP_TOKEN),
drivers=ClientConfigV1Alpha1Drivers(
allow=allow,
unsafe=unsafe,
),
)
return cls()

@classmethod
def _get_path(cls, alias: str) -> Path:
Expand Down Expand Up @@ -290,4 +287,4 @@ def dump_json(self):
def dump_yaml(self):
return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2)

model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True)
model_config = SettingsConfigDict(arbitrary_types_allowed=True, populate_by_name=True)
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ def test_client_config_from_env_allow_unsafe(monkeypatch: pytest.MonkeyPatch):
assert config.metadata.name == "testclient"
assert config.token == "dGhpc2lzYXRva2VuLTEyMzQxMjM0MTIzNEyMzQtc2Rxd3Jxd2VycXdlcnF3ZXJxd2VyLTEyMzQxMjM0MTIz"
assert config.endpoint == "jumpstarter.my-lab.com:1443"
assert config.drivers.allow == []
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why was this necessary?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Since it would now be ["UNSAFE"], but it's effectively ignored later on (as long as unsafe is True), so we don't really care about it's value.

assert config.drivers.unsafe is True


Expand Down
6 changes: 4 additions & 2 deletions packages/jumpstarter/jumpstarter/config/common.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from os import getenv
from pathlib import Path

from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
from xdg_base_dirs import xdg_config_home

from .env import JMP_CLIENT_CONFIG_HOME
Expand All @@ -10,6 +10,8 @@
CONFIG_PATH = Path(getenv(JMP_CLIENT_CONFIG_HOME, xdg_config_home() / "jumpstarter"))


class ObjectMeta(BaseModel):
class ObjectMeta(BaseSettings):
model_config = SettingsConfigDict(env_prefix="JMP_")

namespace: str | None
name: str
7 changes: 3 additions & 4 deletions packages/jumpstarter/jumpstarter/config/user_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

import pytest

from jumpstarter.config.client import ClientConfigV1Alpha1, ClientConfigV1Alpha1Drivers
from jumpstarter.config.common import ObjectMeta
from jumpstarter.config.client import ClientConfigV1Alpha1, ClientConfigV1Alpha1Drivers, ObjectMeta
from jumpstarter.config.user import UserConfigV1Alpha1, UserConfigV1Alpha1Config


Expand Down Expand Up @@ -60,7 +59,7 @@ def test_user_config_load_no_current_client(monkeypatch: pytest.MonkeyPatch):
ClientConfigV1Alpha1,
"load",
return_value=ClientConfigV1Alpha1(
name="testclient",
alias="testclient",
metadata=ObjectMeta(namespace="default", name="testclient"),
endpoint="abc",
token="123",
Expand All @@ -87,7 +86,7 @@ def test_user_config_load_current_client_empty(monkeypatch: pytest.MonkeyPatch):
ClientConfigV1Alpha1,
"load",
return_value=ClientConfigV1Alpha1(
name="testclient",
alias="testclient",
metadata=ObjectMeta(namespace="default", name="testclient"),
endpoint="abc",
token="123",
Expand Down
12 changes: 9 additions & 3 deletions packages/jumpstarter/jumpstarter/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from anyio.from_thread import start_blocking_portal

from jumpstarter.client import client_from_path
from jumpstarter.config.client import _allow_from_env
from jumpstarter.config.client import ClientConfigV1Alpha1Drivers
from jumpstarter.config.env import JUMPSTARTER_HOST


Expand All @@ -21,9 +21,15 @@ async def env_async(portal, stack):
if host is None:
raise RuntimeError(f"{JUMPSTARTER_HOST} not set")

allow, unsafe = _allow_from_env()
drivers = ClientConfigV1Alpha1Drivers()

async with client_from_path(host, portal, stack, allow=allow, unsafe=unsafe) as client:
async with client_from_path(
host,
portal,
stack,
allow=drivers.allow,
unsafe=drivers.unsafe,
) as client:
try:
yield client
finally:
Expand Down
1 change: 1 addition & 0 deletions packages/jumpstarter/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
"tqdm>=4.66.5",
"pydantic>=2.8.2",
"xdg-base-dirs>=6.0.2",
"pydantic-settings>=2.9.1",
]

[dependency-groups]
Expand Down
25 changes: 25 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading