Skip to content

Commit c7ccdb9

Browse files
authored
Add repos[].path property (#3041)
- If not set, the legacy `/workflow` path is used (only during the transitional period) - Relative paths are resolved relative to `working_dir` - `~[/path]` is supported, but `~username[/path]` is not, as with files - Available inside the container as `DSTACK_REPO_RER` env variable `working_dir` - Must be absolute - If not set, the image default value is used for tasks and services without `commands` and the legacy `/workflow` path for other configurations (replicating pre-0.19.27 `JobConfigurator` logic in CLI only during the transitional period) - `~[/path]` is supported, but `~username[/path]` is not, as with files runner - `/workflow/.venv` moved to `/dstack/venv` - `/tmp/dstack_profile` moved to `/dstack/profile` - `--working-dir` is deprecated and ignored Closes: #2851
1 parent be4d895 commit c7ccdb9

File tree

38 files changed

+641
-282
lines changed

38 files changed

+641
-282
lines changed

docs/docs/reference/dstack.yml/dev-environment.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ The `dev-environment` configuration type allows running [dev environments](../..
100100
* `volume-name:/container/path` for network volumes
101101
* `/instance/path:/container/path` for instance volumes
102102

103+
### `repos[n]` { #_repos data-toc-label="repos" }
104+
105+
> Currently, a maximum of one repo is supported.
106+
107+
> Either `local_path` or `url` must be specified.
108+
109+
#SCHEMA# dstack._internal.core.models.configurations.RepoSpec
110+
overrides:
111+
show_root_heading: false
112+
type:
113+
required: true
114+
115+
??? info "Short syntax"
116+
117+
The short syntax for repos is a colon-separated string in the form of `local_path_or_url:path`.
118+
119+
* `.:/repo`
120+
* `..:repo`
121+
* `~/repos/demo:~/repo`
122+
* `https://github.com/org/repo:~/data/repo`
123+
* `git@github.com:org/repo.git:data/repo`
124+
103125
### `files[n]` { #_files data-toc-label="files" }
104126

105127
#SCHEMA# dstack._internal.core.models.files.FilePathMapping

docs/docs/reference/dstack.yml/service.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,28 @@ The `service` configuration type allows running [services](../../concepts/servic
215215
* `volume-name:/container/path` for network volumes
216216
* `/instance/path:/container/path` for instance volumes
217217

218+
### `repos[n]` { #_repos data-toc-label="repos" }
219+
220+
> Currently, a maximum of one repo is supported.
221+
222+
> Either `local_path` or `url` must be specified.
223+
224+
#SCHEMA# dstack._internal.core.models.configurations.RepoSpec
225+
overrides:
226+
show_root_heading: false
227+
type:
228+
required: true
229+
230+
??? info "Short syntax"
231+
232+
The short syntax for repos is a colon-separated string in the form of `local_path_or_url:path`.
233+
234+
* `.:/repo`
235+
* `..:repo`
236+
* `~/repos/demo:~/repo`
237+
* `https://github.com/org/repo:~/data/repo`
238+
* `git@github.com:org/repo.git:data/repo`
239+
218240
### `files[n]` { #_files data-toc-label="files" }
219241

220242
#SCHEMA# dstack._internal.core.models.files.FilePathMapping

docs/docs/reference/dstack.yml/task.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ The `task` configuration type allows running [tasks](../../concepts/tasks.md).
100100
* `volume-name:/container/path` for network volumes
101101
* `/instance/path:/container/path` for instance volumes
102102

103+
### `repos[n]` { #_repos data-toc-label="repos" }
104+
105+
> Currently, a maximum of one repo is supported.
106+
107+
> Either `local_path` or `url` must be specified.
108+
109+
#SCHEMA# dstack._internal.core.models.configurations.RepoSpec
110+
overrides:
111+
show_root_heading: false
112+
type:
113+
required: true
114+
115+
??? info "Short syntax"
116+
117+
The short syntax for repos is a colon-separated string in the form of `local_path_or_url:path`.
118+
119+
* `.:/repo`
120+
* `..:repo`
121+
* `~/repos/demo:~/repo`
122+
* `https://github.com/org/repo:~/data/repo`
123+
* `git@github.com:org/repo.git:data/repo`
124+
103125
### `files[n]` { #_files data-toc-label="files" }
104126

105127
#SCHEMA# dstack._internal.core.models.files.FilePathMapping

examples/misc/airflow/dags/dstack_tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def dstack_cli_apply_venv() -> str:
4747
dstack is installed into a separate virtual environment available to Airflow.
4848
"""
4949
return (
50-
f"source {DSTACK_VENV_PATH}/bin/activate"
50+
f". {DSTACK_VENV_PATH}/bin/activate"
5151
f" && cd {DSTACK_REPO_PATH}"
5252
" && dstack apply -y -f task.dstack.yml --repo ."
5353
)

runner/cmd/runner/cmd.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import (
44
"log"
55
"os"
66

7+
"github.com/dstackai/dstack/runner/consts"
78
"github.com/urfave/cli/v2"
89
)
910

1011
// Version is a build-time variable. The value is overridden by ldflags.
1112
var Version string
1213

1314
func App() {
14-
var paths struct{ tempDir, homeDir, workingDir string }
15+
var tempDir string
16+
var homeDir string
1517
var httpPort int
1618
var sshPort int
1719
var logLevel int
@@ -37,36 +39,36 @@ func App() {
3739
&cli.PathFlag{
3840
Name: "temp-dir",
3941
Usage: "Temporary directory for logs and other files",
40-
Required: true,
41-
Destination: &paths.tempDir,
42+
Value: consts.RunnerTempDir,
43+
Destination: &tempDir,
4244
},
4345
&cli.PathFlag{
4446
Name: "home-dir",
4547
Usage: "HomeDir directory for credentials and $HOME",
46-
Required: true,
47-
Destination: &paths.homeDir,
48+
Value: consts.RunnerHomeDir,
49+
Destination: &homeDir,
4850
},
51+
// TODO: Not used, left for compatibility with old servers. Remove eventually.
4952
&cli.PathFlag{
5053
Name: "working-dir",
51-
Usage: "Base path for the job",
52-
Required: true,
53-
Destination: &paths.workingDir,
54+
Hidden: true,
55+
Destination: nil,
5456
},
5557
&cli.IntFlag{
5658
Name: "http-port",
5759
Usage: "Set a http port",
58-
Value: 10999,
60+
Value: consts.RunnerHTTPPort,
5961
Destination: &httpPort,
6062
},
6163
&cli.IntFlag{
6264
Name: "ssh-port",
6365
Usage: "Set the ssh port",
64-
Required: true,
66+
Value: consts.RunnerSSHPort,
6567
Destination: &sshPort,
6668
},
6769
},
6870
Action: func(c *cli.Context) error {
69-
err := start(paths.tempDir, paths.homeDir, paths.workingDir, httpPort, sshPort, logLevel, Version)
71+
err := start(tempDir, homeDir, httpPort, sshPort, logLevel, Version)
7072
if err != nil {
7173
return cli.Exit(err, 1)
7274
}

runner/cmd/runner/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func main() {
1919
App()
2020
}
2121

22-
func start(tempDir string, homeDir string, workingDir string, httpPort int, sshPort int, logLevel int, version string) error {
22+
func start(tempDir string, homeDir string, httpPort int, sshPort int, logLevel int, version string) error {
2323
if err := os.MkdirAll(tempDir, 0o755); err != nil {
2424
return tracerr.Errorf("Failed to create temp directory: %w", err)
2525
}
@@ -38,7 +38,7 @@ func start(tempDir string, homeDir string, workingDir string, httpPort int, sshP
3838
log.DefaultEntry.Logger.SetOutput(io.MultiWriter(os.Stdout, defaultLogFile))
3939
log.DefaultEntry.Logger.SetLevel(logrus.Level(logLevel))
4040

41-
server, err := api.NewServer(tempDir, homeDir, workingDir, fmt.Sprintf(":%d", httpPort), sshPort, version)
41+
server, err := api.NewServer(tempDir, homeDir, fmt.Sprintf(":%d", httpPort), sshPort, version)
4242
if err != nil {
4343
return tracerr.Errorf("Failed to create server: %w", err)
4444
}

runner/cmd/shim/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,14 @@ func main() {
7979
&cli.IntFlag{
8080
Name: "runner-http-port",
8181
Usage: "Set runner's http port",
82-
Value: 10999,
82+
Value: consts.RunnerHTTPPort,
8383
Destination: &args.Runner.HTTPPort,
8484
EnvVars: []string{"DSTACK_RUNNER_HTTP_PORT"},
8585
},
8686
&cli.IntFlag{
8787
Name: "runner-ssh-port",
8888
Usage: "Set runner's ssh port",
89-
Value: 10022,
89+
Value: consts.RunnerSSHPort,
9090
Destination: &args.Runner.SSHPort,
9191
EnvVars: []string{"DSTACK_RUNNER_SSH_PORT"},
9292
},

runner/consts/consts.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ const (
3030
RunnerWorkingDir = "/workflow"
3131
)
3232

33+
const (
34+
RunnerHTTPPort = 10999
35+
RunnerSSHPort = 10022
36+
)
37+
3338
const ShimLogFileName = "shim.log"

runner/internal/common/utils.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package common
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"path"
8+
"slices"
9+
10+
"github.com/dstackai/dstack/runner/internal/log"
11+
)
12+
13+
func ExpandPath(pth string, base string, home string) (string, error) {
14+
pth = path.Clean(pth)
15+
if pth == "~" {
16+
return path.Clean(home), nil
17+
}
18+
if len(pth) >= 2 && pth[0] == '~' {
19+
if pth[1] == '/' {
20+
return path.Join(home, pth[2:]), nil
21+
}
22+
return "", errors.New("~username syntax is not supported")
23+
}
24+
if base != "" && !path.IsAbs(pth) {
25+
return path.Join(base, pth), nil
26+
}
27+
return pth, nil
28+
}
29+
30+
func MkdirAll(ctx context.Context, pth string, uid int, gid int) error {
31+
paths := []string{pth}
32+
for {
33+
pth = path.Dir(pth)
34+
if pth == "/" {
35+
break
36+
}
37+
paths = append(paths, pth)
38+
}
39+
for _, p := range slices.Backward(paths) {
40+
if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) {
41+
if err := os.Mkdir(p, 0o755); err != nil {
42+
return err
43+
}
44+
if uid != -1 || gid != -1 {
45+
if err := os.Chown(p, uid, gid); err != nil {
46+
log.Warning(ctx, "Failed to chown", "path", p, "err", err)
47+
}
48+
}
49+
} else if err != nil {
50+
return err
51+
}
52+
}
53+
return nil
54+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package common
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestExpandPath_NoPath_NoBase(t *testing.T) {
10+
path, err := ExpandPath("", "", "")
11+
require.NoError(t, err)
12+
require.Equal(t, ".", path)
13+
}
14+
15+
func TestExpandPath_NoPath_RelBase(t *testing.T) {
16+
path, err := ExpandPath("", "repo", "")
17+
require.NoError(t, err)
18+
require.Equal(t, "repo", path)
19+
}
20+
21+
func TestExpandPath_NoPath_AbsBase(t *testing.T) {
22+
path, err := ExpandPath("", "/repo", "")
23+
require.NoError(t, err)
24+
require.Equal(t, "/repo", path)
25+
}
26+
27+
func TestExpandtPath_RelPath_NoBase(t *testing.T) {
28+
path, err := ExpandPath("repo", "", "")
29+
require.NoError(t, err)
30+
require.Equal(t, "repo", path)
31+
}
32+
33+
func TestExpandtPath_RelPath_RelBase(t *testing.T) {
34+
path, err := ExpandPath("repo", "data", "")
35+
require.NoError(t, err)
36+
require.Equal(t, "data/repo", path)
37+
}
38+
39+
func TestExpandtPath_RelPath_AbsBase(t *testing.T) {
40+
path, err := ExpandPath("repo", "/data", "")
41+
require.NoError(t, err)
42+
require.Equal(t, "/data/repo", path)
43+
}
44+
45+
func TestExpandtPath_AbsPath_NoBase(t *testing.T) {
46+
path, err := ExpandPath("/repo", "", "")
47+
require.NoError(t, err)
48+
require.Equal(t, "/repo", path)
49+
}
50+
51+
func TestExpandtPath_AbsPath_RelBase(t *testing.T) {
52+
path, err := ExpandPath("/repo", "data", "")
53+
require.NoError(t, err)
54+
require.Equal(t, "/repo", path)
55+
}
56+
57+
func TestExpandtPath_AbsPath_AbsBase(t *testing.T) {
58+
path, err := ExpandPath("/repo", "/data", "")
59+
require.NoError(t, err)
60+
require.Equal(t, "/repo", path)
61+
}
62+
63+
func TestExpandPath_BareTilde_NoHome(t *testing.T) {
64+
path, err := ExpandPath("~", "", "")
65+
require.NoError(t, err)
66+
require.Equal(t, ".", path)
67+
}
68+
69+
func TestExpandPath_BareTilde_RelHome(t *testing.T) {
70+
path, err := ExpandPath("~", "", "user")
71+
require.NoError(t, err)
72+
require.Equal(t, "user", path)
73+
}
74+
75+
func TestExpandPath_BareTilde_AbsHome(t *testing.T) {
76+
path, err := ExpandPath("~", "", "/home/user")
77+
require.NoError(t, err)
78+
require.Equal(t, "/home/user", path)
79+
}
80+
81+
func TestExpandtPath_TildeWithPath_NoHome(t *testing.T) {
82+
path, err := ExpandPath("~/repo", "", "")
83+
require.NoError(t, err)
84+
require.Equal(t, "repo", path)
85+
}
86+
87+
func TestExpandtPath_TildeWithPath_RelHome(t *testing.T) {
88+
path, err := ExpandPath("~/repo", "", "user")
89+
require.NoError(t, err)
90+
require.Equal(t, "user/repo", path)
91+
}
92+
93+
func TestExpandtPath_TildeWithPath_AbsHome(t *testing.T) {
94+
path, err := ExpandPath("~/repo", "", "/home/user")
95+
require.NoError(t, err)
96+
require.Equal(t, "/home/user/repo", path)
97+
}
98+
99+
func TestExpandtPath_ErrorTildeUsernameNotSupported_BareTildeUsername(t *testing.T) {
100+
path, err := ExpandPath("~username", "", "")
101+
require.ErrorContains(t, err, "~username syntax is not supported")
102+
require.Equal(t, "", path)
103+
}
104+
105+
func TestExpandtPath_ErrorTildeUsernameNotSupported_TildeUsernameWithPath(t *testing.T) {
106+
path, err := ExpandPath("~username/repo", "", "")
107+
require.ErrorContains(t, err, "~username syntax is not supported")
108+
require.Equal(t, "", path)
109+
}

0 commit comments

Comments
 (0)