Skip to content

Commit 8225864

Browse files
patricoferristalex5
authored andcommitted
generalise the sandbox
1 parent ee67a5c commit 8225864

11 files changed

Lines changed: 263 additions & 115 deletions

File tree

lib/build.ml

Lines changed: 19 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -221,66 +221,7 @@ module Make (Raw_store : S.STORE) (Sandbox : S.SANDBOX) = struct
221221
| `Shell shell ->
222222
k ~base ~context:{context with shell}
223223

224-
let export_env base : Config.env Lwt.t =
225-
Os.pread ["docker"; "image"; "inspect";
226-
"--format"; {|{{range .Config.Env}}{{print . "\x00"}}{{end}}|};
227-
"--"; base] >|= fun env ->
228-
String.split_on_char '\x00' env
229-
|> List.filter_map (function
230-
| "\n" -> None
231-
| kv ->
232-
match Astring.String.cut ~sep:"=" kv with
233-
| None -> Fmt.failwith "Invalid environment in Docker image %S (should be 'K=V')" kv
234-
| Some _ as pair -> pair
235-
)
236-
237-
let copy_to_log ~src ~dst =
238-
let buf = Bytes.create 4096 in
239-
let rec aux () =
240-
Lwt_unix.read src buf 0 (Bytes.length buf) >>= function
241-
| 0 -> Lwt.return_unit
242-
| n -> Build_log.write dst (Bytes.sub_string buf 0 n) >>= aux
243-
in
244-
aux ()
245-
246-
let with_container ~log base fn =
247-
Os.with_pipe_from_child (fun ~r ~w ->
248-
(* We might need to do a pull here, so log the output to show progress. *)
249-
let copy = copy_to_log ~src:r ~dst:log in
250-
Os.pread ~stderr:(`FD_move_safely w) ["docker"; "create"; "--"; base] >>= fun cid ->
251-
copy >|= fun () ->
252-
String.trim cid
253-
) >>= fun cid ->
254-
Lwt.finalize
255-
(fun () -> fn cid)
256-
(fun () -> Os.exec ~stdout:`Dev_null ["docker"; "rm"; "--"; cid])
257-
258-
let get_base t ~log base =
259-
log `Heading (Fmt.strf "(from %a)" Sexplib.Sexp.pp_hum (Atom base));
260-
let id = Sha256.to_hex (Sha256.string base) in
261-
Store.build t.store ~id ~log (fun ~cancelled:_ ~log tmp ->
262-
Log.info (fun f -> f "Base image not present; importing %S..." base);
263-
let rootfs = tmp / "rootfs" in
264-
Os.sudo ["mkdir"; "--mode=755"; "--"; rootfs] >>= fun () ->
265-
(* Lwt_process.exec ("", [| "docker"; "pull"; "--"; base |]) >>= fun _ -> *)
266-
with_container ~log base (fun cid ->
267-
Os.with_pipe_between_children @@ fun ~r ~w ->
268-
let exporter = Os.exec ~stdout:(`FD_move_safely w) ["docker"; "export"; "--"; cid] in
269-
let tar = Os.sudo ~stdin:(`FD_move_safely r) ["tar"; "-C"; rootfs; "-xf"; "-"] in
270-
exporter >>= fun () ->
271-
tar
272-
) >>= fun () ->
273-
export_env base >>= fun env ->
274-
Os.write_file ~path:(tmp / "env")
275-
(Sexplib.Sexp.to_string_hum Saved_context.(sexp_of_t {env})) >>= fun () ->
276-
Lwt_result.return ()
277-
)
278-
>>!= fun id ->
279-
let path = Option.get (Store.result t.store id) in
280-
let { Saved_context.env } = Saved_context.t_of_sexp (Sexplib.Sexp.load_sexp (path / "env")) in
281-
Lwt_result.return (id, env)
282-
283-
let rec build ~scope t context { Obuilder_spec.child_builds; from = base; ops } =
224+
let rec build ~scope t context { Obuilder_spec.child_builds; from; ops } =
284225
let rec aux context = function
285226
| [] -> Lwt_result.return context
286227
| (name, child_spec) :: child_builds ->
@@ -291,7 +232,18 @@ module Make (Raw_store : S.STORE) (Sandbox : S.SANDBOX) = struct
291232
aux context child_builds
292233
in
293234
aux context child_builds >>!= fun context ->
294-
get_base t ~log:context.Context.log base >>!= fun (id, env) ->
235+
let log = context.Context.log in
236+
let id = Sha256.to_hex (Sha256.string from) in
237+
let f = Sandbox.from ~from ~log t.sandbox in
238+
(Store.build t.store ~id ~log f >>!= fun id ->
239+
(match Store.result t.store id with
240+
| Some path ->
241+
if Sys.file_exists @@ path / "env" then begin
242+
let { Saved_context.env } = Saved_context.t_of_sexp (Sexplib.Sexp.load_sexp (path / "env")) in
243+
Lwt_result.return (id, env)
244+
end else Lwt_result.return (id, [])
245+
| None -> Lwt_result.return (id, [])))
246+
>>!= fun (id, env) ->
295247
let context = { context with env = context.env @ env } in
296248
run_steps t ~context ~base:id ops
297249

@@ -325,7 +277,12 @@ module Make (Raw_store : S.STORE) (Sandbox : S.SANDBOX) = struct
325277
(* Get the base image first, before starting the timer. *)
326278
let switch = Lwt_switch.create () in
327279
let context = Context.v ~switch ~log ~src_dir:"/tmp" () in
328-
get_base t ~log healthcheck_base >>= function
280+
let id = Sha256.to_hex (Sha256.string healthcheck_base) in
281+
let f = Sandbox.from ~from:healthcheck_base ~log t.sandbox in
282+
(Store.build t.store ~id ~log f >>!= fun id ->
283+
let path = Option.get (Store.result t.store id) in
284+
let { Saved_context.env } = Saved_context.t_of_sexp (Sexplib.Sexp.load_sexp (path / "env")) in
285+
Lwt_result.return (id, env)) >>= function
329286
| Error (`Msg _) as x -> Lwt.return x
330287
| Error `Cancelled -> failwith "Cancelled getting base image (shouldn't happen!)"
331288
| Ok (id, env) ->

lib/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
(name obuilder)
33
(public_name obuilder)
44
(preprocess (pps ppx_sexp_conv))
5-
(libraries lwt lwt.unix fmt yojson tar-unix sexplib sqlite3 astring logs sha obuilder-spec))
5+
(libraries lwt lwt.unix fmt yojson tar-unix sexplib sqlite3 astring logs sha obuilder-spec cmdliner))

lib/runc_sandbox.ml

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
open Lwt.Infix
2+
open Sexplib.Conv
23

34
let ( / ) = Filename.concat
45

@@ -8,6 +9,12 @@ type t = {
89
arches : string list;
910
}
1011

12+
type config = {
13+
fast_sync : bool;
14+
} [@@deriving sexp]
15+
16+
let sandbox_type = "runc"
17+
1118
let get_machine () =
1219
let ch = Unix.open_process_in "uname -m" in
1320
let arch = input_line ch in
@@ -27,6 +34,12 @@ let get_arches () =
2734

2835
let secret_file id = "secret-" ^ string_of_int id
2936

37+
module Saved_context = struct
38+
type t = {
39+
env : Config.env;
40+
} [@@deriving sexp]
41+
end
42+
3043
module Json_config = struct
3144
let mount ?(options=[]) ~ty ~src dst =
3245
`Assoc [
@@ -93,7 +106,7 @@ module Json_config = struct
93106
] else [
94107
]
95108

96-
let seccomp_policy t =
109+
let seccomp_policy (t : t) =
97110
let fields = [
98111
"defaultAction", `String "SCMP_ACT_ALLOW";
99112
"syscalls", `List (seccomp_syscalls ~fast_sync:t.fast_sync);
@@ -279,6 +292,52 @@ let copy_to_log ~src ~dst =
279292
in
280293
aux ()
281294

295+
let export_env base : Config.env Lwt.t =
296+
Os.pread ["docker"; "image"; "inspect";
297+
"--format"; {|{{range .Config.Env}}{{print . "\x00"}}{{end}}|};
298+
"--"; base] >|= fun env ->
299+
String.split_on_char '\x00' env
300+
|> List.filter_map (function
301+
| "\n" -> None
302+
| kv ->
303+
match Astring.String.cut ~sep:"=" kv with
304+
| None -> Fmt.failwith "Invalid environment in Docker image %S (should be 'K=V')" kv
305+
| Some _ as pair -> pair
306+
)
307+
308+
let with_container ~log base fn =
309+
Os.with_pipe_from_child (fun ~r ~w ->
310+
(* We might need to do a pull here, so log the output to show progress. *)
311+
let copy = copy_to_log ~src:r ~dst:log in
312+
Os.pread ~stderr:(`FD_move_safely w) ["docker"; "create"; "--"; base] >>= fun cid ->
313+
copy >|= fun () ->
314+
String.trim cid
315+
) >>= fun cid ->
316+
Lwt.finalize
317+
(fun () -> fn cid)
318+
(fun () -> Os.exec ~stdout:`Dev_null ["docker"; "rm"; "--"; cid])
319+
320+
let from ~log ~from _t =
321+
let base = from in
322+
log `Heading (Fmt.strf "(from %a)" Sexplib.Sexp.pp_hum (Atom base));
323+
(fun ~cancelled:_ ~log tmp ->
324+
Log.info (fun f -> f "Base image not present; importing %S...@." base);
325+
let rootfs = tmp / "rootfs" in
326+
Os.sudo ["mkdir"; "--mode=755"; "--"; rootfs] >>= fun () ->
327+
(* Lwt_process.exec ("", [| "docker"; "pull"; "--"; base |]) >>= fun _ -> *)
328+
with_container ~log base (fun cid ->
329+
Os.with_pipe_between_children @@ fun ~r ~w ->
330+
let exporter = Os.exec ~stdout:(`FD_move_safely w) ["docker"; "export"; "--"; cid] in
331+
let tar = Os.sudo ~stdin:(`FD_move_safely r) ["tar"; "-C"; rootfs; "-xf"; "-"] in
332+
exporter >>= fun () ->
333+
tar
334+
) >>= fun () ->
335+
export_env base >>= fun env ->
336+
Os.write_file ~path:(tmp / "env")
337+
(Sexplib.Sexp.to_string_hum Saved_context.(sexp_of_t {env})) >>= fun () ->
338+
Lwt_result.return ()
339+
)
340+
282341
let run ~cancelled ?stdin:stdin ~log t config results_dir =
283342
Lwt_io.with_temp_dir ~perm:0o700 ~prefix:"obuilder-runc-" @@ fun tmp ->
284343
let json_config = Json_config.make config ~config_dir:tmp ~results_dir t in
@@ -329,9 +388,28 @@ let clean_runc dir =
329388
Os.sudo ["runc"; "--root"; dir; "delete"; item]
330389
)
331390

332-
let create ?(fast_sync=false) ~runc_state_dir () =
333-
Os.ensure_dir runc_state_dir;
334-
let arches = get_arches () in
335-
Log.info (fun f -> f "Architectures for multi-arch system: %a" Fmt.(Dump.list string) arches);
336-
clean_runc runc_state_dir >|= fun () ->
337-
{ runc_state_dir; fast_sync; arches }
391+
let create ?state_dir (c : config) =
392+
match state_dir with
393+
| None -> Fmt.failwith "Runc requires a state directory"
394+
| Some runc_state_dir ->
395+
Os.ensure_dir runc_state_dir;
396+
let arches = get_arches () in
397+
Log.info (fun f -> f "Architectures for multi-arch system: %a" Fmt.(Dump.list string) arches);
398+
clean_runc runc_state_dir >|= fun () ->
399+
{ runc_state_dir; fast_sync = c.fast_sync; arches }
400+
401+
open Cmdliner
402+
403+
let fast_sync =
404+
Arg.value @@
405+
Arg.opt Arg.bool false @@
406+
Arg.info
407+
~doc:"Install a seccomp filter that skips allsync syscalls"
408+
~docv:"FAST_SYNC"
409+
["fast-sync"]
410+
411+
let cmdliner : config Term.t =
412+
let make fast_sync =
413+
{ fast_sync }
414+
in
415+
Term.(const make $ fast_sync)

lib/runc_sandbox.mli

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,3 @@
22

33
include S.SANDBOX
44

5-
val create : ?fast_sync:bool -> runc_state_dir:string -> unit -> t Lwt.t
6-
(** [create dir] is a runc sandboxing system that keeps state in [dir].
7-
@param fast_sync Use seccomp to skip all sync syscalls. This is fast (and
8-
safe, since we discard builds after a crash), but requires
9-
runc version 1.0.0-rc92 or later. Note that the runc version
10-
is not the same as the spec version. If "runc --version"
11-
only prints the spec version, then it's too old. *)

lib/s.ml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,34 @@ end
6464
module type SANDBOX = sig
6565
type t
6666

67+
val sandbox_type : string
68+
(** A string declaring the type of sandboxing environment *)
69+
70+
type config [@@deriving sexp]
71+
(** The type of sandbox configurations *)
72+
73+
val cmdliner : config Cmdliner.Term.t
74+
(** [cmdliner] is used for command-line interfaces to generate the necessary flags
75+
and parameters to setup a specific sandbox's configuration. *)
76+
77+
val create : ?state_dir:string -> config -> t Lwt.t
78+
(** [create ?state_dir config] generates a new sandbox -- the state directory is used for
79+
runc environments where the store's state directory can be passed in, otherwise just leave
80+
it out. *)
81+
82+
val from :
83+
log:logger ->
84+
from:string ->
85+
t ->
86+
cancelled:unit Lwt.t ->
87+
log:Build_log.t ->
88+
string -> (unit, [ `Cancelled | `Msg of string ]) result Lwt.t
89+
(** [from t ~log ~from_stage] generates the function to be run as the initial build-step
90+
for the sandboxing environment using Obuilder's from stage.
91+
@param log Used for writing logs.
92+
@param from The base template to build a new sandbox from (e.g. docker image hash).
93+
*)
94+
6795
val run :
6896
cancelled:unit Lwt.t ->
6997
?stdin:Os.unix_fd ->

main.ml

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ let log tag msg =
1515
| `Note -> Fmt.pr "%a@." Fmt.(styled (`Fg `Yellow) string) msg
1616
| `Output -> output_string stdout msg; flush stdout
1717

18-
let create_builder ?fast_sync spec =
18+
let create_builder spec conf =
1919
Obuilder.Store_spec.to_store spec >>= fun (Store ((module Store), store)) ->
2020
let module Builder = Obuilder.Builder(Store)(Sandbox) in
21-
Sandbox.create ~runc_state_dir:(Store.state_dir store / "runc") ?fast_sync () >|= fun sandbox ->
21+
Sandbox.create ~state_dir:(Store.state_dir store / "runc") conf >|= fun sandbox ->
2222
let builder = Builder.v ~store ~sandbox in
2323
Builder ((module Builder), builder)
2424

@@ -28,9 +28,10 @@ let read_whole_file path =
2828
let len = in_channel_length ic in
2929
really_input_string ic len
3030

31-
let build fast_sync store spec src_dir secrets =
31+
32+
let build store spec conf src_dir secrets =
3233
Lwt_main.run begin
33-
create_builder ~fast_sync store >>= fun (Builder ((module Builder), builder)) ->
34+
create_builder store conf >>= fun (Builder ((module Builder), builder)) ->
3435
let spec =
3536
try Obuilder.Spec.t_of_sexp (Sexplib.Sexp.load_sexp spec)
3637
with Failure msg ->
@@ -51,11 +52,11 @@ let build fast_sync store spec src_dir secrets =
5152
exit 1
5253
end
5354

54-
let healthcheck fast_sync verbose store =
55+
let healthcheck verbose store conf =
5556
if verbose then
5657
Logs.Src.set_level Obuilder.log_src (Some Logs.Info);
5758
Lwt_main.run begin
58-
create_builder ~fast_sync store >>= fun (Builder ((module Builder), builder)) ->
59+
create_builder store conf >>= fun (Builder ((module Builder), builder)) ->
5960
Builder.healthcheck builder >|= function
6061
| Error (`Msg m) ->
6162
Fmt.epr "Healthcheck failed: %s@." m;
@@ -64,9 +65,9 @@ let healthcheck fast_sync verbose store =
6465
Fmt.pr "Healthcheck passed@."
6566
end
6667

67-
let delete store id =
68+
let delete store conf id =
6869
Lwt_main.run begin
69-
create_builder store >>= fun (Builder ((module Builder), builder)) ->
70+
create_builder store conf >>= fun (Builder ((module Builder), builder)) ->
7071
Builder.delete builder id ~log:(fun id -> Fmt.pr "Removing %s@." id)
7172
end
7273

@@ -113,29 +114,22 @@ let id =
113114
~docv:"ID"
114115
[]
115116

116-
let fast_sync =
117-
Arg.value @@
118-
Arg.flag @@
119-
Arg.info
120-
~doc:"Ignore sync syscalls (requires runc >= 1.0.0-rc92)"
121-
["fast-sync"]
122-
123117
let secrets =
124118
(Arg.value @@
125-
Arg.(opt_all (pair ~sep:':' string file)) [] @@
126-
Arg.info
127-
~doc:"Provide a secret under the form id:file"
128-
~docv:"SECRET"
129-
["secret"])
119+
Arg.(opt_all (pair ~sep:':' string file)) [] @@
120+
Arg.info
121+
~doc:"Provide a secret under the form id:file"
122+
~docv:"SECRET"
123+
["secret"])
130124

131125
let build =
132126
let doc = "Build a spec file." in
133-
Term.(const build $ fast_sync $ store $ spec_file $ src_dir $ secrets),
127+
Term.(const build $ store $ spec_file $ Sandbox.cmdliner $ src_dir $ secrets),
134128
Term.info "build" ~doc
135129

136130
let delete =
137131
let doc = "Recursively delete a cached build result." in
138-
Term.(const delete $ store $ id),
132+
Term.(const delete $ store $ Sandbox.cmdliner $ id),
139133
Term.info "delete" ~doc
140134

141135
let buildkit =
@@ -159,7 +153,7 @@ let verbose =
159153

160154
let healthcheck =
161155
let doc = "Perform a self-test" in
162-
Term.(const healthcheck $ fast_sync $ verbose $ store),
156+
Term.(const healthcheck $ verbose $ store $ Sandbox.cmdliner),
163157
Term.info "healthcheck" ~doc
164158

165159
let cmds = [build; delete; dockerfile; healthcheck]

0 commit comments

Comments
 (0)