Source file docker_sandbox.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
open Lwt.Syntax
let ( >>!= ) = Lwt_result.bind
open Sexplib.Conv
let ( / ) = Filename.concat
let ( // ) dirname filename =
if Sys.win32 then
let l = String.length dirname in
if l = 0 || dirname.[l-1] = '/'
then dirname ^ filename
else dirname ^ "/" ^ filename
else Filename.concat dirname filename
let strf = Printf.sprintf
type isolation = [ `HyperV | `Process | `Default ] [@@deriving sexp]
let isolations : (isolation * string) list = [(`HyperV, "hyperv"); (`Process, "process"); (`Default, "default")]
type t = {
docker_cpus : float;
docker_isolation : isolation;
docker_memory : string option;
docker_network : string;
}
type config = {
cpus : float;
isolation : isolation;
memory : string option;
network : string;
} [@@deriving sexp]
let secrets_guest_root = if Sys.win32 then {|C:\ProgramData\obuilder\|} else "/run/secrets/obuilder"
let secret_dir id = "secrets" / string_of_int id
module Docker_config = struct
let make {Config.cwd; argv; hostname; user; env; mounts; network; mount_secrets; entrypoint}
?(config_dir="")
({docker_cpus; docker_isolation; docker_memory; _} : t) =
assert (entrypoint <> None);
let mounts = List.concat_map Docker.mount_args mounts in
let env = env |> List.concat_map (fun (k, v) -> [ "--env"; strf "%s=%s" k v ]) in
let network = network |> List.concat_map (fun network -> ["--network"; network]) in
let user =
match user with
| `Unix { Obuilder_spec.uid; gid } when not Sys.win32 -> ["--user"; strf "%d:%d" uid gid]
| `Windows { name } when Sys.win32 -> ["--user"; name]
| _ -> assert false
in
let mount_secrets =
let id = ref (-1) in
List.concat_map (fun _ ->
incr id;
Config.Mount.{ty = `Bind; src = config_dir / secret_dir !id; dst = secrets_guest_root / secret_dir !id; readonly = true }
|> Docker.mount_args) mount_secrets in
let memory = Option.fold ~none:[] ~some:(fun m -> ["--memory"; m]) docker_memory in
let docker_argv = [
"--cpus"; strf "%f" docker_cpus;
"--isolation"; (List.assoc docker_isolation isolations);
"--hostname"; hostname;
"--workdir"; cwd;
"--entrypoint"; Option.get entrypoint;
] @ memory @ user @ env @ mounts @ mount_secrets @ network in
docker_argv, argv
end
let secrets_layer ~log mount_secrets base_image container docker_argv =
let link id link =
let target = secrets_guest_root / secret_dir id / "secret" in
if Sys.win32 then
["mkdir"; Filename.dirname link; "&&";
"mklink"; link; target]
else
["mkdir"; "-p"; Filename.(dirname link |> quote); "&&";
"ln"; "-s"; "--"; Filename.quote target; Filename.quote link]
in
let (_, argv) =
List.fold_left (fun (id, argv) {Config.Secret.target; _} ->
let argv = if argv = [] then link id target else argv @ "&&" :: link id target in
id + 1, argv)
(0, []) mount_secrets
in
if mount_secrets = [] then
Lwt_result.ok Lwt.return_unit
else
let docker_argv, argv =
if Sys.win32 then
docker_argv @ ["--entrypoint"; {|C:\Windows\System32\cmd.exe|}],
["/S"; "/C"; String.concat " " argv]
else
docker_argv @ ["--entrypoint"; {|/bin/sh|}],
["-c"; String.concat " " argv]
in
Lwt_result.bind_lwt
(Docker.Cmd_log.run_result ~log ~name:container docker_argv base_image argv)
(fun () ->
let* () = Docker.Cmd_log.commit ~log base_image container base_image in
Docker.Cmd_log.rm ~log [container])
let teardown ~log ~commit id =
let container = Docker.docker_container id in
let base_image = Docker.docker_image ~tmp:true id in
let target_image = Docker.docker_image id in
let* () =
if commit then Docker.Cmd_log.commit ~log base_image container target_image
else Lwt.return_unit
in
Docker.Cmd_log.rm ~log [container]
let run ~cancelled ?stdin ~log t config (id:S.id) =
Lwt_io.with_temp_dir ~perm:0o700 ~prefix:"obuilder-docker-" @@ fun tmp ->
let docker_argv, argv = Docker_config.make config ~config_dir:tmp t in
let* _ = Lwt_list.fold_left_s
(fun id Config.Secret.{value; _} ->
Os.ensure_dir (tmp / "secrets");
Os.ensure_dir (tmp / secret_dir id);
let+ () = Os.write_file ~path:(tmp / secret_dir id / "secret") value in
id + 1
) 0 config.mount_secrets
in
let container = Docker.docker_container id in
let base_image = Docker.docker_image ~tmp:true id in
let proc =
Lwt_result.bind
(secrets_layer ~log config.Config.mount_secrets base_image container docker_argv)
(fun () ->
let* r = Docker.Cmd.exists container in
let* () =
if Result.is_ok r then begin
let `Docker_container name = container in
Log.warn (fun f -> f "Removing left over container %s." name);
Docker.Cmd.rm [ container ]
end else
Lwt.return_unit
in
let stdin = Option.map (fun x -> `FD_move_safely x) stdin in
Docker.Cmd_log.run_result ~log ?stdin ~name:container docker_argv base_image argv)
in
Lwt.on_termination cancelled (fun () ->
let aux () =
if Lwt.is_sleeping proc then (
Docker.Cmd_log.rm ~log [container]
) else Lwt.return_unit
in
Lwt.async aux
);
let* r = proc in
let+ () = match r with
| Ok () -> Lwt.return_unit
| _ -> Docker.Cmd_log.rm ~log [container]
in
if Lwt.is_sleeping cancelled then (r :> (unit, [`Msg of string | `Cancelled]) result)
else Error `Cancelled
let hostname = "builder"
let manifest_from_build t ~base ~exclude src workdir user =
let argv =
Printf.sprintf "exec %s %S %S %d %s %d %s"
(Docker.mount_point_inside_unix // Docker.obuilder_libexec () // "manifest.bash")
workdir
"/"
(List.length exclude)
(String.concat " " (List.map Filename.quote exclude))
(List.length src)
(String.concat " " (List.map Filename.quote src))
in
let config =
let entrypoint, argv = Docker.setup_command ~entp:Docker.(bash_entrypoint (obuilder_libexec ())) ~cmd:[argv] in
Config.v
~cwd:workdir
~argv
~hostname
~user
~env:["PATH", if Sys.win32 then Docker.mount_point_inside_unix // Docker.obuilder_libexec () else "/bin:/usr/bin"]
~mount_secrets:[]
~mounts:[Docker.obuilder_libexec_volume ()]
~network:[]
~entrypoint
()
in
let docker_args, args = Docker_config.make config t in
Docker.Cmd.run_pread_result ~rm:true docker_args (Docker.docker_image base) args >>!= fun manifests ->
match Parsexp.Many.parse_string manifests with
| Ok ts -> List.rev_map Manifest.t_of_sexp ts |> Lwt_result.return
| Error e -> Lwt_result.fail (`Msg (Parsexp.Parse_error.message e))
let manifest_files_from op fd =
let copy_root manifest =
let list = Manifest.to_from_files ~null:true manifest in
Os.write_all_string fd list 0 (String.length list)
in
match op with
| `Copy_items (src_manifest, _) -> Lwt_list.iter_s copy_root src_manifest
| `Copy_item (src_manifest, _) -> copy_root src_manifest
let tarball_from_build t ~log ~files_from ~tar workdir user id =
let entrypoint =
if Sys.win32 then Docker.mount_point_inside_native / Docker.obuilder_libexec () / "tar.exe"
else "tar"
in
let argv =
[ "-cf-"; "--format=gnu";
"--directory"; workdir;
"--verbatim-files-from"; "--null"; "--absolute-names"; "--files-from=-" ]
in
let config =
Config.v
~cwd:workdir
~argv
~hostname
~user
~env:[]
~mount_secrets:[]
~mounts:[Docker.obuilder_libexec_volume ()]
~network:[]
~entrypoint
()
in
let docker_args, args = Docker_config.make config t in
let is_success = if Sys.win32 then Some (function 0 | 1 -> true | _ -> false) else None in
Docker.Cmd_log.run' ~log ~stdin:(`FD_move_safely files_from) ~stdout:(`FD_move_safely tar)
~rm:true ?is_success docker_args (Docker.docker_image id) args
let transform op ~user ~from_tar ~to_untar =
match op with
| `Copy_items (src_manifest, dst_dir) ->
Tar_transfer.transform_files ~from_tar ~src_manifest ~dst_dir ~user ~to_untar
| `Copy_item (src_manifest, dst) ->
Tar_transfer.transform_file ~from_tar ~src_manifest ~dst ~user ~to_untar
let untar t ~cancelled ~stdin ~log ?dst_dir id =
let entrypoint, argv =
if Sys.win32 && dst_dir <> None then
"powershell",
["-Command";
Printf.sprintf {|$path = "%s"; if (Test-Path -Path $path -PathType Container) { $acl = Get-Acl -Path $path }; & %s/tar.exe -xpf - --verbose; if ($acl -ne $null) { Set-Acl -Path $path $acl }|}
(Option.get dst_dir) (Docker.mount_point_inside_native // Docker.obuilder_libexec ()) ]
else begin
assert (dst_dir = None);
"tar", ["-xpf"; "-"; "--verbose"]
end in
let config = Config.v
~cwd:(if Sys.unix then "/" else "C:/")
~argv
~hostname
~user:Obuilder_spec.root
~env:[]
~mount_secrets:[]
~mounts:(if Sys.win32 then [Docker.obuilder_libexec_volume ()] else [])
~network:[]
~entrypoint
()
in
Lwt_result.bind_lwt
(run ~cancelled ~stdin ~log t config id)
(fun () -> teardown ~log ~commit:true id)
let copy_from_context t ~cancelled ~log op ~user ~src_dir ?dst_dir id =
Os.with_pipe_to_child @@ fun ~r:from_us ~w:to_untar ->
let proc = untar t ~cancelled ~stdin:from_us ~log ?dst_dir id in
let send =
Lwt.finalize
(fun () ->
match op with
| `Copy_items (src_manifest, dst_dir) ->
Tar_transfer.send_files ~src_dir ~src_manifest ~dst_dir ~to_untar ~user
| `Copy_item (src_manifest, dst) ->
Tar_transfer.send_file ~src_dir ~src_manifest ~dst ~to_untar ~user
)
(fun () -> Lwt_unix.close to_untar) in
let* result = proc in
let+ () = send in
result
let copy_from_build t ~cancelled ~log op ~user ~workdir ?dst_dir ~from_id id =
Lwt_switch.with_switch @@ fun switch ->
let kill () = Lwt_switch.turn_off switch in
let kill_exn exn = let+ () = kill () in raise exn in
let tarball ~tar () =
Os.with_pipe_to_child @@ fun ~r:files_from ~w:files_from_out ->
let proc = tarball_from_build ~log t ~files_from ~tar workdir user from_id in
let f () = Os.ensure_closed_lwt files_from_out in
let send = Lwt.try_bind (fun () ->
let* () = manifest_files_from op files_from_out in
f ())
f kill_exn in
let* () = Lwt_switch.add_hook_or_exec (Some switch) f in
let* result = proc in
let+ () = send in
result
in
let transform ~to_untar () =
Os.with_pipe_from_child @@ fun ~r:from_tar ~w:tar ->
let f () = Os.ensure_closed_lwt from_tar in
let proc =
let* () = transform op ~user ~from_tar ~to_untar in
f ()
in
let send = Lwt.try_bind (tarball ~tar) f kill_exn in
let* () = Lwt_switch.add_hook_or_exec (Some switch) f in
let* result = proc in
let+ () = send in
result
in
Os.with_pipe_to_child @@ fun ~r:from_us ~w:to_untar ->
let proc = untar t ~cancelled ~stdin:from_us ~log ?dst_dir id in
let f () = Os.ensure_closed_lwt to_untar in
let send = Lwt.try_bind (transform ~to_untar) f kill_exn in
let* () = Lwt_switch.add_hook_or_exec (Some switch) f in
let* result = proc in
let+ () = send in
result
let servercore =
let img = ref None in
fun () ->
match !img with
| None ->
let keyname = {|HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion|} in
let valuename = "CurrentBuild" in
let* value = Os.pread ["reg"; "query"; keyname; "/v"; valuename] in
let line = String.(value |> trim |> split_on_char '\n') |> Fun.flip List.nth 1 in
Scanf.sscanf line " CurrentBuild REG_SZ %i" @@ fun version ->
let version' = match version with
| 14393 -> "ltsc2016"
| 16299 -> "1709"
| 17134 -> "1803"
| 17763 -> "ltsc2019"
| 18362 -> "1903"
| 18363 -> "1909"
| 19041 -> "2004"
| 19042 -> "20H2"
| _ -> "ltsc2022"
in
let img' = "mcr.microsoft.com/windows/servercore:" ^ version' in
Log.info (fun f -> f "Windows host is build %i, will use tag %s." version img');
img := Some (Lwt.return (`Docker_image img'));
Option.get !img
| Some img -> img
let create_tar_volume (t:t) =
Log.info (fun f -> f "Preparing tar volume…");
let name = Docker.obuilder_libexec () in
let vol = `Docker_volume name and img = `Docker_image name in
let* _ = Docker.Cmd.volume (`Create vol) in
let* (`Docker_image base) = if Sys.win32 then servercore () else Lwt.return (`Docker_image "busybox") in
let* config =
if Sys.win32 then
let destination = Docker.(mount_point_inside_native // obuilder_libexec ()) in
let dockerfile =
"# escape=`\n" ^ (strf "FROM %s\n" base) ^ {|
ENV CYGWIN="winsymlinks:native"
ADD [ "https://www.cygwin.com/setup-x86_64.exe", "C:\\cygwin-setup-x86_64.exe" ]
RUN mkdir C:\cygwin64\lib\cygsympathy && mkdir C:\cygwin64\etc\postinstall
ADD [ "https://raw.githubusercontent.com/metastack/cygsympathy/master/cygsympathy.cmd", "C:\\cygwin64\\lib\\cygsympathy\\" ]
ADD [ "https://raw.githubusercontent.com/metastack/cygsympathy/master/cygsympathy.sh", "C:\\cygwin64\\lib\\cygsympathy\\cygsympathy" ]
RUN mklink C:\cygwin64\etc\postinstall\zp_zcygsympathy.sh C:\cygwin64\lib\cygsympathy\cygsympathy
RUN C:\cygwin-setup-x86_64.exe --quiet-mode --no-shortcuts --no-startmenu `
--no-desktop --only-site --local-package-dir %TEMP% --root C:\cygwin64 `
--site http://mirrors.kernel.org/sourceware/cygwin/ `
--packages tar
COPY [ "extract.cmd", "C:/extract.cmd" ]
COPY [ "manifest.bash", "C:/manifest.bash" ]
|} in
let+ () = Lwt_io.with_temp_dir ~perm:0o700 @@ fun temp_dir ->
let write_file dst ?(perm=0o400) contents =
Lwt_io.(with_file ~perm ~mode:Output (temp_dir / dst)) @@ fun ch ->
Lwt_io.fprint ch contents in
let* () = write_file "Dockerfile" dockerfile in
let* () = write_file "extract.cmd" ~perm:0o500 (Option.get (Static_files.read "extract.cmd")) in
let* () = write_file "manifest.bash" ~perm:0o500 (Option.get (Static_files.read "manifest.bash")) in
let docker_argv = [
"--isolation"; List.assoc t.docker_isolation isolations;
"--network"; t.docker_network;
] in
Docker.Cmd.build docker_argv img temp_dir
in
let entrypoint, argv = {|C:\Windows\System32\cmd.exe|}, ["/S"; "/C"; {|C:\extract.cmd|}] in
Config.v ~cwd:{|C:/|} ~argv ~hostname:""
~user:Obuilder_spec.((root_windows :> user))
~env:["DESTINATION", destination]
~mount_secrets:[]
~mounts:[Docker.obuilder_libexec_volume ~readonly:false ()]
~network:[]
~entrypoint
()
else
let destination = Docker.(mount_point_inside_native / obuilder_libexec ()) in
let dockerfile = strf "FROM %s\n" base ^ strf {|COPY [ "manifest.bash", "%s/manifest.bash" ]|} destination in
let+ () = Lwt_io.with_temp_dir ~perm:0o700 @@ fun temp_dir ->
let write_file dst ?(perm=0o400) contents =
Lwt_io.(with_file ~perm ~mode:Output (temp_dir / dst)) @@ fun ch ->
Lwt_io.fprint ch contents in
let* () = write_file "Dockerfile" dockerfile in
let* () = write_file "manifest.bash" ~perm:0o500 (Option.get (Static_files.read "manifest.bash")) in
let docker_argv = [
"--isolation"; List.assoc t.docker_isolation isolations;
"--network"; t.docker_network;
] in
Docker.Cmd.build docker_argv img temp_dir
in
let entrypoint, argv = "/bin/sh", ["-c"; ":"] in
Config.v ~cwd:"/" ~argv ~hostname:""
~user:Obuilder_spec.((root_unix :> user))
~env:["DESTINATION", destination]
~mount_secrets:[]
~mounts:[Docker.obuilder_libexec_volume ~readonly:false ()]
~network:[]
~entrypoint
()
in
let docker_args, args = Docker_config.make config t in
let* () = Docker.Cmd.run ~rm:true docker_args img args in
Docker.Cmd.image (`Remove img)
let create (c : config) =
let t = { docker_cpus = c.cpus; docker_isolation = c.isolation;
docker_memory = c.memory; docker_network = c.network; } in
let* volume_exists = Docker.Cmd.exists (`Docker_volume (Docker.obuilder_libexec ())) in
let+ () = if Result.is_error volume_exists then create_tar_volume t else Lwt.return_unit in
t
let finished () =
Lwt.return ()
open Cmdliner
let docs = "DOCKER BACKEND"
let cpus =
Arg.value @@
Arg.opt Arg.float 2.0 @@
Arg.info ~docs
~doc:"Number of CPUs to be used by Docker."
~docv:"CPUS"
["docker-cpus"]
let isolation =
let isolations = List.rev_map (fun (k, v) -> v, k) isolations in
let doc = Arg.doc_alts_enum isolations |> strf
"Docker isolation, must be %s. Only $(b,default) is available on \
Linux, only $(b,process) and $(b,hyperv) are available on Windows." in
Arg.value @@
Arg.opt (Arg.enum isolations) (if Sys.win32 then `HyperV else `Default) @@
Arg.info ~doc ~docs
~docv:"ISOLATION"
["docker-isolation"]
let memory =
Arg.value @@
Arg.opt Arg.(some string) None @@
Arg.info ~docs
~doc:"The maximum amount of memory the container can use. A positive \
integer, followed by a suffix of b, k, m, g, to indicate bytes, \
kilobytes, megabytes, or gigabytes."
~docv:"MEMORY"
["docker-memory"]
let network =
Arg.value @@
Arg.opt Arg.string (if Sys.unix then "host" else "nat") @@
Arg.info ~docs
~doc:"Docker network used for the Docker backend setup."
~docv:"NETWORK"
["docker-network"]
let cmdliner : config Term.t =
let make cpus isolation memory network =
{ cpus; isolation; memory; network; }
in
Term.(const make $ cpus $ isolation $ memory $ network)