Skip to content

Commit ffc6061

Browse files
committed
feat: subpath mounts
Signed-off-by: Vedran Jukic <[email protected]>
1 parent 78d6a11 commit ffc6061

File tree

19 files changed

+220
-22
lines changed

19 files changed

+220
-22
lines changed

.devcontainer/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ RUN apt update && export DEBIAN_FRONTEND=noninteractive \
1313
RUN apt update && export DEBIAN_FRONTEND=noninteractive \
1414
&& apt -y install --no-install-recommends openjdk-11-jdk protobuf-compiler libprotobuf-dev
1515

16+
# Mount-s3 (AWS S3 FUSE driver)
17+
RUN wget https://s3.amazonaws.com/mountpoint-s3-release/1.20.0/x86_64/mount-s3-1.20.0-x86_64.deb \
18+
&& apt install -y ./mount-s3-1.20.0-x86_64.deb \
19+
&& rm -f mount-s3-1.20.0-x86_64.deb
20+
1621
# Telepresence
1722
RUN curl -fL https://app.getambassador.io/download/tel2oss/releases/download/v2.17.0/telepresence-linux-${TARGETARCH} -o /usr/local/bin/telepresence && \
1823
chmod a+x /usr/local/bin/telepresence

apps/api/src/sandbox/dto/sandbox.dto.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export class SandboxVolume {
2525
example: '/data',
2626
})
2727
mountPath: string
28+
29+
@ApiPropertyOptional({
30+
description:
31+
'Optional subpath within the volume to mount. When specified, only this S3 prefix will be accessible. When omitted, the entire volume is mounted.',
32+
example: 'users/alice',
33+
})
34+
subpath?: string
2835
}
2936

3037
@ApiSchema({ name: 'Sandbox' })

apps/api/src/sandbox/runner-adapter/runnerAdapter.legacy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export class RunnerAdapterLegacy implements RunnerAdapter {
193193
volumes: sandbox.volumes?.map((volume) => ({
194194
volumeId: volume.volumeId,
195195
mountPath: volume.mountPath,
196+
subpath: volume.subpath,
196197
})),
197198
networkBlockAll: sandbox.networkBlockAll,
198199
networkAllowList: sandbox.networkAllowList,

apps/runner/pkg/api/docs/docs.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/runner/pkg/api/docs/swagger.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/runner/pkg/api/docs/swagger.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/runner/pkg/api/dto/volume.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ package dto
66
type VolumeDTO struct {
77
VolumeId string `json:"volumeId"`
88
MountPath string `json:"mountPath"`
9+
Subpath string `json:"subpath,omitempty"`
910
}

apps/runner/pkg/docker/volumes_mountpaths.go

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ package docker
55

66
import (
77
"context"
8+
"crypto/md5"
9+
"encoding/hex"
810
"fmt"
911
"io"
1012
"os"
1113
"os/exec"
1214
"path/filepath"
15+
"strings"
1316
"sync"
1417

1518
"github.com/daytonaio/runner/cmd/runner/config"
@@ -23,14 +26,17 @@ func (d *DockerClient) getVolumesMountPathBinds(ctx context.Context, volumes []d
2326

2427
for _, vol := range volumes {
2528
volumeIdPrefixed := fmt.Sprintf("daytona-volume-%s", vol.VolumeId)
26-
runnerVolumeMountPath := d.getRunnerVolumeMountPath(volumeIdPrefixed)
29+
runnerVolumeMountPath := d.getRunnerVolumeMountPath(volumeIdPrefixed, vol.Subpath)
2730

28-
// Get or create mutex for this volume
31+
// Create unique key for this volume+subpath combination for mutex
32+
volumeKey := d.getVolumeKey(volumeIdPrefixed, vol.Subpath)
33+
34+
// Get or create mutex for this volume+subpath
2935
d.volumeMutexesMutex.Lock()
30-
volumeMutex, exists := d.volumeMutexes[volumeIdPrefixed]
36+
volumeMutex, exists := d.volumeMutexes[volumeKey]
3137
if !exists {
3238
volumeMutex = &sync.Mutex{}
33-
d.volumeMutexes[volumeIdPrefixed] = volumeMutex
39+
d.volumeMutexes[volumeKey] = volumeMutex
3440
}
3541
d.volumeMutexesMutex.Unlock()
3642

@@ -39,7 +45,7 @@ func (d *DockerClient) getVolumesMountPathBinds(ctx context.Context, volumes []d
3945
defer volumeMutex.Unlock()
4046

4147
if d.isDirectoryMounted(runnerVolumeMountPath) {
42-
log.Infof("volume %s is already mounted to %s", volumeIdPrefixed, runnerVolumeMountPath)
48+
log.Infof("volume %s (subpath: %s) is already mounted to %s", volumeIdPrefixed, vol.Subpath, runnerVolumeMountPath)
4349
volumeMountPathBinds = append(volumeMountPathBinds, fmt.Sprintf("%s/:%s/", runnerVolumeMountPath, vol.MountPath))
4450
continue
4551
}
@@ -49,40 +55,69 @@ func (d *DockerClient) getVolumesMountPathBinds(ctx context.Context, volumes []d
4955
return nil, fmt.Errorf("failed to create mount directory %s: %s", runnerVolumeMountPath, err)
5056
}
5157

52-
log.Infof("mounting S3 volume %s to %s", volumeIdPrefixed, runnerVolumeMountPath)
58+
log.Infof("mounting S3 volume %s (subpath: %s) to %s", volumeIdPrefixed, vol.Subpath, runnerVolumeMountPath)
5359

54-
cmd := d.getMountCmd(ctx, volumeIdPrefixed, runnerVolumeMountPath)
60+
cmd := d.getMountCmd(ctx, volumeIdPrefixed, vol.Subpath, runnerVolumeMountPath)
5561
err = cmd.Run()
5662
if err != nil {
57-
return nil, fmt.Errorf("failed to mount S3 volume %s to %s: %s", volumeIdPrefixed, runnerVolumeMountPath, err)
63+
return nil, fmt.Errorf("failed to mount S3 volume %s (subpath: %s) to %s: %s", volumeIdPrefixed, vol.Subpath, runnerVolumeMountPath, err)
5864
}
5965

60-
log.Infof("mounted S3 volume %s to %s", volumeIdPrefixed, runnerVolumeMountPath)
66+
log.Infof("mounted S3 volume %s (subpath: %s) to %s", volumeIdPrefixed, vol.Subpath, runnerVolumeMountPath)
6167

6268
volumeMountPathBinds = append(volumeMountPathBinds, fmt.Sprintf("%s/:%s/", runnerVolumeMountPath, vol.MountPath))
6369
}
6470

6571
return volumeMountPathBinds, nil
6672
}
6773

68-
func (d *DockerClient) getRunnerVolumeMountPath(volumeId string) string {
69-
volumePath := filepath.Join("/mnt", volumeId)
74+
func (d *DockerClient) getRunnerVolumeMountPath(volumeId, subpath string) string {
75+
// If subpath is provided, create a unique mount point for this volume+subpath combination
76+
mountDirName := volumeId
77+
if subpath != "" {
78+
// Create a short hash of the subpath to keep the path reasonable
79+
hash := md5.Sum([]byte(subpath))
80+
hashStr := hex.EncodeToString(hash[:])[:8]
81+
mountDirName = fmt.Sprintf("%s-%s", volumeId, hashStr)
82+
}
83+
84+
volumePath := filepath.Join("/mnt", mountDirName)
7085
if config.GetEnvironment() == "development" {
71-
volumePath = filepath.Join("/tmp", volumeId)
86+
volumePath = filepath.Join("/tmp", mountDirName)
7287
}
7388

7489
return volumePath
7590
}
7691

92+
func (d *DockerClient) getVolumeKey(volumeId, subpath string) string {
93+
if subpath == "" {
94+
return volumeId
95+
}
96+
return fmt.Sprintf("%s:%s", volumeId, subpath)
97+
}
98+
7799
func (d *DockerClient) isDirectoryMounted(path string) bool {
78100
cmd := exec.Command("mountpoint", path)
79101
_, err := cmd.Output()
80102

81103
return err == nil
82104
}
83105

84-
func (d *DockerClient) getMountCmd(ctx context.Context, volume, path string) *exec.Cmd {
85-
cmd := exec.CommandContext(ctx, "mount-s3", "--allow-other", "--allow-delete", "--allow-overwrite", "--file-mode", "0666", "--dir-mode", "0777", volume, path)
106+
func (d *DockerClient) getMountCmd(ctx context.Context, volume, subpath, path string) *exec.Cmd {
107+
args := []string{"--allow-other", "--allow-delete", "--allow-overwrite", "--file-mode", "0666", "--dir-mode", "0777"}
108+
109+
if subpath != "" {
110+
// Ensure subpath ends with /
111+
prefix := subpath
112+
if !strings.HasSuffix(prefix, "/") {
113+
prefix = prefix + "/"
114+
}
115+
args = append(args, "--prefix", prefix)
116+
}
117+
118+
args = append(args, volume, path)
119+
120+
cmd := exec.CommandContext(ctx, "mount-s3", args...)
86121

87122
if d.awsEndpointUrl != "" {
88123
cmd.Env = append(cmd.Env, "AWS_ENDPOINT_URL="+d.awsEndpointUrl)

examples/python/volumes/_async/volume.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,28 @@ async def main():
4444
file = await sandbox2.fs.download_file(os.path.join(mount_dir_2, "new-file.txt"))
4545
print("File:", file)
4646

47+
# Mount a specific subpath within the volume
48+
# This is useful for isolating data or implementing multi-tenancy
49+
mount_dir_3 = "/home/daytona/subpath"
50+
51+
params = CreateSandboxFromSnapshotParams(
52+
language="python",
53+
volumes=[VolumeMount(volumeId=volume.id, mountPath=mount_dir_3, subpath="users/alice")],
54+
)
55+
sandbox3 = await daytona.create(params)
56+
57+
# This sandbox will only see files within the 'users/alice' subpath
58+
# Create a file in the subpath
59+
subpath_file = os.path.join(mount_dir_3, "alice-file.txt")
60+
await sandbox3.fs.upload_file(b"Hello from Alice's subpath!", subpath_file)
61+
62+
# The file is stored at: volume-root/users/alice/alice-file.txt
63+
# but appears at: /home/daytona/subpath/alice-file.txt in the sandbox
64+
4765
# Cleanup
4866
await daytona.delete(sandbox)
4967
await daytona.delete(sandbox2)
68+
await daytona.delete(sandbox3)
5069
# daytona.volume.delete(volume)
5170

5271

examples/python/volumes/volume.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,28 @@ def main():
4444
file = sandbox2.fs.download_file(os.path.join(mount_dir_2, "new-file.txt"))
4545
print("File:", file)
4646

47+
# Mount a specific subpath within the volume
48+
# This is useful for isolating data or implementing multi-tenancy
49+
mount_dir_3 = "/home/daytona/subpath"
50+
51+
params = CreateSandboxFromSnapshotParams(
52+
language="python",
53+
volumes=[VolumeMount(volumeId=volume.id, mountPath=mount_dir_3, subpath="users/alice")],
54+
)
55+
sandbox3 = daytona.create(params)
56+
57+
# This sandbox will only see files within the 'users/alice' subpath
58+
# Create a file in the subpath
59+
subpath_file = os.path.join(mount_dir_3, "alice-file.txt")
60+
sandbox3.fs.upload_file(b"Hello from Alice's subpath!", subpath_file)
61+
62+
# The file is stored at: volume-root/users/alice/alice-file.txt
63+
# but appears at: /home/daytona/subpath/alice-file.txt in the sandbox
64+
4765
# Cleanup
4866
daytona.delete(sandbox)
4967
daytona.delete(sandbox2)
68+
daytona.delete(sandbox3)
5069
# daytona.volume.delete(volume)
5170

5271

0 commit comments

Comments
 (0)