Skip to content

Commit fdaea39

Browse files
committed
[WIP] Docker backend: implement copy using streamed tarballs
I had implemented copy using a temporary volume. I believe streaming tarballs has the following advantages over volumes: - it doesn't touch the file system, all operations are in-memory; - renaming the files to their destination is much more reliable (no need for escaping).
1 parent 005eef8 commit fdaea39

File tree

12 files changed

+226
-133
lines changed

12 files changed

+226
-133
lines changed

copy.spec

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
((build dev
2+
((from ocaml/opam@sha256:116c960addbbda19190d47b49e42310916cf42fe432fa5e37eb6104c488218d6)
3+
(workdir /src)
4+
(run (shell "sudo chown opam /src"))
5+
(user (uid 1000) (gid 1000)) ; Build as the "opam" user
6+
; (copy (src obuilder.opam) (dst ./))
7+
(run (shell "echo hello > obuilder.opam"))
8+
))
9+
(from debian:10)
10+
(copy (from (build dev))
11+
(src /src/obuilder.opam)
12+
(dst /chabada)))

dune-project

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
(fmt (>= 0.8.9))
1818
logs
1919
cmdliner
20-
tar-unix
20+
(tar (>= 1.2))
21+
(tar-unix (>= 1.2))
2122
yojson
2223
sexplib
2324
ppx_deriving

lib/build.ml

Lines changed: 97 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -393,24 +393,37 @@ module Docker = struct
393393
| [item] -> Ok (`Copy_item (item, dst))
394394
| _ -> Fmt.error_msg "When copying multiple items, the destination must end with '/'"
395395

396+
let tarball_from_context ~src_dir op user ~to_untar =
397+
Log.debug (fun f -> f "tarball_from_context");
398+
(* If the sending thread finishes (or fails), close the writing
399+
socket immediately so that the tar process finishes too. *)
400+
(* Lwt.finalize *)
401+
(* (fun () -> *)
402+
match op with
403+
| `Copy_items (src_manifest, dst_dir) ->
404+
Tar_transfer.send_files ~src_dir ~src_manifest ~dst_dir ~to_untar ~user
405+
| `Copy_item (src_manifest, dst) ->
406+
Tar_transfer.send_file ~src_dir ~src_manifest ~dst ~to_untar ~user(* ) *)
407+
(* (fun () -> Lwt_unix.close to_untar) *)
408+
396409
let mount_point_inside_unix = if Sys.win32 then "/cygdrive/c" else "/var/lib/obuilder"
397410
let mount_point_inside_native = if Sys.win32 then {|C:\|} else mount_point_inside_unix
398411

399-
let docker_manifest_from_build ~base ~exclude src user =
412+
let manifest_from_build ~base ~exclude src workdir user =
400413
let obuilder_volume = Docker.obuilder_volume () in
401414
let docker_argv = [
402415
"--mount"; Printf.sprintf "type=volume,src=%s,dst=%s,readonly" obuilder_volume (mount_point_inside_native / obuilder_volume);
403416
"--entrypoint"; if Sys.win32 then mount_point_inside_native / obuilder_volume / "bash.exe" else "bash";
404417
"--env"; Printf.sprintf "PATH=%s" (if Sys.win32 then mount_point_inside_unix // obuilder_volume else "/bin:/usr/bin");
405-
"--workdir"; if Sys.win32 then {|C:\|} else "/";
418+
"--workdir"; workdir;
406419
"--user"; match user with `Unix { Obuilder_spec.uid; gid } -> Printf.sprintf "%d:%d" uid gid | `Windows { Obuilder_spec.name } -> name
407420
(* FIXME: we don't have access to isolation type here. *)
408421
] in
409422
let manifest_bash =
410423
(* FIXME: does Filename.quote always use Bash quoting rules? *)
411424
Printf.sprintf "exec %s %S %S %d %s %d %s"
412425
(mount_point_inside_unix // obuilder_volume // "manifest.bash")
413-
"."
426+
workdir
414427
"/"
415428
(List.length exclude)
416429
(String.concat " " (List.map Filename.quote exclude))
@@ -421,96 +434,79 @@ module Docker = struct
421434
let pp f = Os.pp_cmd f ["Generating source manifest"] in
422435
Docker.run_pread_result ~pp ~rm:true docker_argv (Docker.docker_image base) argv >>!= fun manifests ->
423436
match Parsexp.Many.parse_string manifests with
424-
| Ok ts -> List.rev_map Manifest.t_of_sexp ts |> Lwt.return |> Lwt_result.ok
437+
| Ok ts -> List.rev_map Manifest.t_of_sexp ts |> Lwt_result.return
425438
| Error e -> Lwt_result.fail (`Msg (Parsexp.Parse_error.message e))
426439

427-
let docker_tarball_from_context ~src_dir mount_point op user =
428-
let name = if Sys.win32 then mount_point / "archive.tar" else Filename.temp_file "obuilder-" "" in
429-
Log.debug (fun f -> f "docker_tarball_from_context name:%s" name);
430-
Lwt_unix.openfile name [O_WRONLY; O_CREAT] 0o600 >>= fun to_untar ->
431-
(* If the sending thread finishes (or fails), close the writing socket
432-
immediately so that the tar process finishes too. *)
433-
Lwt.finalize
434-
(fun () ->
435-
Lwt.try_bind
436-
(fun () ->
437-
match op with
438-
| `Copy_items (src_manifest, dst_dir) ->
439-
Tar_transfer.send_files ~src_dir ~src_manifest ~dst_dir ~to_untar ~user
440-
| `Copy_item (src_manifest, dst) ->
441-
Tar_transfer.send_file ~src_dir ~src_manifest ~dst ~to_untar ~user)
442-
(fun () ->
443-
Lwt_unix.close to_untar >>= fun () ->
444-
if not (Sys.win32) then Os.sudo ["cp"; "--"; name; mount_point / "archive.tar"]
445-
else Lwt.return_unit)
446-
(fun exn -> Lwt_unix.close to_untar >>= fun () -> Lwt.fail exn))
447-
(fun () -> if not (Sys.win32) then Lwt_unix.unlink name else Lwt.return_unit)
448-
449-
let docker_tar_transfer ~dst op ch =
450-
let copy_root dst acc t =
451-
(* FIXME: escape dst, src, path for regexp? *)
452-
begin match t with
453-
| `File (path, _) | `Symlink (path, _) ->
454-
Lwt_io.fprint ch path >>= fun () ->
455-
Lwt.return ("--transform" :: (Printf.sprintf "s,^%s$,%s," path dst) :: acc)
456-
| `Dir (src, _) as dir ->
457-
Lwt_io.fprint ch (String.concat "\n" (Manifest.to_list dir)) >>= fun () ->
458-
Lwt.return ("--transform" :: (Printf.sprintf "s,^%s,%s," (Filename.dirname src) dst) :: acc)
459-
end >>= fun tar_argv -> Lwt_io.fprint ch "\n" >>= fun () -> Lwt.return tar_argv
440+
let manifest_files_from op fd =
441+
let copy_root manifest =
442+
let list = Manifest.to_from_files ~null:true manifest in
443+
Os.write_all_string fd list 0 (String.length list)
460444
in
461445
match op with
462-
| `Copy_items (src_manifest, _) -> begin Lwt_list.fold_left_s (copy_root dst) [] src_manifest end
463-
| `Copy_item (src_manifest, _) -> begin copy_root dst [] (src_manifest:Manifest.t) end
446+
| `Copy_items (src_manifest, _) -> Lwt_list.iter_s copy_root src_manifest
447+
| `Copy_item (src_manifest, _) -> copy_root src_manifest
464448

465-
let docker_tarball_from_build t ~log ~dst ~from (`Docker_volume volume) mount_point op user =
449+
let tarball_from_build t ~files_from ~to_untar workdir user id =
466450
Log.debug (fun f -> f "docker_tarball_from_build");
467-
begin
468-
let name = mount_point / "manifest" in
469-
if Sys.win32 then Lwt_io.with_file ~mode:Output name (docker_tar_transfer ~dst op)
470-
else Lwt_io.with_temp_file (fun (tmp, ch) ->
471-
docker_tar_transfer ~dst op ch >>= fun tar_argv ->
472-
Os.sudo ["cp"; "-a"; "--"; tmp; name] >>= fun () ->
473-
Lwt.return tar_argv)
474-
end >>= fun tar_argv ->
475451
let argv =
476452
["--login"; "-c";
477453
String.concat " "
478454
([ if Sys.win32 then "tar.exe" else "tar";
479-
"-cf"; mount_point_inside_unix // volume // "archive.tar";
480-
"--absolute-names"; "--no-recursion";
481-
"--show-transformed-names"; "-v"; (* for debugging *)
482-
"--files-from"; mount_point_inside_unix // volume // "manifest" ]
483-
@ tar_argv)]
455+
"-c"; "--format=ustar";
456+
"--directory"; workdir;
457+
(* Beware, the order is meaningfull: --files-from should come last. *)
458+
"--verbatim-files-from"; "--null"; "--absolute-names"; "--files-from=-";
459+
"-f-"])]
484460
in
485461
let config =
486462
let obuilder_volume = Docker.obuilder_volume () in
487463
Config.v
488-
~cwd:(if Sys.win32 then {|C:\|} else "/")
464+
~cwd:workdir
489465
~argv
490466
~hostname
491467
~user
492468
~env:["PATH", if Sys.win32 then mount_point_inside_unix // obuilder_volume else "/bin:/usr/bin"]
493469
~mount_secrets:[]
494470
~mounts:Config.Mount.[
495-
{src = volume; dst = mount_point_inside_native / volume; readonly = false};
496471
{src = obuilder_volume; dst = mount_point_inside_native / obuilder_volume; readonly = true}]
497472
~network:[]
498473
~entrypoint:(if Sys.win32 then mount_point_inside_native / obuilder_volume / "bash.exe" else "/bin/bash")
499474
()
500475
in
501-
let from_tmp = Docker.docker_image ~tmp:true from in
502-
Docker.tag (Docker.docker_image from) from_tmp >>= fun () ->
503-
(* Bypass db_store. *)
504-
let cancelled, set_cancelled = Lwt.wait () in
505-
Lwt.finalize
506-
(fun () ->
507-
Lwt_result.bind_lwt
508-
(Docker_sandbox.run ~cancelled ~log t.sandbox config from)
509-
(fun () -> docker_teardown_sandbox from ~commit:false))
510-
(fun () ->
511-
Docker.image "rm" from_tmp >>= fun () ->
512-
Lwt.wakeup_later set_cancelled ();
513-
Lwt.return_unit)
476+
let docker_args, args = Docker_sandbox.Docker_config.make config ~config_dir:"" (* unused *) t in
477+
Docker.run ~stdin:(`FD_move_safely files_from) ~stdout:(`FD_move_safely to_untar)
478+
~rm:true docker_args (Docker.docker_image id) args
479+
480+
let untar t ~cancelled ~stdin ~log workdir id =
481+
let obuilder_volume = Docker.obuilder_volume () in
482+
let mounts =
483+
if Sys.win32 then [Config.Mount.{
484+
src = obuilder_volume;
485+
dst = mount_point_inside_native / obuilder_volume;
486+
readonly = true; }]
487+
else []
488+
in
489+
let entrypoint =
490+
if Sys.win32 then Printf.sprintf {|C:\%s\tar.exe|} obuilder_volume
491+
else "/usr/bin/env" in
492+
let argv = ["-xf"; "-"; "--absolute-names"; ]
493+
|> if not Sys.win32 then List.cons "tar" else Fun.id
494+
in
495+
let config = Config.v
496+
~cwd:workdir
497+
~argv
498+
~hostname
499+
~user:Obuilder_spec.root
500+
~env:[]
501+
~mount_secrets:[]
502+
~mounts
503+
~network:[]
504+
~entrypoint
505+
()
506+
in
507+
Lwt_result.bind_lwt
508+
(Docker_sandbox.run ~cancelled ~stdin ~log t.sandbox config id)
509+
(fun () -> docker_teardown_sandbox id ~commit:true)
514510

515511
let copy t ~context ~base { Obuilder_spec.from; src; dst; exclude } =
516512
let { Context.switch; src_dir; workdir; user; log; shell = _; env = _; scope; secrets = _ } = context in
@@ -526,11 +522,11 @@ module Docker = struct
526522
| None ->
527523
Lwt_result.fail (`Msg (Fmt.str "Build result %S not found" id))
528524
| Some dir ->
529-
Lwt_result.return (`Build (id, dir / "rootfs"))
525+
Lwt_result.return (`Build (id, dir))
530526
end >>!= fun src_dir ->
531527
begin match src_dir with
532528
| `Context src_dir -> sequence (List.map (Manifest.generate ~exclude ~src_dir) src) |> Lwt.return
533-
| `Build (id, _) -> docker_manifest_from_build ~base:id ~exclude src user
529+
| `Build (id, _) -> manifest_from_build ~base:id ~exclude src workdir user
534530
end >>= fun src_manifest ->
535531
match Result.bind src_manifest (to_copy_op ~dst) with
536532
| Error _ as e -> Lwt.return e
@@ -543,53 +539,37 @@ module Docker = struct
543539
Log.debug (fun f -> f "COPY: %a@." Sexplib.Sexp.pp_hum (sexp_of_copy_details details));
544540
let id = Sha256.to_hex (Sha256.string (Sexplib.Sexp.to_string (sexp_of_copy_details details))) in
545541
Store.build t.store ?switch ~base ~id ~log (fun ~cancelled ~log _ ->
546-
let volume = Docker.docker_volume_copy id in
547-
Docker.volume ["create"] volume >>= fun _ ->
548-
Lwt.finalize
549-
(fun () ->
550-
Docker.mount_point volume >>= fun mount_point ->
551-
Log.debug (fun f -> f "Generating tarball");
552-
begin match src_dir with
553-
| `Context src_dir -> docker_tarball_from_context ~src_dir mount_point op user |> Lwt_result.ok
554-
| `Build (from, _) -> docker_tarball_from_build t ~log ~dst ~from volume mount_point op user
555-
end >>!= fun () ->
556-
let argv = ["-xf"; mount_point_inside_unix // id // "archive.tar"; "--absolute-names";
557-
"-v" (* for debugging *) ]
558-
|> if not Sys.win32 then List.cons "tar" else Fun.id
559-
in
560-
let config =
561-
let obuilder_volume = Docker.obuilder_volume () in
562-
let mounts =
563-
[ Config.Mount.{
564-
src = Docker.volume_copy_name id;
565-
dst = mount_point_inside_native / id;
566-
readonly = false }]
567-
|> if Sys.win32 then List.cons Config.Mount.{
568-
src = obuilder_volume;
569-
dst = mount_point_inside_native / obuilder_volume;
570-
readonly = true; }
571-
else Fun.id
572-
in
573-
let entrypoint =
574-
if Sys.win32 then Printf.sprintf {|C:\%s\tar.exe|} obuilder_volume
575-
else "/usr/bin/env" in
576-
Config.v
577-
~cwd:(if Sys.win32 then {|C:\|} else "/")
578-
~argv
579-
~hostname
580-
~user:Obuilder_spec.root
581-
~env:[]
582-
~mount_secrets:[]
583-
~mounts
584-
~network:[]
585-
~entrypoint
586-
()
587-
in
588-
Log.debug (fun f -> f "Docker_sandbox is running");
589-
Lwt_result.bind_lwt
590-
(Docker_sandbox.run ~cancelled ~log t.sandbox config id)
591-
(fun () -> docker_teardown_sandbox id ~commit:true))
592-
(fun () -> Docker.volume ["rm"] volume >>= fun _ -> Lwt.return_unit))
542+
Log.debug (fun f -> f "Produce the tar stream");
543+
(* let dbgout = Os.{raw = Unix.openfile "/tmp/ocaml.tar" [O_WRONLY;O_CREAT;O_TRUNC] 0o666; needs_close = false} in *)
544+
(* Lwt_unix.openfile "/tmp/ocaml.tar" [O_WRONLY;O_CREAT;O_TRUNC] 0o666 >>= fun dbgout -> *)
545+
Os.with_pipe_between_children @@ fun ~r:from_tar ~w:to_untar -> begin
546+
begin match src_dir with
547+
| `Context src_dir ->
548+
Log.debug (fun f -> f "… from build context");
549+
tarball_from_context ~src_dir op user ~to_untar:(Lwt_unix.of_unix_file_descr to_untar.raw)
550+
| `Build (id, _dir) ->
551+
Log.debug (fun f -> f "… from build %s" id );
552+
Os.with_pipe_to_child @@ fun ~r:files_from_in ~w:files_from_out -> begin
553+
manifest_files_from op files_from_out >>= fun () ->
554+
tarball_from_build t.sandbox ~files_from:files_from_in ~to_untar workdir user id
555+
end
556+
end >>= fun () -> begin
557+
Log.debug (fun f -> f "Rename the files in tar");
558+
Os.with_pipe_between_children @@ fun ~r:from_tar' ~w:to_untar -> begin
559+
begin
560+
let from_tar = Lwt_unix.of_unix_file_descr from_tar.raw in
561+
let to_untar = Lwt_unix.of_unix_file_descr to_untar.raw in
562+
match op with
563+
| `Copy_items (_, dst_dir) ->
564+
Tar_transfer.transform_files ~dst_dir ~user ~from_tar ~to_untar
565+
| `Copy_item (_, dst) ->
566+
Tar_transfer.transform_file ~dst ~user ~from_tar ~to_untar
567+
end >>= fun () ->
568+
Log.debug (fun f -> f "Untar");
569+
untar t ~cancelled ~log ~stdin:from_tar' workdir id
570+
end
571+
end
572+
end)
593573

594574
let pp_op ~(context:Context.t) f op =
595575
Fmt.pf f "@[<v2>%s: %a@]" context.workdir Obuilder_spec.pp_op op

lib/docker.ml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,14 @@ let build docker_argv (`Docker_image image) context_path =
8484
let run ?stdin ?stdout ?stderr ?name ?(rm=false) docker_argv (`Docker_image image) argv =
8585
let docker_argv = if rm then "--rm" :: docker_argv else docker_argv in
8686
let docker_argv = Option.fold ~none:docker_argv ~some:(fun (`Docker_container name) -> "--name" :: name :: docker_argv) name in
87+
let docker_argv = match stdin with Some (`FD_move_safely _) -> "-i" :: docker_argv | _ -> docker_argv in
8788
let argv = docker_argv @ image :: argv in
8889
exec' ?stdin ?stdout ?stderr ("run" :: argv)
8990

9091
let run_result ?stdin ~pp ?name ?(rm=false) docker_argv (`Docker_image image) argv =
9192
let docker_argv = if rm then "--rm" :: docker_argv else docker_argv in
9293
let docker_argv = Option.fold ~none:docker_argv ~some:(fun (`Docker_container name) -> "--name" :: name :: docker_argv) name in
93-
let docker_argv = if Option.is_some stdin then "-i" :: docker_argv else docker_argv in
94+
let docker_argv = match stdin with Some (`FD_move_safely _) -> "-i" :: docker_argv | _ -> docker_argv in
9495
let argv = docker_argv @ image :: argv in
9596
exec_result' ?stdin ~pp ("run" :: argv)
9697

lib/manifest.ml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,12 @@ let generate ~exclude ~src_dir src =
6666
with Failure m ->
6767
Error (`Msg m)
6868

69-
let to_list t =
70-
let rec aux l = function
71-
| `File (name, _hash) -> name :: l
72-
| `Symlink (name, _target) -> name :: l
73-
| `Dir (name, entries) -> name :: List.fold_left aux l entries
69+
let to_from_files ?(null=false) t =
70+
let sep = if null then '\000' else '\n' in
71+
let buf = Buffer.create 64 in
72+
let rec aux = function
73+
| `File (name, _) | `Symlink (name, _) -> Buffer.add_string buf name; Buffer.add_char buf sep
74+
| `Dir (name, entries) -> Buffer.add_string buf name; Buffer.add_char buf sep; List.iter aux entries
7475
in
75-
aux [] t
76+
aux t;
77+
Buffer.contents buf

lib/manifest.mli

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ val generate : exclude:string list -> src_dir:string -> string -> (t, [> `Msg of
1010
Files with basenames in [exclude] are ignored.
1111
Returns an error if [src] is not under [src_dir] or does not exist. *)
1212

13-
val to_list : t -> string list
13+
val to_from_files : ?null:bool -> t -> string
14+
(** [to_from_files t] returns a buffer containing the list of files,
15+
separated by ASCII LF (the default) or NUL if [null] is true. *)

lib/os.ml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ type unix_fd = {
66
raw : Unix.file_descr;
77
mutable needs_close : bool;
88
}
9-
9+
1010
let close fd =
1111
assert (fd.needs_close);
1212
Unix.close fd.raw;
@@ -26,7 +26,7 @@ let pp_signal f x =
2626
else Fmt.int f x
2727

2828
let pp_cmd = Fmt.box Fmt.(list ~sep:sp (quote string))
29-
29+
3030
let redirection = function
3131
| `FD_move_safely x -> `FD_copy x.raw
3232
| `Dev_null -> `Dev_null
@@ -93,6 +93,14 @@ let rec write_all fd buf ofs len =
9393
write_all fd buf (ofs + n) (len - n)
9494
)
9595

96+
let rec write_all_string fd buf ofs len =
97+
assert (len >= 0);
98+
if len = 0 then Lwt.return_unit
99+
else (
100+
Lwt_unix.write_string fd buf ofs len >>= fun n ->
101+
write_all_string fd buf (ofs + n) (len - n)
102+
)
103+
96104
let write_file ~path contents =
97105
Lwt_io.(with_file ~mode:output) path @@ fun ch ->
98106
Lwt_io.write ch contents
@@ -135,6 +143,15 @@ let with_pipe_between_children fn =
135143
Lwt.return_unit
136144
)
137145

146+
(* let with_lwt_pipe_between_children_result f g = *)
147+
(* let r, w = Lwt_unix.pipe () in *)
148+
(* Lwt.finalize *)
149+
(* (fun () -> Lwt_result.both (f ~w) (g ~r)) *)
150+
(* (fun () -> *)
151+
(* ensure_closed_lwt r >>= fun () -> *)
152+
(* ensure_closed_lwt w *)
153+
(* ) *)
154+
138155
let pread ?stderr argv =
139156
with_pipe_from_child @@ fun ~r ~w ->
140157
let child = exec ~stdout:(`FD_move_safely w) ?stderr argv in

0 commit comments

Comments
 (0)