Skip to content

Commit 6b7cd8b

Browse files
[Feature]: Store user SSH key on the server (#3176)
* [Feature]: Store user SSH key on the server #2053 * [Feature]: Store user SSH key on the server #2053 Fixing tests * [Feature]: Store user SSH key on the server #2053 Bugfix * Update src/dstack/_internal/server/services/users.py Co-authored-by: jvstme <36324149+jvstme@users.noreply.github.com> * [Feature]: Store user SSH key on the server #2053 Review: move `ssh_public_key` to `User` model (from `UserWithCreds`) * [Feature]: Store user SSH key on the server #2053 Review --------- Co-authored-by: jvstme <36324149+jvstme@users.noreply.github.com>
1 parent de03bf5 commit 6b7cd8b

File tree

21 files changed

+205
-61
lines changed

21 files changed

+205
-61
lines changed

src/dstack/_internal/cli/commands/offer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ def _command(self, args: argparse.Namespace):
104104

105105
run_spec = RunSpec(
106106
configuration=conf,
107-
ssh_key_pub="(dummy)",
108107
profile=profile,
108+
ssh_key_pub="(dummy)", # TODO: Remove since 0.19.40
109109
)
110110

111111
if args.group_by:

src/dstack/_internal/cli/services/configurators/run.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
from dstack._internal.utils.logging import get_logger
6363
from dstack._internal.utils.nested_list import NestedList, NestedListItem
6464
from dstack._internal.utils.path import is_absolute_posix_path
65-
from dstack.api._public.repos import get_ssh_keypair
6665
from dstack.api._public.runs import Run
6766
from dstack.api.server import APIClient
6867
from dstack.api.utils import load_profile
@@ -135,17 +134,14 @@ def apply_configuration(
135134

136135
config_manager = ConfigManager()
137136
repo = self.get_repo(conf, configuration_path, configurator_args, config_manager)
138-
self.api.ssh_identity_file = get_ssh_keypair(
139-
configurator_args.ssh_identity_file,
140-
config_manager.dstack_key_path,
141-
)
142137
profile = load_profile(Path.cwd(), configurator_args.profile)
143138
with console.status("Getting apply plan..."):
144139
run_plan = self.api.runs.get_run_plan(
145140
configuration=conf,
146141
repo=repo,
147142
configuration_path=configuration_path,
148143
profile=profile,
144+
ssh_identity_file=configurator_args.ssh_identity_file,
149145
)
150146

151147
print_run_plan(run_plan, max_offers=configurator_args.max_offers)

src/dstack/_internal/core/backends/kubernetes/compute.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ def run_job(
161161
volumes: List[Volume],
162162
) -> JobProvisioningData:
163163
instance_name = generate_unique_instance_name_for_job(run, job)
164+
assert run.run_spec.ssh_key_pub is not None
164165
commands = get_docker_commands(
165166
[run.run_spec.ssh_key_pub.strip(), project_ssh_public_key.strip()]
166167
)

src/dstack/_internal/core/backends/runpod/compute.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def run_job(
8989
project_ssh_private_key: str,
9090
volumes: List[Volume],
9191
) -> JobProvisioningData:
92+
assert run.run_spec.ssh_key_pub is not None
9293
instance_config = InstanceConfiguration(
9394
project_name=run.project_name,
9495
instance_name=get_job_instance_name(run, job),

src/dstack/_internal/core/backends/vastai/compute.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def run_job(
8585
instance_name = generate_unique_instance_name_for_job(
8686
run, job, max_length=MAX_INSTANCE_NAME_LEN
8787
)
88+
assert run.run_spec.ssh_key_pub is not None
8889
commands = get_docker_commands(
8990
[run.run_spec.ssh_key_pub.strip(), project_ssh_public_key.strip()]
9091
)

src/dstack/_internal/core/models/runs.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,11 +462,12 @@ class RunSpec(generate_dual_core_model(RunSpecConfig)):
462462
configuration: Annotated[AnyRunConfiguration, Field(discriminator="type")]
463463
profile: Annotated[Optional[Profile], Field(description="The profile parameters")] = None
464464
ssh_key_pub: Annotated[
465-
str,
465+
Optional[str],
466466
Field(
467467
description="The contents of the SSH public key that will be used to connect to the run."
468+
" Can be empty only before the run is submitted."
468469
),
469-
]
470+
] = None
470471
# merged_profile stores profile parameters merged from profile and configuration.
471472
# Read profile parameters from merged_profile instead of profile directly.
472473
# TODO: make merged_profile a computed field after migrating to pydanticV2

src/dstack/_internal/core/models/users.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class User(CoreModel):
3030
email: Optional[str]
3131
active: bool
3232
permissions: UserPermissions
33+
ssh_public_key: Optional[str] = None
3334

3435

3536
class UserTokenCreds(CoreModel):
@@ -38,3 +39,4 @@ class UserTokenCreds(CoreModel):
3839

3940
class UserWithCreds(User):
4041
creds: UserTokenCreds
42+
ssh_private_key: Optional[str] = None

src/dstack/_internal/core/services/configs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def dstack_ssh_dir(self) -> Path:
117117

118118
@property
119119
def dstack_key_path(self) -> Path:
120+
# TODO: Remove since 0.19.40
120121
return self.dstack_ssh_dir / "id_rsa"
121122

122123
@property

src/dstack/_internal/server/background/tasks/process_running_jobs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ async def _process_running_job(session: AsyncSession, job_model: JobModel):
243243
job_submission.age,
244244
)
245245
ssh_user = job_provisioning_data.username
246+
assert run.run_spec.ssh_key_pub is not None
246247
user_ssh_key = run.run_spec.ssh_key_pub.strip()
247248
public_keys = [project.ssh_public_key.strip(), user_ssh_key]
248249
if job_provisioning_data.backend == BackendType.LOCAL:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""user.ssh_key
2+
3+
Revision ID: ff1d94f65b08
4+
Revises: 2498ab323443
5+
Create Date: 2025-10-09 20:31:31.166786
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "ff1d94f65b08"
14+
down_revision = "2498ab323443"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
with op.batch_alter_table("users", schema=None) as batch_op:
22+
batch_op.add_column(sa.Column("ssh_private_key", sa.Text(), nullable=True))
23+
batch_op.add_column(sa.Column("ssh_public_key", sa.Text(), nullable=True))
24+
25+
# ### end Alembic commands ###
26+
27+
28+
def downgrade() -> None:
29+
# ### commands auto generated by Alembic - please adjust! ###
30+
with op.batch_alter_table("users", schema=None) as batch_op:
31+
batch_op.drop_column("ssh_public_key")
32+
batch_op.drop_column("ssh_private_key")
33+
34+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)