@@ -4,37 +4,35 @@ import Tar_jll
4
4
Base. @kwdef mutable struct DockerExecutor <: SandboxExecutor
5
5
label:: String = Random. randstring (10 )
6
6
privileges:: Symbol = :privileged
7
- persistence_dir:: Union{String,Nothing} = nothing
7
+ end
8
+
9
+ function docker_exe ()
10
+ return load_env_pref (
11
+ " SANDBOX_DOCKER_EXE" ,
12
+ " docker_exe" ,
13
+ something (Sys. which .((" docker" , " podman" ))... , Some (nothing )),
14
+ )
8
15
end
9
16
10
17
function cleanup (exe:: DockerExecutor )
11
- success (` docker system prune --force --filter=label=$(docker_image_label (exe)) ` )
12
-
13
- if exe. persistence_dir != = nothing && isdir (exe. persistence_dir)
14
- # Because a lot of these files are unreadable, we must `chmod +r` them before deleting.
15
- chmod_recursive (exe. persistence_dir, 0o777 , true )
16
- try
17
- rm (exe. persistence_dir; force= true , recursive= true )
18
- catch
19
- end
20
- end
18
+ success (` $(docker_exe ()) system prune --force --filter=label=$(docker_image_label (exe)) ` )
21
19
end
22
20
23
21
Base. show (io:: IO , exe:: DockerExecutor ) = write (io, " Docker Executor" )
24
22
25
23
function executor_available (:: Type{DockerExecutor} ; verbose:: Bool = false )
26
24
# don't even try to exec if it doesn't exist
27
- if Sys . which ( " docker " ) === nothing
25
+ if docker_exe ( ) === nothing
28
26
if verbose
29
- @info (" No `docker` command found; docker unavailable" )
27
+ @info (" No `docker` or `podman` command found; DockerExecutor unavailable" )
30
28
end
31
29
return false
32
30
end
33
31
34
32
# Return true if we can `docker ps`; if we can't, then there's probably a permissions issue
35
- if ! success (` docker ps` )
33
+ if ! success (` $( docker_exe ()) ps` )
36
34
if verbose
37
- @warn (" Unable to run `docker ps`; perhaps you're not in the `docker` group?" )
35
+ @warn (" Unable to run `$( docker_exe ()) ps`; perhaps you're not in the `docker` group?" )
38
36
end
39
37
return false
40
38
end
@@ -68,19 +66,21 @@ function save_timestamp(image_name::String, timestamp::Float64)
68
66
end
69
67
end
70
68
71
- docker_image_name (root_path:: String , uid:: Cint , gid:: Cint ) = " sandbox_rootfs:$(string (Base. _crc32c (root_path), base= 16 )) -$(uid) -$(gid) "
69
+ function docker_image_name (paths:: Dict{String,String} , uid:: Cint , gid:: Cint )
70
+ hash = foldl (⊻ , vcat (Base. _crc32c .(keys (paths))... , Base. _crc32c .(values (paths))))
71
+ return " sandbox_rootfs:$(string (hash, base= 16 )) -$(uid) -$(gid) "
72
+ end
72
73
docker_image_label (exe:: DockerExecutor ) = string (" org.julialang.sandbox.jl=" , exe. label)
73
- function should_build_docker_image (root_path :: String , uid:: Cint , gid:: Cint )
74
+ function should_build_docker_image (paths :: Dict{ String,String} , uid:: Cint , gid:: Cint )
74
75
# If the image doesn't exist at all, always return true
75
- image_name = docker_image_name (root_path , uid, gid)
76
- if ! success (` docker image inspect $(image_name) ` )
76
+ image_name = docker_image_name (paths , uid, gid)
77
+ if ! success (` $( docker_exe ()) image inspect $(image_name) ` )
77
78
return true
78
79
end
79
80
80
81
# If this image has been built before, compare its historical timestamp to the current one
81
- curr_ctime = max_directory_ctime (root_path)
82
82
prev_ctime = get (load_timestamps (), image_name, 0.0 )
83
- return curr_ctime != prev_ctime
83
+ return any ( max_directory_ctime (path) > prev_ctime for path in values (paths))
84
84
end
85
85
86
86
"""
@@ -92,25 +92,34 @@ things on top of that, with no recursive mounting. We cut down on unnecessary w
92
92
somewhat by quick-scanning the directory for changes and only rebuilding if changes
93
93
are detected.
94
94
"""
95
- function build_docker_image (root_path:: String , uid:: Cint , gid:: Cint ; verbose:: Bool = false )
96
- image_name = docker_image_name (root_path, uid, gid)
97
- if should_build_docker_image (root_path, uid, gid)
98
- max_ctime = max_directory_ctime (root_path)
95
+ function build_docker_image (mounts:: Dict , uid:: Cint , gid:: Cint ; verbose:: Bool = false )
96
+ overlayed_paths = Dict (path => m. host_path for (path, m) in mounts if m. type == MountType. Overlayed)
97
+ image_name = docker_image_name (overlayed_paths, uid, gid)
98
+ if should_build_docker_image (overlayed_paths, uid, gid)
99
+ max_ctime = maximum (max_directory_ctime (path) for path in values (overlayed_paths))
99
100
if verbose
100
- @info (" Building docker image $(image_name) with max timestamp $( max_ctime)" )
101
+ @info (" Building docker image $(image_name) with max timestamp 0x $( string ( round (UInt64, max_ctime), base = 16 ) )" )
101
102
end
102
103
104
+ # We're going to tar up all Overlayed mounts, using `--transform` to convert our host paths
105
+ # to the required destination paths.
106
+ tar_cmds = String[]
107
+ append! (tar_cmds, [" --transform=s&$(host[2 : end ]) &$(dst[2 : end ]) &" for (dst, host) in overlayed_paths])
108
+ append! (tar_cmds, [host for (_, host) in overlayed_paths])
109
+
103
110
# Build the docker image
104
- open (` docker import - $(image_name) ` , " w" , verbose ? stdout : devnull ) do io
111
+ open (` $( docker_exe ()) import - $(image_name) ` , " w" , verbose ? stdout : devnull ) do io
105
112
# We need to record permissions, and therefore we cannot use Tar.jl.
106
113
# Some systems (e.g. macOS) ship with a BSD tar that does not support the
107
114
# `--owner` and `--group` command-line options. Therefore, if Tar_jll is
108
115
# available, we use the GNU tar provided by Tar_jll. If Tar_jll is not available,
109
116
# we fall back to the system tar.
110
- cd (root_path) do
111
- tar = Tar_jll. is_available () ? Tar_jll. tar () : ` tar`
112
- run (pipeline (` $(tar) -c --owner=$(uid) --group=$(gid) .` , stdout = io))
113
- end
117
+ tar = Tar_jll. is_available () ? Tar_jll. tar () : ` tar`
118
+ run (pipeline (
119
+ ` $(tar) -c --owner=$(uid) --group=$(gid) $(tar_cmds) ` ,
120
+ stdout = io,
121
+ stderr = verbose ? stderr : devnull ,
122
+ ))
114
123
end
115
124
116
125
# Record that we built it
@@ -121,20 +130,20 @@ function build_docker_image(root_path::String, uid::Cint, gid::Cint; verbose::Bo
121
130
end
122
131
123
132
function commit_previous_run (exe:: DockerExecutor , image_name:: String )
124
- ids = split (readchomp (` docker ps -a --filter label=$(docker_image_label (exe)) --format "{{.ID}}"` ))
133
+ ids = split (readchomp (` $( docker_exe ()) ps -a --filter label=$(docker_image_label (exe)) --format "{{.ID}}"` ))
125
134
if isempty (ids)
126
135
return image_name
127
136
end
128
137
129
138
# We'll take the first docker container ID that we get, as its the most recent, and commit it.
130
139
image_name = " sandbox_rootfs_persist:$(first (ids)) "
131
- run (` docker commit $(first (ids)) $(image_name) ` )
140
+ run (` $( docker_exe ()) commit $(first (ids)) $(image_name) ` )
132
141
return image_name
133
142
end
134
143
135
144
function build_executor_command (exe:: DockerExecutor , config:: SandboxConfig , user_cmd:: Cmd )
136
145
# Build the docker image that corresponds to this rootfs
137
- image_name = build_docker_image (config. mounts[ " / " ] . host_path , config. uid, config. gid; verbose= config. verbose)
146
+ image_name = build_docker_image (config. mounts, config. uid, config. gid; verbose= config. verbose)
138
147
139
148
if config. persist
140
149
# If this is a persistent run, check to see if any previous runs have happened from
@@ -158,7 +167,7 @@ function build_executor_command(exe::DockerExecutor, config::SandboxConfig, user
158
167
else
159
168
throw (ArgumentError (" invalid value for exe.privileges: $(exe. privileges) " ))
160
169
end
161
- cmd_string = String[" docker " , " run" , privilege_args... , " -i" , " --label" , docker_image_label (exe)]
170
+ cmd_string = String[docker_exe () , " run" , privilege_args... , " -i" , " --label" , docker_image_label (exe)]
162
171
163
172
# If we're doing a fully-interactive session, tell it to allocate a psuedo-TTY
164
173
if all (isa .((config. stdin , config. stdout , config. stderr ), Base. TTY))
@@ -178,8 +187,7 @@ function build_executor_command(exe::DockerExecutor, config::SandboxConfig, user
178
187
if mount_info. type == MountType. ReadOnly
179
188
mount_type_str = " :ro"
180
189
elseif mount_info. type == MountType. Overlayed
181
- mount_type_str = " :ro"
182
- push! (overlay_mappings, sandbox_path)
190
+ continue
183
191
elseif mount_info. type == MountType. ReadWrite
184
192
mount_type_str = " "
185
193
else
@@ -188,52 +196,6 @@ function build_executor_command(exe::DockerExecutor, config::SandboxConfig, user
188
196
append! (cmd_string, [" -v" , " $(mount_info. host_path) :$(sandbox_path)$(mount_type_str) " ])
189
197
end
190
198
191
- # There are two ways to emulate overlayed mounts in Docker; either by building a
192
- # new image that has `COPY` statements in its Dockerfile to copy in the directories,
193
- # or by manually inserting an entrypoint into container to run the necessary `mount`
194
- # statements for us. We choose the latter as it should be much more efficient.
195
- entrypoint = config. entrypoint
196
- if ! isempty (overlay_mappings)
197
- # We need a bindmount to store our persistence data, create one now and mount it in.
198
- # If we're not persistent, we generate a new one every time.
199
- if exe. persistence_dir === nothing || ! config. persist
200
- exe. persistence_dir = mktempdir ()
201
- end
202
- append! (cmd_string, [" -v" , " $(exe. persistence_dir) :/.overlayed" ])
203
-
204
- entrypoint_path = joinpath (exe. persistence_dir, " entrypoint" )
205
- open (entrypoint_path, write= true ) do io
206
- println (io, """
207
- #!/bin/sh
208
- """ )
209
-
210
- # For each path in our overlay mappings, we create an overlay that stores its
211
- # state in our persistence directory.
212
- for path in overlay_mappings
213
- path_slug = " $(path) -$(string (hash (path); base= 16 )) "
214
- println (io, """
215
- mkdir -p /.overlayed/$(path_slug) -upper
216
- mkdir -p /.overlayed/$(path_slug) -work
217
- mount -t overlay overlay -olowerdir=$(path) -oupperdir=/.overlayed/$(path_slug) -upper -oworkdir=/.overlayed/$(path_slug) -work $(path)
218
- """ )
219
- end
220
-
221
- # Finally, if we were given an entrypoint, sub off to that, otherwise directly execute the command:
222
- if config. entrypoint != = nothing
223
- println (io, " exec \" $(config. entrypoint) \" " )
224
- else
225
- println (io, " exec \"\$ @\" " )
226
- end
227
- end
228
- chmod (entrypoint_path, 0o755 )
229
-
230
- # Tell the later `--entrypoint` argument to use this
231
- entrypoint = " /.overlayed/entrypoint"
232
-
233
- # Tell Julia to clean this up before we exit
234
- Base. Filesystem. temp_cleanup_later (entrypoint_path)
235
- end
236
-
237
199
# Apply environment mappings, first from `config`, next from `user_cmd`.
238
200
for (k, v) in config. env
239
201
append! (cmd_string, [" -e" , " $(k) =$(v) " ])
@@ -245,8 +207,8 @@ function build_executor_command(exe::DockerExecutor, config::SandboxConfig, user
245
207
end
246
208
247
209
# Add in entrypoint, if it is set
248
- if entrypoint != = nothing
249
- append! (cmd_string, [" --entrypoint" , entrypoint])
210
+ if config . entrypoint != = nothing
211
+ append! (cmd_string, [" --entrypoint" , config . entrypoint])
250
212
end
251
213
252
214
if config. hostname != = nothing
@@ -307,7 +269,7 @@ function export_docker_image(image_name::String,
307
269
end
308
270
309
271
# Get a container ID ready to be passed to `docker export`
310
- container_id = readchomp (` docker create $(image_name) /bin/true` )
272
+ container_id = readchomp (` $( docker_exe ()) create $(image_name) /bin/true` )
311
273
312
274
# Get the ID of that container (since we can't export by label, sadly)
313
275
if isempty (container_id)
@@ -320,14 +282,14 @@ function export_docker_image(image_name::String,
320
282
# Export the container filesystem to a directory
321
283
try
322
284
mkpath (output_dir)
323
- open (` docker export $(container_id) ` ) do tar_io
285
+ open (` $( docker_exe ()) export $(container_id) ` ) do tar_io
324
286
Tar. extract (tar_io, output_dir) do hdr
325
287
# Skip known troublesome files
326
288
return hdr. type ∉ (:chardev ,)
327
289
end
328
290
end
329
291
finally
330
- run (` docker rm -f $(container_id) ` )
292
+ run (` $( docker_exe ()) rm -f $(container_id) ` )
331
293
end
332
294
return output_dir
333
295
end
@@ -347,7 +309,7 @@ image with `platform`.
347
309
"""
348
310
function pull_docker_image (image_name:: String ,
349
311
output_dir:: String = @get_scratch! (" docker-$(sanitize_key (image_name)) " );
350
- platform:: String = " " ,
312
+ platform:: Union{ String,Nothing} = nothing ,
351
313
force:: Bool = false ,
352
314
verbose:: Bool = false )
353
315
if ispath (output_dir) && ! isempty (readdir (output_dir))
@@ -363,12 +325,10 @@ function pull_docker_image(image_name::String,
363
325
364
326
# Pull the latest version of the image
365
327
try
366
- p = isempty (platform) ? ` ` : ` --platform $(platform) `
367
- run (` docker pull $(p) $(image_name) ` )
368
- catch
369
- if verbose
370
- @warn (" Cannot pull" , image_name)
371
- end
328
+ p = platform === nothing ? ` ` : ` --platform $(platform) `
329
+ run (` $(docker_exe ()) pull $(p) $(image_name) ` )
330
+ catch e
331
+ @warn (" Cannot pull" , image_name, e)
372
332
return nothing
373
333
end
374
334
0 commit comments