this repo has no description

Compare changes

Choose any two refs to compare.

+5 -3
dune-project
···
(synopsis "Shelter from the Storm")
(description "A shell session shim to help you explore!")
(depends
-
ocaml
+
(ocaml (< "5.3.0~~")) ; for the irmin pin only
+
(ctypes (< "0.23.0")) ; for a const ptr mismatch with zfs from https://github.com/yallop/ocaml-ctypes/pull/782
eio_posix
-
void
zfs
cid
ppx_repr
-
irmin-fs
+
irmin-git
+
morbig
+
ppx_blob
cmdliner
)
(tags
+8 -4
shelter.opam
···
bug-reports: "https://github.com/username/reponame/issues"
depends: [
"dune" {>= "3.17"}
-
"ocaml"
+
"ocaml" {< "5.3.0~~"}
+
"ctypes" {< "0.23.0"}
"eio_posix"
-
"void"
"zfs"
"cid"
"ppx_repr"
-
"irmin-fs"
+
"irmin-git"
+
"morbig"
+
"ppx_blob"
"cmdliner"
"odoc" {with-doc}
]
···
[ "zfs.dev" "git+https://github.com/patricoferris/ocaml-zfs" ]
[ "void.dev" "git+https://github.com/quantifyearth/void" ]
[ "irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
-
[ "irmin-fs.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "ppx_irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "irmin-git.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "irmin-watcher.dev" "git+https://github.com/patricoferris/irmin-watcher#675125d9e95cd09ef0c18ab1d9d6d69a26856b9f" ]
]
+3 -1
shelter.opam.template
···
[ "zfs.dev" "git+https://github.com/patricoferris/ocaml-zfs" ]
[ "void.dev" "git+https://github.com/quantifyearth/void" ]
[ "irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
-
[ "irmin-fs.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "ppx_irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "irmin-git.dev" "git+https://github.com/mirage/irmin#eio" ]
+
[ "irmin-watcher.dev" "git+https://github.com/patricoferris/irmin-watcher#675125d9e95cd09ef0c18ab1d9d6d69a26856b9f" ]
]
+13 -5
src/bin/main.ml
···
Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 path;
path
+
module Eventloop = struct
+
let run fn =
+
Eio_posix.run @@ fun env ->
+
Lwt_eio.with_event_loop ~clock:env#clock @@ fun _token -> fn env
+
end
+
(* Command Line *)
open Cmdliner
···
let main =
let run config cmd_file =
-
Eio_posix.run @@ fun env ->
+
Eventloop.run @@ fun env ->
let cmd_file = Option.map (Eio.Path.( / ) env#fs) cmd_file in
let dir = state_dir env#fs "shelter" in
-
Main.main config env#fs env#clock env#process_mgr dir cmd_file
+
let stdout = (env#stdout :> Eio.Flow.sink_ty Eio.Flow.sink) in
+
Main.main config ~stdout env#fs env#clock env#process_mgr dir cmd_file
in
let t = Term.(const run $ Shelter_main.config_term $ cmd_file) in
let man =
···
let passthrough =
let run config cmd_file =
-
Eio_posix.run @@ fun env ->
+
Eventloop.run @@ fun env ->
let cmd_file = Option.map (Eio.Path.( / ) env#fs) cmd_file in
let dir = state_dir env#fs "passthrough" in
-
Pass.main config env#fs env#clock env#process_mgr dir cmd_file
+
let stdout = (env#stdout :> Eio.Flow.sink_ty Eio.Flow.sink) in
+
Pass.main config ~stdout env#fs env#clock env#process_mgr dir cmd_file
in
let t = Term.(const run $ Shelter_passthrough.config_term $ cmd_file) in
let info = Cmd.info "passthrough" in
···
let extract_commands =
let run cmd_file =
-
Eio_posix.run @@ fun env ->
+
Eventloop.run @@ fun env ->
let cmd_file = Eio.Path.( / ) env#fs (Option.get cmd_file) in
Shelter.Script.to_commands cmd_file |> List.iter (Fmt.pr "%s\n")
in
+1 -1
src/lib/dune
···
(library
(name shelter)
(public_name shelter)
-
(libraries cmdliner irmin-fs.unix eio.unix eio linenoise void repr morbig))
+
(libraries cmdliner irmin-git.unix eio.unix eio linenoise void repr morbig))
+9 -1
src/lib/engine.ml
···
(** A context that is not persisted, but is passed through each loop of the
shell *)
+
type error
+
(** Shell specific errors *)
+
+
val pp_error : error Fmt.t
+
val init :
_ Eio.Path.t ->
Eio_unix.Process.mgr_ty Eio_unix.Process.mgr ->
···
val run :
config ->
+
stdout:Eio.Flow.sink_ty Eio.Flow.sink ->
Eio.Fs.dir_ty Eio.Path.t ->
_ Eio.Time.clock ->
Eio_unix.Process.mgr_ty Eio_unix.Process.mgr ->
entry History.t * ctx ->
action ->
-
(entry History.t * ctx, Eio.Process.error) result
+
( entry History.t * ctx,
+
[ `Process of Eio.Process.error | `Shell of error ] )
+
result
(** [run history action] runs the action in [history]. Return a new [history]
that can be persisted *)
+7 -3
src/lib/passthrough/shelter_passthrough.ml
···
open Eio
+
type error = string
+
+
let pp_error = Fmt.string
+
type config = unit
let config_term = Cmdliner.Term.const ()
···
in
List.iter (fun v -> LNoise.history_add v |> ignore) entries
-
let run (() : config) _fs clock proc
+
let run (() : config) ~stdout:_ _fs clock proc
( ((Shelter.History.Store ((module S), store) : entry Shelter.History.t) as
full_store),
() ) (Exec command) =
···
S.set_exn ~info store (key ()) command;
let _ : (unit, string) result = LNoise.history_add command in
Ok (full_store, ()))
-
else Error (Eio.Process.Child_error res)
-
with Eio.Exn.Io (Eio.Process.E e, _) -> Error e
+
else Shelter.process_error (Eio.Process.Child_error res)
+
with Eio.Exn.Io (Eio.Process.E e, _) -> Shelter.process_error e
+8 -1
src/lib/shelter/dune
···
+
; (rule
+
; (target opentrace)
+
; (action
+
; (with-stdout-to opentrace (run echo hello))))
+
(library
(name shelter_main)
(public_name shelter.main)
+
(preprocessor_deps
+
(file opentrace))
(preprocess
-
(pps ppx_repr))
+
(pps ppx_repr ppx_blob))
(libraries shelter cid void zfs))
src/lib/shelter/opentrace

This is a binary file and will not be displayed.

+85 -41
src/lib/shelter/runc.ml
···
("options", `List (List.map (fun x -> `String x) options));
]
+
type mount = { ty : [ `Bind ]; src : string; dst : string; readonly : bool }
+
+
let user_mounts =
+
List.map @@ fun { ty; src; dst; readonly } ->
+
assert (ty = `Bind);
+
let options = [ "bind"; "nosuid"; "nodev" ] in
+
mount ~ty:"bind" ~src dst
+
~options:(if readonly then "ro" :: options else options)
+
let strings xs = `List (List.map (fun x -> `String x) xs)
let namespace x = `Assoc [ ("type", `String x) ]
···
network : string list;
user : int * int;
env : string list;
+
mounts : mount list;
entrypoint : string option;
}
-
let make { cwd; argv; hostname; network; user; env; entrypoint } ~config_dir
-
~results_dir : Yojson.Safe.t =
+
let make { cwd; argv; hostname; network; user; env; mounts; entrypoint }
+
~config_dir ~results_dir : Yojson.Safe.t =
assert (entrypoint = None);
let user =
let uid, gid = user in
···
`Assoc
[
("type", `String "RLIMIT_NOFILE");
-
("hard", `Int 1024);
-
("soft", `Int 1024);
+
("hard", `Int 10_024);
+
("soft", `Int 10_024);
+
];
+
`Assoc
+
[
+
("type", `String "RLIMIT_MEMLOCK");
+
("hard", `Int 1_000_000);
+
("soft", `Int 1_000_000);
];
] );
("noNewPrivileges", `Bool false);
···
~options:
[ (* TODO: copy to others? *) "nosuid"; "noexec"; "nodev" ]
~ty:"proc" ~src:"proc"
-
:: mount "/dev" ~ty:"tmpfs" ~src:"tmpfs"
-
~options:[ "nosuid"; "strictatime"; "mode=755"; "size=65536k" ]
-
:: mount "/dev/pts" ~ty:"devpts" ~src:"devpts"
-
~options:
-
[
-
"nosuid";
-
"noexec";
-
"newinstance";
-
"ptmxmode=0666";
-
"mode=0620";
-
"gid=5";
-
(* tty *)
-
]
-
:: mount
-
"/sys"
-
(* This is how Docker does it. runc's default is a bit different. *)
-
~ty:"sysfs" ~src:"sysfs"
-
~options:[ "nosuid"; "noexec"; "nodev"; "ro" ]
-
:: mount "/sys/fs/cgroup" ~ty:"cgroup" ~src:"cgroup"
-
~options:[ "ro"; "nosuid"; "noexec"; "nodev" ]
-
:: mount "/dev/shm" ~ty:"tmpfs" ~src:"shm"
-
~options:
-
[ "nosuid"; "noexec"; "nodev"; "mode=1777"; "size=65536k" ]
-
:: mount "/dev/mqueue" ~ty:"mqueue" ~src:"mqueue"
-
~options:[ "nosuid"; "noexec"; "nodev" ]
-
:: mount "/etc/hosts" ~ty:"bind" ~src:(config_dir // "hosts")
-
~options:[ "ro"; "rbind"; "rprivate" ]
-
::
-
(if network = [ "host" ] then
-
[
-
mount "/etc/resolv.conf" ~ty:"bind" ~src:"/etc/resolv.conf"
-
~options:[ "ro"; "rbind"; "rprivate" ];
-
]
-
else [])) );
+
:: mount "/dev" ~ty:"tmpfs" ~src:"tmpfs"
+
~options:
+
[ "nosuid"; "strictatime"; "mode=755"; "size=65536k" ]
+
:: mount "/dev/pts" ~ty:"devpts" ~src:"devpts"
+
~options:
+
[
+
"nosuid";
+
"noexec";
+
"newinstance";
+
"ptmxmode=0666";
+
"mode=0620";
+
"gid=5";
+
(* tty *)
+
]
+
:: mount
+
"/sys"
+
(* This is how Docker does it. runc's default is a bit different. *)
+
~ty:"sysfs" ~src:"sysfs"
+
~options:[ "nosuid"; "noexec"; "nodev"; "ro" ]
+
:: mount "/sys/fs/cgroup" ~ty:"cgroup" ~src:"cgroup"
+
~options:[ "ro"; "nosuid"; "noexec"; "nodev" ]
+
:: mount "/dev/shm" ~ty:"tmpfs" ~src:"shm"
+
~options:
+
[ "nosuid"; "noexec"; "nodev"; "mode=1777"; "size=65536k" ]
+
:: mount "/dev/mqueue" ~ty:"mqueue" ~src:"mqueue"
+
~options:[ "nosuid"; "noexec"; "nodev" ]
+
:: mount "/etc/hosts" ~ty:"bind" ~src:(config_dir // "hosts")
+
~options:[ "ro"; "rbind"; "rprivate" ]
+
::
+
(if network = [ "host" ] then
+
[
+
mount "/etc/resolv.conf" ~ty:"bind" ~src:"/etc/resolv.conf"
+
~options:[ "ro"; "rbind"; "rprivate" ];
+
]
+
else [])
+
@ user_mounts mounts) );
( "linux",
`Assoc
[
···
let next_id = ref 0
-
let spawn ~sw fs proc config dir =
+
let to_other_sink_as_well ~other
+
(Eio.Resource.T (t, handler) : Eio.Flow.sink_ty Eio.Flow.sink) =
+
let module Sink = (val Eio.Resource.get handler Eio.Flow.Pi.Sink) in
+
let copy_buf = Buffer.create 128 in
+
let copy () ~src =
+
Eio.Flow.copy src (Eio.Flow.buffer_sink copy_buf);
+
Eio.Flow.copy_string (Buffer.contents copy_buf) other;
+
Sink.copy t ~src:(Buffer.contents copy_buf |> Eio.Flow.string_source);
+
Buffer.clear copy_buf
+
in
+
let single_write () x =
+
let _ : int = Eio.Flow.single_write other x in
+
Sink.single_write t x
+
in
+
let module T = struct
+
type t = unit
+
+
let single_write = single_write
+
let copy = copy
+
end in
+
Eio.Resource.T ((), Eio.Flow.Pi.sink (module T))
+
+
let spawn ~sw log env config dir =
let tmp = Filename.temp_dir ~perms:0o700 "shelter-run-" "" in
-
let eio_tmp = Eio.Path.(fs / tmp) in
+
let eio_tmp = Eio.Path.(env#fs / tmp) in
let json_config = Json_config.make config ~config_dir:tmp ~results_dir:dir in
Eio.Path.save ~create:(`If_missing 0o644) (eio_tmp / "config.json")
(Yojson.Safe.pretty_to_string json_config ^ "\n");
···
let id = string_of_int !next_id in
incr next_id;
let cmd = [ "runc"; "--root"; "runc"; "run"; id ] in
-
Eio.Process.spawn ~sw proc ~cwd:eio_tmp cmd
+
let stdout =
+
to_other_sink_as_well ~other:env#stdout
+
(log :> Eio.Flow.sink_ty Eio.Flow.sink)
+
in
+
Eio.Process.spawn ~sw ~stdout env#proc ~cwd:eio_tmp cmd
(*
Apache License
+388 -193
src/lib/shelter/shelter_main.ml
···
module Store = Store
module H = Shelter.History
+
type error = string
+
+
let pp_error = Fmt.string
+
module History = struct
type mode = Void.mode
···
| "R" -> Void.R | "RW" -> Void.RW | _ -> failwith "Malformed Void.mode")
(function Void.R -> "R" | Void.RW -> "RW")
-
type t = {
+
type post = { diff : Diff.t; time : int64 } [@@deriving repr]
+
+
type pre = {
mode : mode;
build : Store.Build.t;
args : string list;
-
time : int64;
env : string list;
cwd : string;
user : int * int;
-
diff : Diff.t;
}
[@@deriving repr]
+
(** Needed for execution *)
+
+
type t = { pre : pre; post : post } [@@deriving repr]
let merge = Irmin.Merge.(default (Repr.option t))
end
···
type entry = History.t
type action =
+
(* Change modes *)
| Set_mode of History.mode
+
(* Fork a new branch from an existing one,
+
or switch to a branch if it exists *)
| Set_session of string
+
(* Run a command *)
| Exec of string list
-
| Info
+
(* Undo the last command *)
| Undo
-
| Fork of string
+
(* Replay the current branch onto another *)
| Replay of string
+
(* Display info *)
+
| Info of [ `Current | `History ]
+
(* Error state *)
| Unknown of string list
-
| History
[@@deriving repr]
let split_and_remove_empty s =
···
let shelter_action = function
| "mode" :: [ "r" ] -> Set_mode R
-
| "mode" :: [ "rw" ] -> Set_mode R
+
| "mode" :: [ "rw" ] -> Set_mode RW
| "session" :: [ m ] -> Set_session m
-
| "fork" :: [ m ] -> Fork m
| "replay" :: [ onto ] -> Replay onto
-
| [ "info" ] -> Info
+
| [ "info" ] -> Info `Current
| [ "undo" ] -> Undo
-
| [ "history" ] -> History
+
| [ "history" ] -> Info `History
| other -> Unknown other
let action_of_command cmd =
···
let history_key = [ "history" ]
let key clock = history_key @ [ string_of_float @@ Eio.Time.now clock ]
-
let list (H.Store ((module S), store) : entry H.t) =
-
match S.list store history_key with
-
| [] -> []
-
| xs ->
-
let rec loop acc = function
-
| (s, `Contents (v, _meta)) :: next -> loop ((s, v) :: acc) next
-
| _ :: next -> loop acc next
-
| [] -> List.rev acc
+
let history (H.Store ((module S), store) : entry H.t) =
+
let repo = S.repo store in
+
match S.Head.find store with
+
| None -> []
+
| Some hd ->
+
let rec linearize c =
+
match S.Commit.parents c |> List.map (S.Commit.of_hash repo) with
+
| [ Some p ] -> c :: linearize p
+
| _ -> [ c ]
in
-
loop [] (List.map (fun (v, tree) -> (v, S.Tree.to_concrete tree)) xs)
-
|> List.stable_sort (fun (s1, _) (s2, _) ->
-
Float.compare (Float.of_string s1) (Float.of_string s2))
-
|> List.rev
+
let commits = linearize hd in
+
let get_diff_content t1 t2 =
+
match S.Tree.diff t1 t2 with
+
| [ (_, `Added (c, _)) ] -> c
+
| lst ->
+
let pp_diff =
+
Repr.pp (Irmin.Diff.t (Repr.pair History.t S.metadata_t))
+
in
+
Fmt.epr "Get diff (%i) content %a%!" (List.length lst)
+
Fmt.(list ~sep:Fmt.comma (Fmt.pair (Repr.pp S.path_t) pp_diff))
+
lst;
+
invalid_arg "Get diff should only have a single difference."
+
in
+
let hash c = S.Commit.hash c |> S.Hash.to_raw_string in
+
let rec diff_calc = function
+
| [] -> []
+
| [ x ] ->
+
let diff = get_diff_content (S.Tree.empty ()) (S.Commit.tree x) in
+
[ (hash x, diff) ]
+
| c :: p :: rest ->
+
let diff = get_diff_content (S.Commit.tree p) (S.Commit.tree c) in
+
(hash c, diff) :: diff_calc (p :: rest)
+
in
+
diff_calc commits
let with_latest ~default s f =
-
match list s with [] -> default () | hd :: _ -> f hd
+
match history s with [] -> default () | (_, hd) :: _ -> f hd
let text c = Fmt.(styled (`Fg c) string)
···
let repo = S.repo session in
let heads = List.map (fun b -> (S.Branch.find repo b, b)) branches in
let head = S.Head.find session in
-
List.assoc_opt head heads
+
let head_hash =
+
Option.map
+
(fun hash -> String.sub (Fmt.str "%a" S.Commit.pp_hash hash) 0 7)
+
head
+
in
+
(head_hash, List.assoc_opt head heads)
(* Reset the head of the current session by one commit *)
let reset_hard ((H.Store ((module S), session) : entry H.t) as s) =
···
Ok store
(* Fork a new session from an existing one *)
-
let display_history (H.Store ((module S), session) : entry H.t) =
-
let history = S.history ~depth:max_int session in
-
let content c =
-
H.Store ((module S), S.of_commit c) |> list |> List.hd |> snd
-
in
+
let display_history (s : entry H.t) =
let pp_diff fmt d =
if d = [] then () else Fmt.pf fmt "\n %a" (Repr.pp Diff.t) d
in
let pp_entry fmt (e : entry) =
Fmt.pf fmt "%-10s %s%a"
-
Fmt.(str "%a" (styled (`Fg `Yellow) uint64_ns_span) e.time)
-
(String.concat " " e.args) pp_diff e.diff
+
Fmt.(str "%a" (styled (`Fg `Yellow) uint64_ns_span) e.post.time)
+
(String.concat " " e.pre.args)
+
pp_diff e.post.diff
in
-
let linearize =
-
S.History.fold_vertex (fun c v -> content c :: v) history [] |> List.rev
-
in
-
List.iter (fun c -> Fmt.pr "%a\n%!" pp_entry c) linearize
+
let entries = history s |> List.rev in
+
List.iter (fun (_hash, c) -> Fmt.pr "%a\n%!" pp_entry c) entries
let prompt status ((H.Store ((module S), _session) : entry H.t) as store) =
-
let sesh = Option.value ~default:"main" (which_branch store) in
+
let head, sesh = which_branch store in
+
let sesh = Option.value ~default:"main" sesh in
let prompt () =
Fmt.(styled (`Fg `Yellow) string) Format.str_formatter "shelter> ";
Format.flush_str_formatter ()
in
-
let pp_sesh fmt sesh = Fmt.pf fmt "[%a]" (text `Green) sesh in
+
let pp_head fmt = function
+
| None -> Fmt.nop fmt ()
+
| Some h -> Fmt.pf fmt "#%a" (text `Magenta) h
+
in
+
let pp_sesh fmt sesh = Fmt.pf fmt "[%a%a]" (text `Green) sesh pp_head head in
let pp_status fmt = function
| `Exited 0 -> Fmt.nop fmt ()
| `Exited n -> Fmt.pf fmt "%a " (text `Red) (string_of_int n)
| _ -> Fmt.nop fmt ()
in
-
let prompt_entry (_, (e : entry)) =
+
let prompt_entry (e : entry) =
Fmt.pf Format.str_formatter "%a%a%a : { mode: %a }> " pp_status status
(text `Yellow) "shelter" pp_sesh sesh (text `Red)
-
(if e.mode = R then "r" else "rw");
+
(if e.pre.mode = R then "r" else "rw");
Format.flush_str_formatter ()
in
with_latest store ~default:prompt prompt_entry
-
type ctx = Store.t
+
type ctx = { store : Store.t; tool_dir : string }
+
+
let tools = [ ("opentrace", Tools.opentrace) ]
let init fs proc s =
let store = Store.init fs proc "shelter" in
List.iter
-
(fun (_, { History.args; _ }) ->
+
(fun (_, { History.pre = { History.args; _ }; _ }) ->
LNoise.history_add (String.concat " " args) |> ignore)
-
(list s);
-
store
+
(history s);
+
let tool_cid = Store.cid (String.concat ":" (List.map snd tools)) in
+
let tools =
+
Store.Run.with_tool store tool_cid @@ fun tool_dir ->
+
Eio.Fiber.List.iter
+
(fun (toolname, content) ->
+
let new_path = Eio.Path.(fs / tool_dir / toolname) in
+
Eio.Path.save ~create:(`If_missing 0o755) new_path content)
+
tools;
+
tool_dir
+
in
+
{ store; tool_dir = tools }
+
+
(* Run a command:
+
+
- TODO: pretty confusing that we `entry` to build from and also as the
+
thing we are building (e.g. the build field and the args field... *)
+
let exec (config : config) ~stdout fs proc
+
((H.Store ((module S), _) : entry H.t), (ctx : ctx)) (entry : entry) =
+
let build, env, (uid, gid) =
+
match entry.pre.build with
+
| Store.Build.Image img ->
+
let build, env, user = Store.fetch ctx.store img in
+
(build, env, Option.value ~default:(0, 0) user)
+
| Store.Build.Build cid -> (cid, entry.pre.env, entry.pre.user)
+
in
+
let command = entry.pre.args in
+
let hash_entry =
+
{ entry with pre = { entry.pre with build = Build build } }
+
in
+
(* Store things under History.pre, this makes it possible to rediscover
+
the hash for something purely from the arguments needed to execute something
+
rather than needing, for example, the time it took to execute!
+
+
Also, combine it with previous build step. *)
+
let new_cid =
+
Store.cid (Cid.to_string build ^ Repr.to_string History.pre_t hash_entry.pre)
+
in
+
let with_rootfs fn =
+
if entry.pre.mode = R then (Store.Run.with_build ctx.store build fn, [])
+
else
+
let diff_path =
+
Eio.Path.(fs / Filename.temp_dir "shelter-diff-" "" / "diff")
+
in
+
Store.Run.with_clone ctx.store ~src:build new_cid diff_path fn
+
in
+
with_rootfs @@ function
+
| `Exists path ->
+
(* Copy the stdout log to stdout *)
+
let () =
+
Eio.Path.(with_open_in (fs / (path :> string) / "log")) @@ fun ic ->
+
Eio.Flow.copy ic stdout
+
in
+
let c = Eio.Path.(load (fs / (path :> string) / "hash")) in
+
Ok (`Reset c)
+
| `Build rootfs ->
+
let spawn sw log =
+
if config.no_runc then
+
(* Experiment Void Process *)
+
let rootfs = Filename.concat rootfs "rootfs" in
+
let void =
+
Void.empty
+
|> Void.rootfs ~mode:entry.pre.mode rootfs
+
|> Void.cwd entry.pre.cwd
+
(* TODO: Support UIDs |> Void.uid 1000 *)
+
|> Void.exec ~env
+
[
+
config.shell;
+
"-c";
+
String.concat " " command ^ " && env > /tmp/shelter-env";
+
]
+
in
+
`Void (Void.spawn ~sw void |> Void.exit_status)
+
else
+
let tool_mount : Runc.Json_config.mount =
+
{
+
ty = `Bind;
+
src = ctx.tool_dir;
+
dst = "/shelter-tools";
+
readonly = true;
+
}
+
in
+
let config =
+
Runc.Json_config.
+
{
+
cwd = entry.pre.cwd;
+
argv =
+
[
+
config.shell;
+
"-c";
+
String.concat " " command ^ " && env > /tmp/shelter-env";
+
];
+
hostname = "builder";
+
network = [ "host" ];
+
user = (uid, gid);
+
env = entry.pre.env;
+
mounts = [ tool_mount ];
+
entrypoint = None;
+
}
+
in
+
let env =
+
object
+
method fs = fs
+
method proc = proc
+
method stdout = stdout
+
end
+
in
+
`Runc (Runc.spawn ~sw log env config rootfs)
+
in
+
let start, res =
+
Switch.run @@ fun sw ->
+
let log =
+
Eio.Path.open_out ~sw ~create:(`If_missing 0o644)
+
Eio.Path.(fs / rootfs / "log")
+
in
+
let res = spawn sw log in
+
let start = Mtime_clock.now () in
+
match res with
+
| `Runc r -> (start, Eio.Process.await r)
+
| `Void v -> (start, Void.to_eio_status (Eio.Promise.await v))
+
in
+
let stop = Mtime_clock.now () in
+
let span = Mtime.span start stop in
+
let time = Mtime.Span.to_uint64_ns span in
+
(* Add command to history regardless of exit status *)
+
let _ : (unit, string) result =
+
LNoise.history_add (String.concat " " command)
+
in
+
if res = `Exited 0 then (
+
(* Extract env *)
+
let env_path =
+
Eio.Path.(fs / rootfs / "rootfs" / "tmp" / "shelter-env")
+
in
+
let env =
+
Eio.Path.(load env_path)
+
|> String.split_on_char '\n'
+
|> List.filter (fun s -> not (String.equal "" s))
+
in
+
Eio.Path.unlink env_path;
+
let cwd =
+
List.find_map
+
(fun v ->
+
match Astring.String.cut ~sep:"=" v with
+
| Some ("PWD", dir) -> Some dir
+
| _ -> None)
+
env
+
|> Option.value ~default:hash_entry.pre.cwd
+
in
+
if entry.pre.mode = RW then
+
Ok
+
(`Entry
+
( {
+
hash_entry with
+
History.pre =
+
{
+
hash_entry.pre with
+
build = Build new_cid;
+
env;
+
cwd;
+
user = (uid, gid);
+
};
+
},
+
rootfs ))
+
else
+
Ok
+
(`Entry
+
( {
+
pre = { hash_entry.pre with cwd; env; user = (uid, gid) };
+
post = { hash_entry.post with time };
+
},
+
rootfs )))
+
else Shelter.process_error (Eio.Process.Child_error res)
+
+
let complete_exec ((H.Store ((module S), store) as s : entry H.t), ctx) clock fs
+
new_entry diff =
+
match new_entry with
+
| Error e -> Error e
+
| Ok (`Reset c) -> (
+
match
+
S.Hash.unsafe_of_raw_string c |> S.Commit.of_hash (S.repo store)
+
with
+
| None ->
+
Fmt.epr "Resetting to existing entry failed...\n%!";
+
Ok (s, ctx)
+
| Some c ->
+
S.Head.set store c;
+
Ok (s, ctx))
+
| Ok (`Entry (entry, path)) ->
+
(* Set diff *)
+
let entry = History.{ entry with post = { entry.post with diff } } in
+
(* Commit if RW *)
+
if entry.pre.mode = RW then (
+
commit
+
~message:("exec " ^ String.concat " " entry.pre.args)
+
clock s entry;
+
(* Save the commit hash for easy restoring later *)
+
let hash = S.Head.get store |> S.Commit.hash |> S.Hash.to_raw_string in
+
Eio.Path.save ~create:(`If_missing 0o644)
+
Eio.Path.(fs / path / "hash")
+
hash);
+
Ok (s, ctx)
+
+
let replay config (H.Store ((module S), s) as store : entry H.t) ctx fs clock
+
proc stdout existing_branch =
+
let seshes = sessions store in
+
if not (List.exists (String.equal existing_branch) seshes) then (
+
Fmt.epr "%s does not exist!" existing_branch;
+
Ok (store, ctx))
+
else
+
let repo = S.repo s in
+
let onto = S.of_branch repo existing_branch in
+
match S.lcas ~n:1 s onto with
+
| Error lcas_error ->
+
Fmt.epr "Replay LCAS: %a" (Repr.pp S.lca_error_t) lcas_error;
+
Ok (store, ctx)
+
| Ok [ lcas ] -> (
+
let all_commits = history store in
+
let lcas_hash = S.Commit.hash lcas |> S.Hash.to_raw_string in
+
let rec collect = function
+
| [] -> []
+
| (x, _) :: _ when String.equal lcas_hash x -> []
+
| v :: vs -> v :: collect vs
+
in
+
let commits_to_apply = collect all_commits in
+
match commits_to_apply with
+
| [] -> Shelter.shell_error ""
+
| (h, first) :: rest ->
+
let _, last_other =
+
history (H.Store ((module S), onto)) |> List.hd
+
in
+
let new_first =
+
{
+
first with
+
pre = { first.pre with build = last_other.pre.build };
+
}
+
in
+
let commits_to_apply = (h, new_first) :: rest in
+
(* Now we reset our head to point to the other store's head
+
and replay our commits onto it *)
+
let other_head = S.Head.get onto in
+
S.Head.set s other_head;
+
let res =
+
List.fold_left
+
(fun last (_, (entry : entry)) ->
+
match last with
+
| Error _ as e -> e
+
| Ok (new_store, new_ctx) ->
+
let new_entry, diff =
+
exec config ~stdout fs proc (new_store, new_ctx) entry
+
in
+
complete_exec (new_store, new_ctx) clock fs new_entry diff)
+
(Ok (H.Store ((module S), s), ctx))
+
commits_to_apply
+
in
+
res)
+
| _ -> assert false (* Because n = 1 *)
-
let run (config : config) fs clock proc
-
(((H.Store ((module S), store) : entry H.t) as s), ctx) = function
+
let run (config : config) ~stdout fs clock proc
+
(((H.Store ((module S), store) : entry H.t) as s), (ctx : ctx)) = function
| Set_mode mode ->
-
with_latest ~default:(fun _ -> Ok (s, ctx)) s @@ fun (_, entry) ->
-
commit ~message:"mode change" clock s { entry with mode };
+
with_latest ~default:(fun _ -> Ok (s, ctx)) s @@ fun entry ->
+
commit ~message:"mode change" clock s
+
{ entry with pre = { entry.pre with mode } };
Ok (s, ctx)
-
| Set_session m ->
-
with_latest ~default:(fun _ -> Ok (s, ctx)) s @@ fun (_, entry) ->
-
let new_store = S.of_branch (S.repo store) m in
-
let new_full_store = H.Store ((module S), new_store) in
-
commit ~message:"new session" clock new_full_store entry;
-
Ok (new_full_store, ctx)
+
| Set_session m -> (
+
(* Either set the session if the branch exists or create a new branch
+
from the latest commit of the current branch *)
+
let sessions = sessions s in
+
match List.exists (String.equal m) sessions with
+
| true ->
+
let sesh = S.of_branch (S.repo store) m in
+
Ok (H.Store ((module S), sesh), ctx)
+
| false -> (
+
match fork s m with
+
| Error err ->
+
Fmt.pr "[fork]: %a\n%!" (text `Red) err;
+
Ok (s, ctx)
+
| Ok store -> Ok (store, ctx)))
| Unknown args ->
-
Fmt.epr "%a: %s\n%!" (text `Red) "Unknown Shelter Action"
-
(String.concat " " args);
-
Ok (s, ctx)
-
| Info ->
+
Fmt.epr "%a" (text `Red) "Unknown Shelter Action\n";
+
Shelter.shell_error (String.concat " " args)
+
| Info `Current ->
let sessions = sessions s in
-
let sesh = Option.value ~default:"main" (which_branch s) in
-
let history = S.history store in
+
let sesh = Option.value ~default:"main" (snd (which_branch s)) in
+
let history = history s in
let pp_commit fmt (hash, msg) =
Fmt.pf fmt "[%a]: %s" (text `Yellow) hash msg
in
+
let repo = S.repo store in
let commits =
-
S.History.fold_vertex
-
(fun commit acc ->
+
List.fold_left
+
(fun acc (commit, _) ->
+
let commit =
+
S.Hash.unsafe_of_raw_string commit
+
|> S.Commit.of_hash repo |> Option.get
+
in
let info = S.Commit.info commit |> S.Info.message in
let hash = S.Commit.hash commit |> Repr.to_string S.Hash.t in
(String.sub hash 0 7, info) :: acc)
-
history []
+
[] history
in
let latest =
with_latest
~default:(fun () -> None)
s
-
(fun (_, e) -> Some (Repr.to_string Store.Build.t e.build))
+
(fun e -> Some (Repr.to_string Store.Build.t e.pre.build))
in
Fmt.pr "Sessions: %a\nCurrent: %a\nHash: %a\nCommits:@. %a\n%!"
Fmt.(list ~sep:(Fmt.any ", ") string)
···
Ok (s, ctx)
| Exec [] -> Ok (s, ctx)
| Undo -> Ok (reset_hard s, ctx)
-
| Fork new_branch -> (
-
match fork s new_branch with
-
| Error err ->
-
Fmt.pr "[fork]: %a\n%!" (text `Red) err;
-
Ok (s, ctx)
-
| Ok store -> Ok (store, ctx))
-
| Replay _ -> Ok (s, ctx)
-
| History ->
+
| Replay branch -> replay config s ctx fs clock proc stdout branch
+
| Info `History ->
display_history s;
Ok (s, ctx)
| Exec command -> (
···
~default:(fun () ->
History.
{
-
mode = Void.RW;
-
build = Store.Build.Image config.image;
-
args = command;
-
time = 0L;
-
diff = [];
-
(* TODO: extract with fetch *)
-
env = [];
-
cwd = "/";
-
user = (0, 0);
+
pre =
+
{
+
mode = Void.RW;
+
build = Store.Build.Image config.image;
+
args = command;
+
(* TODO: extract with fetch *)
+
env = [];
+
cwd = "/";
+
user = (0, 0);
+
};
+
post = { diff = []; time = 0L };
})
-
s
-
@@ fun (_, e) -> e
+
s Fun.id
in
-
let build, env, (uid, gid) =
-
match entry.build with
-
| Store.Build.Image img ->
-
let build, env, user = Store.fetch ctx img in
-
(build, env, Option.value ~default:(0, 0) user)
-
| Store.Build.Build cid -> (cid, entry.env, entry.user)
-
in
-
let hash_entry = { entry with build = Build build; args = command } in
-
let new_cid = Store.cid (Repr.to_string History.t hash_entry) in
-
let with_rootfs fn =
-
if entry.mode = R then (Store.Run.with_build ctx build fn, [])
-
else Store.Run.with_clone ctx ~src:build new_cid fn
-
in
+
let entry = { entry with pre = { entry.pre with args = command } } in
try
-
let new_entry, diff =
-
with_rootfs @@ fun rootfs ->
-
let spawn sw =
-
if config.no_runc then
-
let rootfs = Filename.concat rootfs "rootfs" in
-
let void =
-
Void.empty
-
|> Void.rootfs ~mode:entry.mode rootfs
-
|> Void.cwd entry.cwd
-
(* TODO: Support UIDs |> Void.uid 1000 *)
-
|> Void.exec ~env
-
[
-
config.shell;
-
"-c";
-
String.concat " " command ^ " && env > /tmp/shelter-env";
-
]
-
in
-
`Void (Void.spawn ~sw void |> Void.exit_status)
-
else
-
let config =
-
Runc.Json_config.
-
{
-
cwd = entry.cwd;
-
argv =
-
[
-
config.shell;
-
"-c";
-
String.concat " " command ^ " && env > /tmp/shelter-env";
-
];
-
hostname = "";
-
network = [];
-
user = (uid, gid);
-
env = entry.env;
-
entrypoint = None;
-
}
-
in
-
`Runc (Runc.spawn ~sw fs proc config rootfs)
-
in
-
Switch.run @@ fun sw ->
-
let res = spawn sw in
-
let start = Mtime_clock.now () in
-
let res =
-
match res with
-
| `Runc r -> Eio.Process.await r
-
| `Void v -> Void.to_eio_status (Eio.Promise.await v)
-
in
-
let stop = Mtime_clock.now () in
-
let span = Mtime.span start stop in
-
let time = Mtime.Span.to_uint64_ns span in
-
(* Add command to history regardless of exit status *)
-
let _ : (unit, string) result =
-
LNoise.history_add (String.concat " " command)
-
in
-
if res = `Exited 0 then (
-
(* Extract env *)
-
let env_path =
-
Eio.Path.(fs / rootfs / "rootfs" / "tmp" / "shelter-env")
-
in
-
let env =
-
Eio.Path.(load env_path)
-
|> String.split_on_char '\n'
-
|> List.filter (fun s -> not (String.equal "" s))
-
in
-
Eio.Path.unlink env_path;
-
let cwd =
-
List.find_map
-
(fun v ->
-
match Astring.String.cut ~sep:"=" v with
-
| Some ("PWD", dir) -> Some dir
-
| _ -> None)
-
env
-
|> Option.value ~default:hash_entry.cwd
-
in
-
if entry.mode = RW then
-
Ok
-
{
-
hash_entry with
-
build = Build new_cid;
-
time;
-
env;
-
cwd;
-
user = (uid, gid);
-
}
-
else Ok { hash_entry with time; cwd; env; user = (uid, gid) })
-
else Error (Eio.Process.Child_error res)
-
in
-
match new_entry with
-
| Error e -> Error e
-
| Ok entry ->
-
(* Set diff *)
-
let entry = { entry with diff } in
-
(* Commit if RW *)
-
if entry.mode = RW then
-
commit
-
~message:("exec " ^ String.concat " " command)
-
clock s entry;
-
Ok (s, ctx)
-
with Eio.Exn.Io (Eio.Process.E e, _) -> Error e)
+
let new_entry, diff = exec config ~stdout fs proc (s, ctx) entry in
+
complete_exec (s, ctx) clock fs new_entry diff
+
with Eio.Exn.Io (Eio.Process.E e, _) -> Shelter.process_error e)
+6 -3
src/lib/shelter/shelter_main.mli
···
module Store = Store
module History : sig
-
type t = {
+
type post = { diff : Diff.t; time : int64 } [@@deriving repr]
+
+
type pre = {
mode : Void.mode;
build : Store.Build.t;
args : string list;
-
time : int64;
env : string list;
cwd : string;
user : int * int;
-
diff : Diff.t;
}
[@@deriving repr]
+
(** Needed for execution *)
+
+
type t = { pre : pre; post : post } [@@deriving repr]
include Irmin.Contents.S with type t := t
end
+33 -18
src/lib/shelter/store.ml
···
val builds : string -> dataset
val build : string -> string -> dataset
val snapshot : dataset -> snapshot
+
val tools : string -> dataset
+
val tool : string -> string -> dataset
end = struct
type dataset = string
type snapshot = string
···
let builds pool : dataset = pool / "builds"
let build pool path : dataset = builds pool / path
let snapshot ds = ds ^ "@snappy"
+
let tools pool : dataset = pool / "tools"
+
let tool pool path : dataset = tools pool / path
end
let with_dataset ?(typ = Zfs.Types.filesystem) t dataset f =
···
}
in
create_and_mount t (Datasets.builds t.pool);
+
create_and_mount t (Datasets.tools t.pool);
t
let snapshot t (snap : Datasets.snapshot) =
···
Zfs.clone src (tgt :> string)
let read_all fd =
-
let buf = Buffer.create 128 in
-
let bytes = Bytes.create 4096 in
+
let buf = Buffer.create 10_000 in
+
let bytes = Bytes.create 10_000 in
let rec loop () =
match Unix.read fd bytes 0 4096 with
| 0 | (exception End_of_file) -> Buffer.contents buf
···
in
loop ()
-
let diff t (data : Datasets.snapshot) (snap : Datasets.snapshot) =
+
let diff t (data : Datasets.snapshot) (snap : Datasets.snapshot) output =
let data_fs =
String.sub (data :> string) 0 (String.index (data :> string) '@')
in
let zh = Zfs.open_ t.zfs data_fs Zfs.Types.filesystem in
-
let diff =
-
let r, w = Unix.pipe ~cloexec:false () in
+
let () =
try
-
Zfs.show_diff zh ~from_:(data :> string) ~to_:(snap :> string) w;
-
let f = read_all r in
-
Unix.close r;
-
f
-
with e ->
-
Unix.close r;
-
raise e
+
Eio.Path.with_open_out ~create:(`If_missing 0o644) output
+
@@ fun flow_fd ->
+
let eio_fd = Eio_unix.Resource.fd_opt flow_fd in
+
Eio_unix.Fd.use_exn_opt "zfs-diff" eio_fd @@ function
+
| None -> Fmt.failwith "Output needs to have an FD"
+
| Some fd ->
+
Zfs.show_diff zh ~from_:(data :> string) ~to_:(snap :> string) fd
+
with Unix.Unix_error (Unix.EBADF, _, _) -> ()
in
Zfs.close zh;
+
let diff = Eio.Path.load output in
Diff.of_zfs diff
let cid s =
···
let ds = Datasets.build t.pool (Cid.to_string cid) in
Fun.protect ~finally:(fun () -> unmount_dataset t ds) @@ fun () ->
mount_dataset t ds;
+
fn (`Build ("/" ^ (ds :> string)))
+
+
let with_tool t cid fn =
+
let ds = Datasets.tool t.pool (Cid.to_string cid) in
+
Fun.protect ~finally:(fun () -> unmount_dataset t ds) @@ fun () ->
+
mount_dataset t ds;
fn ("/" ^ (ds :> string))
-
let with_clone t ~src new_cid fn =
+
let with_clone t ~src new_cid output fn =
let ds = Datasets.build t.pool (Cid.to_string src) in
let tgt = Datasets.build t.pool (Cid.to_string new_cid) in
let src_snap = Datasets.snapshot ds in
let tgt_snap = Datasets.snapshot tgt in
-
clone t src_snap tgt;
-
let v = with_build t new_cid fn in
-
snapshot t tgt_snap;
-
let d = diff t src_snap tgt_snap in
-
(v, d)
+
if Zfs.exists t.zfs (tgt :> string) Zfs.Types.dataset then
+
(fn (`Exists ("/" ^ (tgt :> string))), diff t src_snap tgt_snap output)
+
else (
+
clone t src_snap tgt;
+
let v = with_build t new_cid fn in
+
snapshot t tgt_snap;
+
let d = diff t src_snap tgt_snap output in
+
(v, d))
end
+1
src/lib/shelter/tools.ml
···
+
let opentrace = [%blob "./opentrace"]
+24 -14
src/lib/shelter.ml
···
module Engine = Engine
module Script = Script
+
let process_error e = Error (`Process e)
+
let shell_error e = Error (`Shell e)
+
module Make (H : History.S) (Engine : Engine.S with type entry = H.t) = struct
-
module Store = Irmin_fs_unix.KV.Make (H)
+
module Store = Irmin_git_unix.FS.KV (H)
-
let run config fs clock proc store =
+
let run config ~stdout fs clock proc store =
let store = History.Store ((module Store), store) in
let initial_ctx = Engine.init fs proc store in
let rec loop store ctx exit_code =
···
| None -> ()
| Some input -> (
let action = Engine.action_of_command input in
-
match Engine.run config fs clock proc (store, ctx) action with
-
| Error (Eio.Process.Child_error exit_code) ->
+
match Engine.run config ~stdout fs clock proc (store, ctx) action with
+
| Error (`Process (Eio.Process.Child_error exit_code)) ->
Fmt.epr "%a\n%!" Eio.Process.pp_status exit_code;
loop store ctx exit_code
-
| Error (Eio.Process.Executable_not_found m) ->
-
Fmt.epr "cshell: excutable not found %s\n%!" m;
+
| Error (`Process (Eio.Process.Executable_not_found m)) ->
+
Fmt.epr "shelter: excutable not found %s\n%!" m;
loop store ctx (`Exited 127)
+
| Error (`Shell e) ->
+
Fmt.epr "shelter: %a\n%!" Engine.pp_error e;
+
loop store ctx (`Exited 255)
| Ok (store, ctx) -> loop store ctx (`Exited 0))
in
loop store initial_ctx (`Exited 0)
···
Eio.Path.load cf |> String.split_on_char '\n'
|> List.map Engine.action_of_command
-
let main config fs clock proc directory command_file =
-
Irmin_fs.run directory @@ fun () ->
-
let conf = Irmin_fs.config (Eio.Path.native_exn directory) in
+
let main config ~stdout fs clock proc directory command_file =
+
let conf = Irmin_git.config (Eio.Path.native_exn directory) in
let repo = Store.Repo.v conf in
let store = Store.main repo in
match command_file with
···
let folder (store, ctx, exit_code) action =
if exit_code <> `Exited 0 then (store, ctx, exit_code)
else
-
match Engine.run config fs clock proc (store, ctx) action with
-
| Error (Eio.Process.Child_error exit_code) ->
+
match
+
Engine.run config ~stdout fs clock proc (store, ctx) action
+
with
+
| Error (`Process (Eio.Process.Child_error exit_code)) ->
Fmt.epr "%a\n%!" Eio.Process.pp_status exit_code;
(store, ctx, exit_code)
-
| Error (Eio.Process.Executable_not_found m) ->
-
Fmt.epr "cshell: excutable not found %s\n%!" m;
+
| Error (`Process (Eio.Process.Executable_not_found m)) ->
+
Fmt.epr "shelter: excutable not found %s\n%!" m;
(store, ctx, `Exited 127)
+
| Error (`Shell e) ->
+
Fmt.epr "shelter: %a\n%!" Engine.pp_error e;
+
(store, ctx, `Exited 255)
| Ok (store, ctx) -> (store, ctx, `Exited 0)
in
let _store, _ctx, exit_code =
···
| `Exited n | `Signaled n ->
Fmt.epr "%a\n%!" Eio.Process.pp_status exit_code;
exit n)
-
| None -> run config fs clock proc store
+
| None -> run config ~stdout fs clock proc store
end
+3
vendor/void/src/void_action.c
···
#include <string.h>
+
#define CAML_INTERNALS
#include <caml/mlvalues.h>
#include <caml/unixsupport.h>
#include <caml/memory.h>
#include <caml/custom.h>
+
#include <caml/signals.h>
#include <caml/fail.h>
+
#undef CAML_INTERNALS
// From Eio
#include <include/fork_action.h>
-1
vendor/zfs/src/zfs.ml
···
(* TODO: Other Diff Flags https://github.com/openzfs/zfs/blob/5b0c27cd14bbc07d50304c97735cc105d0258673/include/libzfs.h#L917? *)
let res = C.Functions.diff handle (Obj.magic fd : int) from_ to_ 1 in
if res = 0 then () else begin
-
Format.printf "Diff got %i\n%!" res;
invalid_arg "show_diff"
end