Skip to content
Open
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
24 changes: 23 additions & 1 deletion docs/docs/concepts/dev-environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Provisioning remote instances for cloud-based development

# Dev environments

A dev environment lets you provision an instance and access it with your desktop IDE.
A dev environment lets you provision an instance and access it with your desktop IDE or SSH.

??? info "Prerequisites"
Before running a dev environment, make sure you’ve [installed](../installation.md) the server and CLI, and created a [fleet](fleets.md).
Expand All @@ -25,6 +25,8 @@ name: vscode
python: "3.11"
# Uncomment to use a custom Docker image
#image: huggingface/trl-latest-gpu

# Comment if not required
ide: vscode

# Uncomment to leverage spot instances
Expand Down Expand Up @@ -55,12 +57,32 @@ Launching `vscode`...

To open in VS Code Desktop, use this link:
vscode://vscode-remote/ssh-remote+vscode/workflow

To connect via SSH, use: `ssh vscode`
```

</div>

`dstack apply` automatically provisions an instance and sets up an IDE on it.

??? info "SSH-only"
The `ide` property is optional. If omitted, no IDE is pre-installed, but the dev environment
is still accessible via SSH:

<div editor-title=".dstack.yml">

```yaml
type: dev-environment
name: my-env

python: "3.11"

resources:
gpu: 24GB
```

</div>

??? info "Windows"
On Windows, `dstack` works both natively and inside WSL. But, for dev environments,
it's recommended _not to use_ `dstack apply` _inside WSL_ due to a [VS Code issue](https://github.com/microsoft/vscode-remote-release/issues/937).
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@
"name_constraint": "Example: 'my-fleet' or 'default'. If not specified, generated automatically.",
"name_placeholder": "Optional",
"ide": "IDE",
"ide_description": "Select which IDE would you like to use with the dev environment.",
"ide_description": "Optionally select an IDE to pre-install in the dev environment.",
"docker": "Docker",
"docker_image": "Image",
"docker_image_description": "A Docker image name, e.g. 'lmsysorg/sglang:latest'",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run })
const configuration = run.run_spec.configuration as TDevEnvironmentConfiguration;
const latestSubmission = run.jobs[0]?.job_submissions?.slice(-1)[0];
const workingDir = latestSubmission?.job_runtime_data?.working_dir ?? '/';
const openInIDEUrl = `${configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}${workingDir}`;
const ideDisplayName = getIDEDisplayName(configuration.ide);
const hasIDE = !!configuration.ide;
const openInIDEUrl = hasIDE
? `${configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}${workingDir}`
: undefined;
const ideDisplayName = hasIDE ? getIDEDisplayName(configuration.ide!) : undefined;

const [configCliCommand, copyCliCommand] = useConfigProjectCliCommand({ projectName: run.project_name });

Expand Down Expand Up @@ -210,52 +213,82 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run })
),
isOptional: true,
},
{
title: 'Open',
description: `After the CLI is attached, you can open the dev environment in ${ideDisplayName}.`,
content: (
<SpaceBetween size="s">
<Button
variant="primary"
external={true}
onClick={() => window.open(openInIDEUrl, '_blank')}
>
Open in {ideDisplayName}
</Button>
hasIDE
? {
title: 'Open',
description: `After the CLI is attached, you can open the dev environment in ${ideDisplayName}.`,
content: (
<SpaceBetween size="s">
<Button
variant="primary"
external={true}
onClick={() => window.open(openInIDEUrl, '_blank')}
>
Open in {ideDisplayName}
</Button>

<ExpandableSection headerText="Need plain SSH?">
<SpaceBetween size="s">
<Box />
<div className={styles.codeWrapper}>
<Code className={styles.code}>{sshCommand}</Code>
<ExpandableSection headerText="Need plain SSH?">
<SpaceBetween size="s">
<Box />
<div className={styles.codeWrapper}>
<Code className={styles.code}>{sshCommand}</Code>

<div className={styles.copy}>
<Popover
dismissButton={false}
position="top"
size="small"
triggerType="custom"
content={
<StatusIndicator type="success">
{t('common.copied')}
</StatusIndicator>
}
>
<Button
formAction="none"
iconName="copy"
variant="normal"
onClick={() => copySSHCommand()}
/>
</Popover>
</div>
</div>
</SpaceBetween>
</ExpandableSection>
</SpaceBetween>
),
isOptional: true,
},
<div className={styles.copy}>
<Popover
dismissButton={false}
position="top"
size="small"
triggerType="custom"
content={
<StatusIndicator type="success">
{t('common.copied')}
</StatusIndicator>
}
>
<Button
formAction="none"
iconName="copy"
variant="normal"
onClick={() => copySSHCommand()}
/>
</Popover>
</div>
</div>
</SpaceBetween>
</ExpandableSection>
</SpaceBetween>
),
isOptional: true,
}
: {
title: 'Connect via SSH',
description: 'After the CLI is attached, you can connect to the dev environment via SSH.',
content: (
<div className={styles.codeWrapper}>
<Code className={styles.code}>{sshCommand}</Code>

<div className={styles.copy}>
<Popover
dismissButton={false}
position="top"
size="small"
triggerType="custom"
content={
<StatusIndicator type="success">{t('common.copied')}</StatusIndicator>
}
>
<Button
formAction="none"
iconName="copy"
variant="normal"
onClick={() => copySSHCommand()}
/>
</Popover>
</div>
</div>
),
isOptional: true,
},
]}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ export const ParamsWizardStep: React.FC<ParamsWizardStepProps> = ({ formMethods,
return null;
}

const templateIde =
template?.configuration && 'ide' in template.configuration
? ((template.configuration as TDevEnvironmentConfiguration).ide ?? '')
: '';

return (
<FormSelect
label={t('runs.launch.wizard.ide')}
Expand All @@ -102,7 +107,7 @@ export const ParamsWizardStep: React.FC<ParamsWizardStepProps> = ({ formMethods,
name={FORM_FIELD_NAMES.ide}
options={IDE_OPTIONS}
disabled={loading}
defaultValue={'cursor'}
defaultValue={templateIde}
/>
);
};
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/pages/Runs/Launch/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export const FORM_FIELD_NAMES = {
} as const satisfies Record<IRunEnvironmentFormKeys, IRunEnvironmentFormKeys>;

export const IDE_OPTIONS = [
{
label: 'None',
value: '',
},
{
label: 'Cursor',
value: 'cursor',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const useYupValidationResolver = (template?: ITemplate) => {
break;

case 'ide':
schema['ide'] = yup.string().required(requiredFieldError);
schema['ide'] = yup.string().nullable();
break;

case 'resources':
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/Runs/Launch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface IRunEnvironmentFormValues {
gpu_enabled?: boolean;
offer?: IGpu;
name: string;
ide: 'cursor' | 'vscode' | 'windsurf';
ide?: 'cursor' | 'vscode' | 'windsurf';
config_yaml: string;
image?: string;
python?: string;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/types/run.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ declare type TServiceConfiguration = TBaseConfiguration & {

declare type TDevEnvironmentConfiguration = TBaseConfiguration & {
type?: 'dev-environment';
ide: TIde;
ide?: TIde | null;
version?: string;
init?: string[];
inactivity_duration?: string | number | boolean | 'off';
Expand Down
10 changes: 6 additions & 4 deletions src/dstack/_internal/core/models/configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,11 +645,11 @@ def check_image_or_commands_present(cls, values):

class DevEnvironmentConfigurationParams(CoreModel):
ide: Annotated[
Union[Literal["vscode"], Literal["cursor"], Literal["windsurf"]],
Optional[Union[Literal["vscode"], Literal["cursor"], Literal["windsurf"]]],
Field(
description="The IDE to run. Supported values include `vscode`, `cursor`, and `windsurf`"
description="The IDE to pre-install. Supported values include `vscode`, `cursor`, and `windsurf`. Defaults to no IDE (SSH only)"
),
]
] = None
version: Annotated[
Optional[str],
Field(
Expand Down Expand Up @@ -683,9 +683,11 @@ def parse_inactivity_duration(
return None

@root_validator
def validate_windsurf_version_format(cls, values):
def validate_ide_and_version(cls, values):
ide = values.get("ide")
version = values.get("version")
if version and ide is None:
raise ValueError("`version` requires `ide` to be set")
if ide == "windsurf" and version:
# Validate format: version@commit
if not re.match(r"^.+@[a-f0-9]+$", version):
Expand Down
34 changes: 20 additions & 14 deletions src/dstack/_internal/server/services/jobs/configurators/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,36 @@ def __init__(
):
assert run_spec.configuration.type == "dev-environment"

if run_spec.configuration.ide == "vscode":
__class = VSCodeDesktop
elif run_spec.configuration.ide == "cursor":
__class = CursorDesktop
elif run_spec.configuration.ide == "windsurf":
__class = WindsurfDesktop
if run_spec.configuration.ide is None:
self.ide = None
else:
raise ServerClientError(f"Unsupported IDE: {run_spec.configuration.ide}")
self.ide = __class(
run_name=run_spec.run_name,
version=run_spec.configuration.version,
extensions=["ms-python.python", "ms-toolsai.jupyter"],
)
if run_spec.configuration.ide == "vscode":
__class = VSCodeDesktop
elif run_spec.configuration.ide == "cursor":
__class = CursorDesktop
elif run_spec.configuration.ide == "windsurf":
__class = WindsurfDesktop
else:
raise ServerClientError(f"Unsupported IDE: {run_spec.configuration.ide}")
self.ide = __class(
run_name=run_spec.run_name,
version=run_spec.configuration.version,
extensions=["ms-python.python", "ms-toolsai.jupyter"],
)
super().__init__(run_spec=run_spec, secrets=secrets, replica_group_name=replica_group_name)

def _shell_commands(self) -> List[str]:
assert self.run_spec.configuration.type == "dev-environment"

commands = self.ide.get_install_commands()
commands = []
if self.ide is not None:
commands += self.ide.get_install_commands()
commands.append(INSTALL_IPYKERNEL)
commands += self.run_spec.configuration.setup
commands.append("echo")
commands += self.run_spec.configuration.init
commands += self.ide.get_print_readme_commands()
if self.ide is not None:
commands += self.ide.get_print_readme_commands()
commands += [
f"echo 'To connect via SSH, use: `ssh {self.run_spec.run_name}`'",
"echo",
Expand Down
9 changes: 9 additions & 0 deletions src/tests/_internal/core/models/test_configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,12 @@ def test_cursor_version_not_validated(self):
params = DevEnvironmentConfigurationParams(ide="cursor", version="0.40.0")
assert params.ide == "cursor"
assert params.version == "0.40.0"

def test_ide_optional(self):
params = DevEnvironmentConfigurationParams()
assert params.ide is None
assert params.version is None

def test_version_requires_ide(self):
with pytest.raises(ValueError, match="`version` requires `ide` to be set"):
DevEnvironmentConfigurationParams(version="1.80.0")
Loading