this repo has no description

Compare changes

Choose any two refs to compare.

+3
.gitignore
···
*.sjson
*.json
*.shl
+
+
docs/trees
+
docs/output
-2
shelter.opam
···
]
dev-repo: "git+https://github.com/username/reponame.git"
pin-depends:[
-
[ "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" ]
[ "ppx_irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
[ "irmin-git.dev" "git+https://github.com/mirage/irmin#eio" ]
-2
shelter.opam.template
···
pin-depends:[
-
[ "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" ]
[ "ppx_irmin.dev" "git+https://github.com/mirage/irmin#eio" ]
[ "irmin-git.dev" "git+https://github.com/mirage/irmin#eio" ]
-5
src/bin/dune
···
-
(executable
-
(public_name shelter)
-
(package shelter)
-
(name main)
-
(libraries shelter fmt.tty shelter.main shelter.passthrough eio void))
-83
src/bin/main.ml
···
-
module History = struct
-
type t = string
-
-
let t = Repr.string
-
let merge = Irmin.Merge.default (Repr.option t)
-
end
-
-
module Pass = Shelter.Make (History) (Shelter_passthrough)
-
module Main = Shelter.Make (Shelter_main.History) (Shelter_main)
-
-
let home = Unix.getenv "HOME"
-
-
let state_dir fs type' =
-
let path = Eio.Path.(fs / home / ".cache/shelter" / type') in
-
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 cmd_file =
-
let doc = "Path to a file containing a series of commands." in
-
Arg.(
-
value
-
& opt (some file) None
-
& info [ "f"; "file" ] ~docv:"COMMAND_FILE" ~doc)
-
-
let main =
-
let run config cmd_file =
-
Eventloop.run @@ fun env ->
-
let cmd_file = Option.map (Eio.Path.( / ) env#fs) cmd_file in
-
let dir = state_dir env#fs "shelter" in
-
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 =
-
[
-
`P
-
"Shelter is a shell session shim to help control uncertainty when \
-
working from the terminal";
-
]
-
in
-
let doc = "Shelter: version-controlled shell sessions" in
-
let info = Cmd.info ~man ~doc "main" in
-
(Cmd.v info t, t, info)
-
-
let passthrough =
-
let run config cmd_file =
-
Eventloop.run @@ fun env ->
-
let cmd_file = Option.map (Eio.Path.( / ) env#fs) cmd_file in
-
let dir = state_dir env#fs "passthrough" in
-
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
-
Cmd.v info t
-
-
let extract_commands =
-
let run cmd_file =
-
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
-
let t = Term.(const run $ cmd_file) in
-
let info = Cmd.info "extract" in
-
Cmd.v info t
-
-
let cmds =
-
let cmd, term, info = main in
-
let cmds = [ cmd; passthrough; extract_commands ] in
-
Cmd.group ~default:term info cmds
-
-
let () =
-
Fmt_tty.setup_std_outputs ();
-
exit (Cmd.eval cmds)
+44
src/common/admin.ml
···
+
open Capnp_rpc
+
+
let v sr ~add_user ~remove_user =
+
let module X = Raw.Service.Admin in
+
Capnp_rpc.Persistence.with_sturdy_ref sr X.local
+
@@ object
+
inherit X.service
+
+
method add_user_impl params release_param_caps =
+
let open X.AddUser in
+
let id = Params.user_get params in
+
release_param_caps ();
+
let cap = add_user id in
+
let response, results = Service.Response.create Results.init_pointer in
+
Results.cap_set results (Some cap);
+
Capability.dec_ref cap;
+
Service.return response
+
+
method remove_user_impl params release_param_caps =
+
let open X.RemoveUser in
+
let id = Params.user_get params in
+
release_param_caps ();
+
remove_user id;
+
Service.return @@ Service.Response.create_empty ()
+
end
+
+
module X = Raw.Client.Admin
+
+
type t = X.t Capability.t
+
+
let add_user t user =
+
let open X.AddUser in
+
let request, params = Capability.Request.create Params.init_pointer in
+
Params.user_set params user;
+
Capability.call_for_caps t method_id request Results.cap_get_pipelined
+
+
let remove_user t user =
+
let open X.RemoveUser in
+
let request, params = Capability.Request.create Params.init_pointer in
+
Params.user_set params user;
+
let _ : _ StructStorage.reader_t =
+
Capability.call_for_value_exn t method_id request
+
in
+
()
+1
src/common/config.ml
···
+
type t = { shell : string; default_image : string } [@@deriving yojson]
+13
src/common/dune
···
+
(rule
+
(targets schema.ml schema.mli)
+
(deps schema.capnp)
+
(action
+
(run capnpc -o %{bin:capnpc-ocaml} %{deps})))
+
+
(library
+
(name shelter_common)
+
(preprocess
+
(pps ppx_deriving_yojson))
+
(flags
+
(:standard -w -53-55))
+
(libraries capnp-rpc-net))
+1
src/common/raw.ml
···
+
include Schema.MakeRPC (Capnp_rpc)
+22
src/common/schema.capnp
···
+
@0x91b3108e7ebb3830;
+
interface Session {
+
stdin @0 (input :Text) -> ();
+
stdout @1 () -> (output :Text);
+
stderr @2 () -> (output :Text);
+
}
+
+
interface User {
+
connect @0 (config :Text) -> (cap :Session);
+
# Connect to the daemon and get a live session.
+
}
+
+
+
interface Admin {
+
addUser @0 (user :Text) -> (cap :User);
+
# Add a new user, returning a capability to act as a full
+
# Shelter user.
+
+
removeUser @1 (user :Text) -> ();
+
# Remove a user, this will also cancel existing connections
+
# this user may have to the daemon.
+
}
+57
src/common/session.ml
···
+
open Capnp_rpc
+
+
let or_fail = function
+
| Ok v -> v
+
| Error (`Capnp e) -> Fmt.failwith "%a" Capnp_rpc.Error.pp e
+
+
let local ~stdin ~stdout ~stderr =
+
let module X = Raw.Service.Session in
+
X.local
+
@@ object
+
inherit X.service
+
+
method stdout_impl _ release_param_caps =
+
let open X.Stdout in
+
release_param_caps ();
+
let s = stdout () in
+
let response, results = Service.Response.create Results.init_pointer in
+
Results.output_set results s;
+
Service.return response
+
+
method stderr_impl _ release_param_caps =
+
let open X.Stderr in
+
release_param_caps ();
+
let s = stderr () in
+
let response, results = Service.Response.create Results.init_pointer in
+
Results.output_set results s;
+
Service.return response
+
+
method stdin_impl params release_param_caps =
+
let open X.Stdin in
+
let data = Params.input_get params in
+
release_param_caps ();
+
stdin data;
+
Service.return_empty ()
+
end
+
+
module X = Raw.Client.Session
+
+
type t = X.t Capability.t
+
+
let stdout t () =
+
let open X.Stdout in
+
let request = Capability.Request.create_no_args () in
+
let result = Capability.call_for_value t method_id request |> or_fail in
+
Results.output_get result
+
+
let stderr t () =
+
let open X.Stderr in
+
let request = Capability.Request.create_no_args () in
+
let result = Capability.call_for_value t method_id request |> or_fail in
+
Results.output_get result
+
+
let stdin t input =
+
let open X.Stdin in
+
let request, params = Capability.Request.create Params.init_pointer in
+
Params.input_set params input;
+
Capability.call_for_unit t method_id request |> or_fail
+6
src/common/shelter_common.ml
···
+
let or_fail = function Ok v -> v | Error (`Msg m) -> failwith m
+
+
module Raw = Raw
+
module Admin = Admin
+
module User = User
+
module Session = Session
+31
src/common/user.ml
···
+
open Capnp_rpc
+
+
let v sr connect =
+
let module X = Raw.Service.User in
+
Capnp_rpc.Persistence.with_sturdy_ref sr X.local
+
@@ object
+
inherit X.service
+
+
method connect_impl params release_param_caps =
+
let open X.Connect in
+
let config =
+
Params.config_get params |> Yojson.Safe.from_string
+
|> Config.of_yojson |> Result.get_ok
+
in
+
release_param_caps ();
+
let cap = connect config in
+
let response, results = Service.Response.create Results.init_pointer in
+
Results.cap_set results (Some cap);
+
Capability.dec_ref cap;
+
Service.return response
+
end
+
+
module X = Raw.Client.User
+
+
type t = X.t Capability.t
+
+
let connect t config =
+
let open X.Connect in
+
let request, params = Capability.Request.create Params.init_pointer in
+
Params.config_set params (Config.to_yojson config |> Yojson.Safe.to_string);
+
Capability.call_for_caps t method_id request Results.cap_get_pipelined
-4
src/lib/dune
···
-
(library
-
(name shelter)
-
(public_name shelter)
-
(libraries cmdliner irmin-git.unix eio.unix eio linenoise void repr morbig))
-50
src/lib/engine.ml
···
-
module type S = sig
-
type config
-
(** A configuration *)
-
-
val config_term : config Cmdliner.Term.t
-
(** A cmdliner term for constructing a config *)
-
-
type action
-
(** An action to run *)
-
-
val action : action Repr.t
-
val action_of_command : string -> action
-
-
type entry
-
-
type ctx
-
(** 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 ->
-
entry History.t ->
-
ctx
-
(** [init store] will be called before entering the shell loop. You may wish
-
to setup history completions etc. with LNoise. *)
-
-
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,
-
[ `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 *)
-
-
val prompt : Eio.Process.exit_status -> entry History.t -> string
-
(** [prompt previous_exit_code history] generates a prompt from the current
-
[history] *)
-
end
-17
src/lib/history.ml
···
-
module type S = sig
-
type t
-
(** A single history entry *)
-
-
include Irmin.Contents.S with type t := t
-
end
-
-
type 'entry t =
-
| Store :
-
((module Irmin.S
-
with type t = 'a
-
and type Schema.Branch.t = string
-
and type Schema.Contents.t = 'entry
-
and type Schema.Path.t = string list
-
and type Schema.Path.step = string)
-
* 'a)
-
-> 'entry t
-6
src/lib/passthrough/dune
···
-
(library
-
(name shelter_passthrough)
-
(public_name shelter.passthrough)
-
(preprocess
-
(pps ppx_repr))
-
(libraries shelter))
-64
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 ()
-
-
type action = Exec of string [@@deriving repr]
-
-
let action = action_t
-
let action_of_command cmd = Exec cmd
-
-
type entry = string [@@derviving repr]
-
-
let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
-
-
let prompt _ _ =
-
Fmt.(styled (`Fg `Red) string) Format.str_formatter "shelter-p> ";
-
Format.flush_str_formatter ()
-
-
let history_key = [ "history" ]
-
let key () = history_key @ [ string_of_float @@ Unix.gettimeofday () ]
-
-
type ctx = unit
-
-
let init _ _
-
(Shelter.History.Store ((module S), store) : entry Shelter.History.t) =
-
match S.list store history_key with
-
| [] -> ()
-
| xs ->
-
let rec loop acc = function
-
| `Contents (v, _meta) :: next -> loop (v :: acc) next
-
| _ :: next -> loop acc next
-
| [] -> List.rev acc
-
in
-
let entries =
-
loop [] (List.map (fun (_, tree) -> S.Tree.to_concrete tree) xs)
-
in
-
List.iter (fun v -> LNoise.history_add v |> ignore) entries
-
-
let run (() : config) ~stdout:_ _fs clock proc
-
( ((Shelter.History.Store ((module S), store) : entry Shelter.History.t) as
-
full_store),
-
() ) (Exec command) =
-
let info () =
-
S.Info.v ~message:"shelter" (Eio.Time.now clock |> Int64.of_float)
-
in
-
let cmd =
-
String.split_on_char ' ' command
-
|> List.filter (fun v -> not (String.equal "" v))
-
in
-
Switch.run @@ fun sw ->
-
try
-
let proc = Eio.Process.spawn ~sw proc cmd in
-
let res = Eio.Process.await proc in
-
if res = `Exited 0 then (
-
S.set_exn ~info store (key ()) command;
-
let _ : (unit, string) result = LNoise.history_add command in
-
Ok (full_store, ()))
-
else Shelter.process_error (Eio.Process.Child_error res)
-
with Eio.Exn.Io (Eio.Process.E e, _) -> Shelter.process_error e
-1
src/lib/passthrough/shelter_passthrough.mli
···
-
include Shelter.Engine.S with type entry = string
-57
src/lib/script.ml
···
-
module Cst = Morbig.CST
-
-
let redirect_to_string = function
-
| Cst.IoRedirect_IoFile { value = io_file; _ } -> (
-
match io_file with
-
| Cst.IoFile_Great_FileName
-
{ value = Cst.Filename_Word { value = Cst.Word (w, _); _ }; _ } ->
-
Fmt.str "> %s" w
-
| Cst.IoFile_DGreat_FileName
-
{ value = Cst.Filename_Word { value = Cst.Word (w, _); _ }; _ } ->
-
Fmt.str ">> %s" w
-
| _ -> failwith "Redirect Unsupported")
-
| _ -> failwith "IO Redirect Unsupported"
-
-
let cmd_suffix_to_list s =
-
let rec loop = function
-
| Cst.CmdSuffix_Word { value = Cst.Word (s, _); _ } -> [ s ]
-
| Cst.CmdSuffix_CmdSuffix_Word (suff, { value = Cst.Word (s, _); _ }) ->
-
s :: loop suff.value
-
| Cst.CmdSuffix_CmdSuffix_IoRedirect (suff, { value = redirect; _ }) ->
-
let sf = loop suff.value in
-
redirect_to_string redirect :: sf
-
| _ -> failwith "Unsupported!"
-
in
-
loop s |> List.rev |> String.concat " "
-
-
let of_cmd (c : Cst.command) =
-
match c with
-
| Cst.Command_SimpleCommand simple -> (
-
match simple.value with
-
| Cst.SimpleCommand_CmdName
-
{ value = Cst.CmdName_Word { value = Cst.Word (w, _); _ }; _ } ->
-
w
-
| Cst.SimpleCommand_CmdName_CmdSuffix
-
( { value = Cst.CmdName_Word { value = Cst.Word (w, _); _ }; _ },
-
{ value = suff; _ } ) ->
-
let s = cmd_suffix_to_list suff in
-
w ^ " " ^ s
-
| _ -> failwith "Unsupported")
-
| _ -> failwith "Unsupported"
-
-
let cmds_to_strings =
-
let v =
-
object
-
inherit [_] Morbig.CSTVisitors.reduce
-
method zero = []
-
method plus = List.append
-
method! visit_command acc c = of_cmd c :: acc
-
end
-
in
-
v#visit_program []
-
-
let to_commands file =
-
let contents = Eio.Path.load file in
-
let name = Eio.Path.native_exn file |> Filename.basename in
-
let ast = Morbig.parse_string name contents in
-
cmds_to_strings ast
-34
src/lib/shelter/config.ml
···
-
type t = {
-
no_diffing : bool;
-
no_ebpf : bool;
-
no_runc : bool;
-
image : string;
-
shell : string;
-
}
-
-
let cmdliner =
-
let open Cmdliner in
-
let no_diffing =
-
let doc = "Disable diffing." in
-
Arg.(value & flag & info [ "no-diffing" ] ~doc)
-
in
-
let no_ebpf =
-
let doc = "Disable eBPF." in
-
Arg.(value & flag & info [ "no-ebpf" ] ~doc)
-
in
-
let image =
-
let doc = "Base image to start Shelter with." in
-
Arg.(value & opt string "alpine" & info [ "image" ] ~doc)
-
in
-
let shell =
-
let doc = "Path to the shell (e.g. /bin/ash)" in
-
Arg.(value & opt string "/bin/ash" & info [ "shell" ] ~doc)
-
in
-
let no_runc =
-
let doc = "Disable RUNC and use Void processes." in
-
Arg.(value & flag & info [ "no-runc" ] ~doc)
-
in
-
Term.(
-
const (fun no_diffing no_ebpf no_runc image shell ->
-
{ no_diffing; no_ebpf; no_runc; image; shell })
-
$ no_diffing $ no_ebpf $ no_runc $ image $ shell)
-105
src/lib/shelter/diff.ml
···
-
type diff =
-
| Modified of string
-
| Created of string
-
| Renamed of string * string
-
| Removed of string
-
[@@deriving repr]
-
-
let path = function
-
| Modified p -> p
-
| Created p -> p
-
| Renamed (p, _) -> p
-
| Removed p -> p
-
-
type t = diff list [@@deriving repr]
-
-
let truncate_path s =
-
match Astring.String.cut ~sep:"rootfs" s with Some (_, p) -> p | None -> s
-
-
let parse_row = function
-
| [ "M"; s ] ->
-
let path = truncate_path s in
-
if String.equal path "" then None else Some (Modified path)
-
| [ "+"; s ] ->
-
let path = truncate_path s in
-
if String.equal path "" then None else Some (Created path)
-
| [ "R"; a; b ] ->
-
let a_path = truncate_path a in
-
let b_path = truncate_path b in
-
Some (Renamed (a_path, b_path))
-
| [ "-"; s ] ->
-
let path = truncate_path s in
-
if String.equal path "" then None else Some (Removed path)
-
| s ->
-
Fmt.invalid_arg "Unknown ZFS diff: %a"
-
(Fmt.list ~sep:Fmt.comma Fmt.string)
-
s
-
-
let of_zfs s : t =
-
let lines = String.split_on_char '\n' s in
-
let tsv =
-
List.map (String.split_on_char '\t') lines
-
|> List.map (List.filter (fun s -> not (String.equal "" s)))
-
|> List.filter (function [] -> false | _ -> true)
-
in
-
List.filter_map parse_row tsv
-
-
type tree = Leaf of diff | Dir of string * tree list
-
-
let rec insert modified path_components tree =
-
match (path_components, tree) with
-
| [], _ -> tree
-
| [ file ], nodes ->
-
if List.exists (function Leaf f -> path f = file | _ -> false) nodes
-
then nodes
-
else
-
let diff =
-
match modified with
-
| Modified _ -> Modified file
-
| Created _ -> Created file
-
| Renamed (_, to_) -> Renamed (file, to_)
-
| Removed _ -> Removed file
-
in
-
Leaf diff :: nodes
-
| dir :: rest, nodes ->
-
let rec insert_into_dir acc = function
-
| [] -> Dir (dir, insert modified rest []) :: List.rev acc
-
| Dir (name, children) :: tl when name = dir ->
-
List.rev_append acc (Dir (name, insert modified rest children) :: tl)
-
| x :: tl -> insert_into_dir (x :: acc) tl
-
in
-
insert_into_dir [] nodes
-
-
let to_tree (diffs : diff list) =
-
let paths =
-
List.map (fun v -> (v, String.split_on_char '/' (path v))) diffs
-
in
-
List.fold_left (fun acc (m, p) -> insert m p acc) [] paths
-
-
let leaves =
-
let rec loop acc acc2 = function
-
| Leaf (Modified v) -> Modified (Filename.concat acc v) :: acc2
-
| Leaf (Created v) -> Created (Filename.concat acc v) :: acc2
-
| Leaf (Removed v) -> Removed (Filename.concat acc v) :: acc2
-
| Leaf (Renamed (r1, r2)) -> Renamed (Filename.concat acc r1, r2) :: acc2
-
| Dir (p, cs) ->
-
List.fold_left (fun lvs v -> loop (Filename.concat acc p) lvs v) acc2 cs
-
in
-
loop "" []
-
-
let pp_diff fmt = function
-
| Modified v -> Fmt.(styled (`Fg `Yellow) string) fmt ("~ /" ^ v)
-
| Created v -> Fmt.(styled (`Fg `Green) string) fmt ("+ /" ^ v)
-
| Removed v -> Fmt.(styled (`Fg `Red) string) fmt ("- /" ^ v)
-
| Renamed (v, _) -> Fmt.(styled (`Fg `Magenta) string) fmt ("| /" ^ v)
-
-
let pp fmt diffs =
-
let tree = to_tree diffs in
-
let lvs =
-
List.fold_left (fun acc v -> leaves v @ acc) [] tree
-
|> List.filter (fun v ->
-
not
-
(String.starts_with ~prefix:"shelter" (path v)
-
|| String.starts_with ~prefix:"tmp" (path v)))
-
in
-
Fmt.pf fmt "%a" Fmt.(list ~sep:Format.pp_force_newline pp_diff) lvs
-13
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 ppx_blob))
-
(libraries shelter cid void zfs))
-53
src/lib/shelter/fetch.ml
···
-
let ( / ) = Eio.Path.( / )
-
let replace_slash s = String.split_on_char '/' s |> String.concat "-"
-
-
let get_user proc image =
-
Eio.Process.parse_out proc Eio.Buf_read.take_all
-
[
-
"docker";
-
"image";
-
"inspect";
-
"--format";
-
{|{{.Config.User}}|};
-
"--";
-
image;
-
]
-
|> String.trim
-
-
let get_env proc image =
-
Eio.Process.parse_out proc Eio.Buf_read.take_all
-
[
-
"docker";
-
"image";
-
"inspect";
-
"--format";
-
{|{{range .Config.Env}}{{print . "\x00"}}{{end}}|};
-
"--";
-
image;
-
]
-
|> String.split_on_char '\x00'
-
-
let get_image ~dir ~proc image =
-
let container_id =
-
Eio.Process.parse_out proc Eio.Buf_read.take_all
-
[ "docker"; "create"; "--"; image ]
-
|> String.trim
-
in
-
let tar = replace_slash image ^ ".tar.gz" in
-
let dir_s = Eio.Path.native_exn dir in
-
let () =
-
Eio.Process.run proc
-
[ "docker"; "export"; container_id; "-o"; Filename.concat dir_s tar ]
-
in
-
Eio.Path.mkdir ~perm:0o777 (dir / "rootfs");
-
let () =
-
Eio.Process.run proc
-
[
-
"tar";
-
"-xf";
-
Filename.concat dir_s tar;
-
"-C";
-
Filename.concat dir_s "rootfs";
-
]
-
in
-
Filename.concat dir_s "rootfs"
src/lib/shelter/opentrace

This is a binary file and will not be displayed.

-509
src/lib/shelter/runc.ml
···
-
(* From Obuilder see License file at end of document *)
-
let ( / ) = Eio.Path.( / )
-
let ( // ) = Filename.concat
-
-
type config = { fast_sync : bool }
-
-
let get_machine () =
-
let ch = Unix.open_process_in "uname -m" in
-
let arch = input_line ch in
-
match Unix.close_process_in ch with
-
| Unix.WEXITED 0 -> String.trim arch
-
| _ -> failwith "Failed to get arch with 'uname -m'"
-
-
let get_arches () =
-
if Sys.unix then
-
match get_machine () with
-
| "x86_64" -> [ "SCMP_ARCH_X86_64"; "SCMP_ARCH_X86"; "SCMP_ARCH_X32" ]
-
| "aarch64" -> [ "SCMP_ARCH_AARCH64"; "SCMP_ARCH_ARM" ]
-
| _ -> []
-
else []
-
-
let secret_file id = "secret-" ^ string_of_int id
-
-
module Json_config = struct
-
let mount ?(options = []) ~ty ~src dst =
-
`Assoc
-
[
-
("destination", `String dst);
-
("type", `String ty);
-
("source", `String src);
-
("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) ]
-
-
(* This is a subset of the capabilities that Docker uses by default.
-
These control what root can do in the container.
-
If the init process is non-root, permitted, effective and ambient sets are cleared.
-
See capabilities(7) for full details. *)
-
let default_linux_caps =
-
[
-
"CAP_CHOWN";
-
(* Make arbitrary changes to file UIDs and GIDs *)
-
"CAP_DAC_OVERRIDE";
-
(* Bypass file read, write, and execute permission checks. *)
-
"CAP_FSETID";
-
(* Set SUID/SGID bits. *)
-
"CAP_FOWNER";
-
(* Bypass permission checks. *)
-
"CAP_MKNOD";
-
(* Create special files using mknod. *)
-
"CAP_SETGID";
-
(* Make arbitrary manipulations of process GIDs. *)
-
"CAP_SETUID";
-
(* Make arbitrary manipulations of process UIDs. *)
-
"CAP_SETFCAP";
-
(* Set arbitrary capabilities on a file. *)
-
"CAP_SETPCAP";
-
(* Add any capability from bounding set to inheritable set. *)
-
"CAP_SYS_CHROOT";
-
(* Use chroot. *)
-
"CAP_KILL";
-
(* Bypass permission checks for sending signals. *)
-
"CAP_AUDIT_WRITE";
-
(* Write records to kernel auditing log. *)
-
"CAP_BPF";
-
"CAP_PERFMON";
-
(* BPF operations *)
-
(* Allowed by Docker, but disabled here (because we use host networking):
-
"CAP_NET_RAW"; (* Use RAW and PACKET sockets / bind to any address *)
-
"CAP_NET_BIND_SERVICE"; (* Bind a socket to Internet domain privileged ports. *)
-
*)
-
]
-
-
let seccomp_syscalls ~fast_sync =
-
if fast_sync then
-
[
-
`Assoc
-
[
-
(* Sync calls are pointless for the builder, because if the computer crashes then we'll
-
just throw the build dir away and start again. And btrfs sync is really slow.
-
Based on https://bblank.thinkmo.de/using-seccomp-to-filter-sync-operations.html
-
Note: requires runc >= v1.0.0-rc92. *)
-
( "names",
-
strings
-
[
-
"fsync";
-
"fdatasync";
-
"msync";
-
"sync";
-
"syncfs";
-
"sync_file_range";
-
] );
-
("action", `String "SCMP_ACT_ERRNO");
-
("errnoRet", `Int 0);
-
(* Return error "success" *)
-
];
-
]
-
else []
-
-
let seccomp_policy =
-
let fields =
-
[
-
("defaultAction", `String "SCMP_ACT_ALLOW");
-
("syscalls", `List (seccomp_syscalls ~fast_sync:true));
-
]
-
in
-
`Assoc fields
-
-
type config = {
-
cwd : string;
-
argv : string list;
-
hostname : string;
-
network : string list;
-
user : int * int;
-
env : string list;
-
mounts : mount list;
-
entrypoint : string option;
-
}
-
-
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 [ ("uid", `Int uid); ("gid", `Int gid) ]
-
in
-
let network_ns =
-
match network with
-
| [ "host" ] -> []
-
| [] -> [ "network" ]
-
| xs ->
-
Fmt.failwith "Unsupported network configuration %a"
-
Fmt.Dump.(list string)
-
xs
-
in
-
let namespaces = network_ns @ [ "pid"; "ipc"; "uts"; "mount" ] in
-
`Assoc
-
[
-
("ociVersion", `String "1.0.1-dev");
-
( "process",
-
`Assoc
-
[
-
("terminal", `Bool true);
-
("user", user);
-
("args", strings argv);
-
("env", strings env);
-
("cwd", `String cwd);
-
( "capabilities",
-
`Assoc
-
[
-
("bounding", strings default_linux_caps);
-
(* Limits capabilities gained on execve. *)
-
("effective", strings default_linux_caps);
-
(* Checked by kernel to decide access *)
-
("inheritable", strings default_linux_caps);
-
(* Preserved across an execve (if root, or cap in ambient set) *)
-
("permitted", strings default_linux_caps);
-
(* Limiting superset for the effective capabilities *)
-
] );
-
( "rlimits",
-
`List
-
[
-
`Assoc
-
[
-
("type", `String "RLIMIT_NOFILE");
-
("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);
-
] );
-
( "root",
-
`Assoc
-
[
-
("path", `String (results_dir // "rootfs"));
-
("readonly", `Bool false);
-
] );
-
("hostname", `String hostname);
-
( "mounts",
-
`List
-
(mount "/proc"
-
~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 "/sys/kernel/debug" ~ty:"debugfs" ~src:"debug"
-
~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
-
[
-
("namespaces", `List (List.map namespace namespaces));
-
( "maskedPaths",
-
strings
-
[
-
"/proc/acpi";
-
"/proc/asound";
-
"/proc/kcore";
-
"/proc/keys";
-
"/proc/latency_stats";
-
"/proc/timer_list";
-
"/proc/timer_stats";
-
"/proc/sched_debug";
-
"/sys/firmware";
-
"/proc/scsi";
-
] );
-
( "readonlyPaths",
-
strings
-
[
-
"/proc/bus";
-
"/proc/fs";
-
"/proc/irq";
-
"/proc/sys";
-
"/proc/sysrq-trigger";
-
] );
-
("seccomp", seccomp_policy);
-
] );
-
]
-
end
-
-
let next_id = ref 0
-
-
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 buf = Cstruct.create 4096 in
-
let copy () ~src =
-
try
-
while true do
-
match Eio.Flow.single_read src buf with
-
| i ->
-
let bufs = [ Cstruct.sub buf 0 i ] in
-
Eio.Fiber.both
-
(fun () -> Eio.Flow.write other bufs)
-
(fun () -> Sink.copy ~src:(Eio.Flow.cstruct_source bufs) t)
-
done
-
with End_of_file -> ()
-
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.(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");
-
Eio.Path.save ~create:(`If_missing 0o644) (eio_tmp / "hosts")
-
"127.0.0.1 localhost builder";
-
let id = string_of_int !next_id in
-
incr next_id;
-
let cmd = [ "runc"; "run"; id ] in
-
let stdout =
-
to_other_sink_as_well ~other:env#stdout
-
(log :> Eio.Flow.sink_ty Eio.Flow.sink)
-
in
-
Eio.Process.spawn ~sw ~stdout ~stderr:env#stdout env#proc ~cwd:eio_tmp cmd
-
-
(*
-
Apache License
-
Version 2.0, January 2004
-
https://www.apache.org/licenses/
-
-
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-
1. Definitions.
-
-
"License" shall mean the terms and conditions for use, reproduction,
-
and distribution as defined by Sections 1 through 9 of this document.
-
-
"Licensor" shall mean the copyright owner or entity authorized by
-
the copyright owner that is granting the License.
-
-
"Legal Entity" shall mean the union of the acting entity and all
-
other entities that control, are controlled by, or are under common
-
control with that entity. For the purposes of this definition,
-
"control" means (i) the power, direct or indirect, to cause the
-
direction or management of such entity, whether by contract or
-
otherwise, or (ii) ownership of fifty percent (50%) or more of the
-
outstanding shares, or (iii) beneficial ownership of such entity.
-
-
"You" (or "Your") shall mean an individual or Legal Entity
-
exercising permissions granted by this License.
-
-
"Source" form shall mean the preferred form for making modifications,
-
including but not limited to software source code, documentation
-
source, and configuration files.
-
-
"Object" form shall mean any form resulting from mechanical
-
transformation or translation of a Source form, including but
-
not limited to compiled object code, generated documentation,
-
and conversions to other media types.
-
-
"Work" shall mean the work of authorship, whether in Source or
-
Object form, made available under the License, as indicated by a
-
copyright notice that is included in or attached to the work
-
(an example is provided in the Appendix below).
-
-
"Derivative Works" shall mean any work, whether in Source or Object
-
form, that is based on (or derived from) the Work and for which the
-
editorial revisions, annotations, elaborations, or other modifications
-
represent, as a whole, an original work of authorship. For the purposes
-
of this License, Derivative Works shall not include works that remain
-
separable from, or merely link (or bind by name) to the interfaces of,
-
the Work and Derivative Works thereof.
-
-
"Contribution" shall mean any work of authorship, including
-
the original version of the Work and any modifications or additions
-
to that Work or Derivative Works thereof, that is intentionally
-
submitted to Licensor for inclusion in the Work by the copyright owner
-
or by an individual or Legal Entity authorized to submit on behalf of
-
the copyright owner. For the purposes of this definition, "submitted"
-
means any form of electronic, verbal, or written communication sent
-
to the Licensor or its representatives, including but not limited to
-
communication on electronic mailing lists, source code control systems,
-
and issue tracking systems that are managed by, or on behalf of, the
-
Licensor for the purpose of discussing and improving the Work, but
-
excluding communication that is conspicuously marked or otherwise
-
designated in writing by the copyright owner as "Not a Contribution."
-
-
"Contributor" shall mean Licensor and any individual or Legal Entity
-
on behalf of whom a Contribution has been received by Licensor and
-
subsequently incorporated within the Work.
-
-
2. Grant of Copyright License. Subject to the terms and conditions of
-
this License, each Contributor hereby grants to You a perpetual,
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-
copyright license to reproduce, prepare Derivative Works of,
-
publicly display, publicly perform, sublicense, and distribute the
-
Work and such Derivative Works in Source or Object form.
-
-
3. Grant of Patent License. Subject to the terms and conditions of
-
this License, each Contributor hereby grants to You a perpetual,
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-
(except as stated in this section) patent license to make, have made,
-
use, offer to sell, sell, import, and otherwise transfer the Work,
-
where such license applies only to those patent claims licensable
-
by such Contributor that are necessarily infringed by their
-
Contribution(s) alone or by combination of their Contribution(s)
-
with the Work to which such Contribution(s) was submitted. If You
-
institute patent litigation against any entity (including a
-
cross-claim or counterclaim in a lawsuit) alleging that the Work
-
or a Contribution incorporated within the Work constitutes direct
-
or contributory patent infringement, then any patent licenses
-
granted to You under this License for that Work shall terminate
-
as of the date such litigation is filed.
-
-
4. Redistribution. You may reproduce and distribute copies of the
-
Work or Derivative Works thereof in any medium, with or without
-
modifications, and in Source or Object form, provided that You
-
meet the following conditions:
-
-
(a) You must give any other recipients of the Work or
-
Derivative Works a copy of this License; and
-
-
(b) You must cause any modified files to carry prominent notices
-
stating that You changed the files; and
-
-
(c) You must retain, in the Source form of any Derivative Works
-
that You distribute, all copyright, patent, trademark, and
-
attribution notices from the Source form of the Work,
-
excluding those notices that do not pertain to any part of
-
the Derivative Works; and
-
-
(d) If the Work includes a "NOTICE" text file as part of its
-
distribution, then any Derivative Works that You distribute must
-
include a readable copy of the attribution notices contained
-
within such NOTICE file, excluding those notices that do not
-
pertain to any part of the Derivative Works, in at least one
-
of the following places: within a NOTICE text file distributed
-
as part of the Derivative Works; within the Source form or
-
documentation, if provided along with the Derivative Works; or,
-
within a display generated by the Derivative Works, if and
-
wherever such third-party notices normally appear. The contents
-
of the NOTICE file are for informational purposes only and
-
do not modify the License. You may add Your own attribution
-
notices within Derivative Works that You distribute, alongside
-
or as an addendum to the NOTICE text from the Work, provided
-
that such additional attribution notices cannot be construed
-
as modifying the License.
-
-
You may add Your own copyright statement to Your modifications and
-
may provide additional or different license terms and conditions
-
for use, reproduction, or distribution of Your modifications, or
-
for any such Derivative Works as a whole, provided Your use,
-
reproduction, and distribution of the Work otherwise complies with
-
the conditions stated in this License.
-
-
5. Submission of Contributions. Unless You explicitly state otherwise,
-
any Contribution intentionally submitted for inclusion in the Work
-
by You to the Licensor shall be under the terms and conditions of
-
this License, without any additional terms or conditions.
-
Notwithstanding the above, nothing herein shall supersede or modify
-
the terms of any separate license agreement you may have executed
-
with Licensor regarding such Contributions.
-
-
6. Trademarks. This License does not grant permission to use the trade
-
names, trademarks, service marks, or product names of the Licensor,
-
except as required for reasonable and customary use in describing the
-
origin of the Work and reproducing the content of the NOTICE file.
-
-
7. Disclaimer of Warranty. Unless required by applicable law or
-
agreed to in writing, Licensor provides the Work (and each
-
Contributor provides its Contributions) on an "AS IS" BASIS,
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-
implied, including, without limitation, any warranties or conditions
-
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-
PARTICULAR PURPOSE. You are solely responsible for determining the
-
appropriateness of using or redistributing the Work and assume any
-
risks associated with Your exercise of permissions under this License.
-
-
8. Limitation of Liability. In no event and under no legal theory,
-
whether in tort (including negligence), contract, or otherwise,
-
unless required by applicable law (such as deliberate and grossly
-
negligent acts) or agreed to in writing, shall any Contributor be
-
liable to You for damages, including any direct, indirect, special,
-
incidental, or consequential damages of any character arising as a
-
result of this License or out of the use or inability to use the
-
Work (including but not limited to damages for loss of goodwill,
-
work stoppage, computer failure or malfunction, or any and all
-
other commercial damages or losses), even if such Contributor
-
has been advised of the possibility of such damages.
-
-
9. Accepting Warranty or Additional Liability. While redistributing
-
the Work or Derivative Works thereof, You may choose to offer,
-
and charge a fee for, acceptance of support, warranty, indemnity,
-
or other liability obligations and/or rights consistent with this
-
License. However, in accepting such obligations, You may act only
-
on Your own behalf and on Your sole responsibility, not on behalf
-
of any other Contributor, and only if You agree to indemnify,
-
defend, and hold each Contributor harmless for any liability
-
incurred by, or claims asserted against, such Contributor by reason
-
of your accepting any such warranty or additional liability.
-
-
END OF TERMS AND CONDITIONS
-
-
Copyright 2020 Thomas Leonard
-
-
Licensed under the Apache License, Version 2.0 (the "License");
-
you may not use this file except in compliance with the License.
-
You may obtain a copy of the License at
-
-
https://www.apache.org/licenses/LICENSE-2.0
-
-
Unless required by applicable law or agreed to in writing, software
-
distributed under the License is distributed on an "AS IS" BASIS,
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-
See the License for the specific language governing permissions and
-
limitations under the License. *)
-592
src/lib/shelter/shelter_main.ml
···
-
open Eio
-
module Store = Store
-
module H = Shelter.History
-
-
type error = string
-
-
let pp_error = Fmt.string
-
-
module History = struct
-
type mode = Void.mode
-
-
let mode_t =
-
Repr.map Repr.string
-
(function
-
| "R" -> Void.R | "RW" -> Void.RW | _ -> failwith "Malformed Void.mode")
-
(function Void.R -> "R" | Void.RW -> "RW")
-
-
type post = { diff : Diff.t; time : int64 } [@@deriving repr]
-
-
type pre = {
-
mode : mode;
-
build : Store.Build.t;
-
args : string list;
-
env : string list;
-
cwd : string;
-
user : int * int;
-
}
-
[@@deriving repr]
-
(** Needed for execution *)
-
-
type t = { pre : pre; post : post } [@@deriving repr]
-
-
let merge = Irmin.Merge.(default (Repr.option t))
-
end
-
-
type config = Config.t
-
-
let config_term = Config.cmdliner
-
-
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
-
(* Undo the last command *)
-
| Undo
-
(* Replay the current branch onto another *)
-
| Replay of string
-
(* Display info *)
-
| Info of [ `Current | `History ]
-
(* Error state *)
-
| Unknown of string list
-
[@@deriving repr]
-
-
let split_and_remove_empty s =
-
String.split_on_char ' ' s |> List.filter (fun v -> not (String.equal "" v))
-
-
let action = action_t
-
-
let shelter_action = function
-
| "mode" :: [ "r" ] -> Set_mode R
-
| "mode" :: [ "rw" ] -> Set_mode RW
-
| "session" :: [ m ] -> Set_session m
-
| "replay" :: [ onto ] -> Replay onto
-
| [ "info" ] -> Info `Current
-
| [ "undo" ] -> Undo
-
| [ "history" ] -> Info `History
-
| other -> Unknown other
-
-
let action_of_command cmd =
-
match split_and_remove_empty cmd with
-
| "@" :: rest -> shelter_action rest
-
| args -> Exec args
-
-
let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
-
let history_key = [ "history" ]
-
let key clock = history_key @ [ string_of_float @@ Eio.Time.now clock ]
-
-
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
-
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 history s with [] -> default () | (_, hd) :: _ -> f hd
-
-
let text c = Fmt.(styled (`Fg c) string)
-
-
let sessions (H.Store ((module S), store) : entry H.t) =
-
S.Branch.list (S.repo store)
-
-
let commit ~message clock (H.Store ((module S), store) : entry H.t) v =
-
let info () = S.Info.v ~message (Eio.Time.now clock |> Int64.of_float) in
-
S.set_exn ~info store (key clock) v
-
-
let which_branch ((H.Store ((module S), session) : entry H.t) as s) =
-
let branches = sessions s in
-
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
-
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) =
-
match
-
List.filter_map (S.Commit.of_hash (S.repo session))
-
@@ S.Commit.parents (S.Head.get session)
-
with
-
| [] -> s
-
| p :: _ ->
-
S.Head.set session p;
-
s
-
-
(* Fork a new session from an existing one *)
-
let fork (H.Store ((module S), session) : entry H.t) new_branch =
-
let repo = S.repo session in
-
match (S.Head.find session, S.Branch.find repo new_branch) with
-
| _, Some _ ->
-
Error (new_branch ^ " already exists, try @ session " ^ new_branch)
-
| None, _ -> Error "Current branch needs at least one commit"
-
| Some commit, None ->
-
let new_store = S.of_branch (S.repo session) new_branch in
-
S.Branch.set repo new_branch commit;
-
let store = H.Store ((module S), new_store) in
-
Ok store
-
-
(* Fork a new session from an existing one *)
-
let display_history (s : entry H.t) =
-
let pp_diff fmt d = if d = [] then () else Fmt.pf fmt "\n%a%!" Diff.pp d in
-
let pp_entry fmt (e : entry) =
-
Fmt.pf fmt "%-10s %s%a"
-
Fmt.(str "%a" (styled (`Fg `Yellow) uint64_ns_span) e.post.time)
-
(String.concat " " e.pre.args)
-
pp_diff e.post.diff
-
in
-
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 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_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) =
-
Fmt.pf Format.str_formatter "%a%a%a : { mode: %a }> " pp_status status
-
(text `Yellow) "shelter" pp_sesh sesh (text `Red)
-
(if e.pre.mode = R then "r" else "rw");
-
Format.flush_str_formatter ()
-
in
-
with_latest store ~default:prompt prompt_entry
-
-
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.pre = { History.args; _ }; _ }) ->
-
LNoise.history_add (String.concat " " args) |> ignore)
-
(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 savedTio = Unix.tcgetattr Unix.stdin in
-
let tio =
-
{
-
savedTio with
-
(* input modes *)
-
c_ignpar = true;
-
c_istrip = false;
-
c_inlcr = false;
-
c_igncr = false;
-
c_ixon = false;
-
(* c_ixany = false; *)
-
(* c_iuclc = false; *)
-
c_ixoff = false;
-
(* output modes *)
-
c_opost = false;
-
(* control modes *)
-
c_isig = false;
-
c_icanon = false;
-
c_echo = false;
-
c_echoe = false;
-
c_echok = false;
-
c_echonl = false;
-
(* c_iexten = false; *)
-
-
(* special characters *)
-
c_vmin = 1;
-
c_vtime = 0;
-
}
-
in
-
Unix.tcsetattr Unix.stdin TCSADRAIN tio;
-
let start, res =
-
Switch.run @@ fun sw ->
-
let log =
-
Eio.Path.open_out ~sw ~create:(`Or_truncate 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
-
-
(* restore tio *)
-
Unix.tcsetattr Unix.stdin TCSADRAIN savedTio;
-
-
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) ~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 pre = { entry.pre with mode } };
-
Ok (s, 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" (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" (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 =
-
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
-
in
-
let latest =
-
with_latest
-
~default:(fun () -> None)
-
s
-
(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)
-
sessions (text `Green) sesh
-
Fmt.(option string)
-
latest
-
Fmt.(vbox ~indent:2 @@ list pp_commit)
-
commits;
-
Ok (s, ctx)
-
| Exec [] -> Ok (s, ctx)
-
| Undo -> Ok (reset_hard s, ctx)
-
| Replay branch -> replay config s ctx fs clock proc stdout branch
-
| Info `History ->
-
display_history s;
-
Ok (s, ctx)
-
| Exec command -> (
-
let entry =
-
with_latest
-
~default:(fun () ->
-
History.
-
{
-
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.id
-
in
-
let entry = { entry with pre = { entry.pre with args = command } } in
-
try
-
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)
-41
src/lib/shelter/shelter_main.mli
···
-
module Store = Store
-
-
module History : sig
-
type mode = Void.mode
-
type post = { diff : Diff.t; time : int64 } [@@deriving repr]
-
-
type pre = {
-
mode : Void.mode;
-
build : Store.Build.t;
-
args : string list;
-
env : string list;
-
cwd : string;
-
user : int * int;
-
}
-
[@@deriving repr]
-
(** Needed for execution *)
-
-
type t = { pre : pre; post : post } [@@deriving repr]
-
-
include Irmin.Contents.S with type t := t
-
end
-
-
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
-
(* Undo the last command *)
-
| Undo
-
(* Replay the current branch onto another *)
-
| Replay of string
-
(* Display info *)
-
| Info of [ `Current | `History ]
-
(* Error state *)
-
| Unknown of string list
-
[@@deriving repr]
-
-
include Shelter.Engine.S with type entry = History.t and type action := action
-212
src/lib/shelter/store.ml
···
-
(* A store a bit like OBuilder's but a little simplified
-
for our purposes *)
-
module Build = struct
-
type cid = Cid.t
-
-
let cid_of_string s =
-
match Cid.of_string s with
-
| Ok v -> v
-
| Error (`Msg m) -> failwith m
-
| Error (`Unsupported _) -> failwith "unsupported cid"
-
-
let cid_t = Repr.map Repr.string cid_of_string Cid.to_string
-
-
type t = Image of string | Build of cid [@@deriving repr]
-
end
-
-
type path = string list
-
-
type t = {
-
fs : Eio.Fs.dir_ty Eio.Path.t;
-
proc : Eio_unix.Process.mgr_ty Eio_unix.Process.mgr;
-
zfs : Zfs.Handle.t;
-
pool : string;
-
}
-
-
module Datasets : sig
-
type dataset = private string
-
type snapshot = private string
-
-
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 ( / ) a b = a ^ "/" ^ b
-
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 =
-
let exists = Zfs.exists t.zfs (dataset :> string) typ in
-
if not exists then Zfs.create t.zfs dataset typ;
-
let dataset = Zfs.open_ t.zfs dataset typ in
-
Fun.protect ~finally:(fun () -> Zfs.close dataset) (fun () -> f dataset)
-
-
let mount_dataset ?(typ = Zfs.Types.dataset) t (dataset : Datasets.dataset) =
-
match Zfs.is_mounted t.zfs (dataset :> string) with
-
| Some _ -> ()
-
| None -> with_dataset ~typ t (dataset :> string) @@ fun d -> Zfs.mount d
-
-
let mount_snapshot ?(typ = Zfs.Types.snapshot) t (dataset : Datasets.snapshot) =
-
match Zfs.is_mounted t.zfs (dataset :> string) with
-
| Some _ -> ()
-
| None -> with_dataset ~typ t (dataset :> string) @@ fun d -> Zfs.mount d
-
-
let unmount_dataset t (dataset : Datasets.dataset) =
-
match Zfs.is_mounted t.zfs (dataset :> string) with
-
| None -> ()
-
| Some _ ->
-
with_dataset t (dataset :> string) @@ fun d ->
-
let () = Zfs.unmount d in
-
()
-
-
let create_dataset t (dataset : Datasets.dataset) =
-
with_dataset t (dataset :> string) (fun _ -> ())
-
-
let create_and_mount t (dataset : Datasets.dataset) =
-
create_dataset t dataset;
-
mount_dataset t dataset
-
-
let init fs proc pool =
-
let zfs = Zfs.init () in
-
Zfs.debug zfs true;
-
let t =
-
{
-
fs :> Eio.Fs.dir_ty Eio.Path.t;
-
proc :> Eio_unix.Process.mgr_ty Eio_unix.Process.mgr;
-
zfs;
-
pool;
-
}
-
in
-
create_and_mount t (Datasets.builds t.pool);
-
create_and_mount t (Datasets.tools t.pool);
-
t
-
-
let snapshot t (snap : Datasets.snapshot) =
-
let exists = Zfs.exists t.zfs (snap :> string) Zfs.Types.snapshot in
-
if not exists then Zfs.snapshot t.zfs (snap :> string) true
-
-
let destroy t (d : Datasets.dataset) =
-
with_dataset t (d :> string) @@ fun ds -> Zfs.destroy ds false
-
-
let clone t (snap : Datasets.snapshot) (tgt : Datasets.dataset) =
-
with_dataset ~typ:Zfs.Types.snapshot t (snap :> string) @@ fun src ->
-
Zfs.clone src (tgt :> string)
-
-
let read_all fd =
-
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
-
| n ->
-
Buffer.add_bytes buf (Bytes.sub bytes 0 n);
-
loop ()
-
in
-
loop ()
-
-
let cid s =
-
let hash =
-
Multihash_digestif.of_cstruct `Sha2_256 (Cstruct.of_string s)
-
|> Result.get_ok
-
in
-
Cid.v ~version:`Cidv1 ~base:`Base32 ~codec:`Raw ~hash
-
-
let not_empty s = not String.(equal empty s)
-
-
let get_uid_gid ~username rootfs =
-
let passwd =
-
Eio.Path.load Eio.Path.(rootfs / "etc" / "passwd")
-
|> String.split_on_char '\n'
-
|> List.map (String.split_on_char ':')
-
|> List.map (List.filter not_empty)
-
in
-
List.find_map
-
(function
-
| user :: _ :: uid :: gid :: _ when String.equal user username ->
-
Some (int_of_string uid, int_of_string gid)
-
| _ -> None)
-
passwd
-
-
let fetch t image =
-
let cid = cid image in
-
let cids = cid |> Cid.to_string in
-
let dataset = Datasets.build t.pool cids in
-
let username = Fetch.get_user t.proc image in
-
let dir = Eio.Path.(t.fs / ("/" ^ (Datasets.build t.pool cids :> string))) in
-
if Zfs.exists t.zfs (dataset :> string) Zfs.Types.dataset then
-
( cid,
-
Fetch.get_env t.proc image,
-
get_uid_gid ~username Eio.Path.(dir / "rootfs") )
-
else (
-
create_and_mount t dataset;
-
let _dir : string = Fetch.get_image ~dir ~proc:t.proc image in
-
snapshot t (Datasets.snapshot dataset);
-
( cid,
-
Fetch.get_env t.proc image,
-
get_uid_gid ~username Eio.Path.(dir / "rootfs") ))
-
-
module Run = struct
-
let with_build t cid fn =
-
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
-
mount_dataset t ds;
-
fn ("/" ^ (ds :> string))
-
-
let diff t (data : Datasets.snapshot) (snap : Datasets.snapshot) output =
-
let data_fs =
-
String.sub (data :> string) 0 (String.index (data :> string) '@')
-
in
-
let snap_fs =
-
String.sub (snap :> string) 0 (String.index (snap :> string) '@')
-
in
-
if Option.is_none (Zfs.is_mounted t.zfs data_fs) then
-
with_dataset t data_fs Zfs.mount;
-
if Option.is_none (Zfs.is_mounted t.zfs snap_fs) then
-
with_dataset t snap_fs Zfs.mount;
-
with_dataset ~typ:Zfs.Types.filesystem t data_fs @@ fun zh ->
-
let () =
-
try
-
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
-
let diff = Eio.Path.load output in
-
Diff.of_zfs diff
-
-
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
-
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;
-
match with_build t new_cid fn with
-
| Error _ as v ->
-
destroy t tgt;
-
(v, [])
-
| Ok _ as v ->
-
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"]
-73
src/lib/shelter.ml
···
-
module History = History
-
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_git_unix.FS.KV (H)
-
-
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 =
-
let prompt = Engine.prompt exit_code store in
-
match LNoise.linenoise prompt with
-
| None -> ()
-
| Some input -> (
-
let action = Engine.action_of_command input in
-
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 (`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)
-
-
let command_file_to_actions cf =
-
Eio.Path.load cf |> String.split_on_char '\n'
-
|> List.map Engine.action_of_command
-
-
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
-
| Some file -> (
-
let actions = command_file_to_actions file in
-
let store = History.Store ((module Store), store) in
-
let initial_ctx = Engine.init fs proc store in
-
let folder (store, ctx, exit_code) action =
-
if exit_code <> `Exited 0 then (store, ctx, exit_code)
-
else
-
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 (`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 =
-
List.fold_left folder (store, initial_ctx, `Exited 0) actions
-
in
-
match exit_code with
-
| `Exited 0 -> ()
-
| `Exited n | `Signaled n ->
-
Fmt.epr "%a\n%!" Eio.Process.pp_status exit_code;
-
exit n)
-
| None -> run config ~stdout fs clock proc store
-
end
+5
src/shelter/bin/dune
···
+
(executable
+
(public_name shelter)
+
(package shelter)
+
(name main)
+
(libraries shelter fmt.tty shelter.main shelter.passthrough eio void))
+83
src/shelter/bin/main.ml
···
+
module History = struct
+
type t = string
+
+
let t = Repr.string
+
let merge = Irmin.Merge.default (Repr.option t)
+
end
+
+
module Pass = Shelter.Make (History) (Shelter_passthrough)
+
module Main = Shelter.Make (Shelter_main.History) (Shelter_main)
+
+
let home = Unix.getenv "HOME"
+
+
let state_dir fs type' =
+
let path = Eio.Path.(fs / home / ".cache/shelter" / type') in
+
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 cmd_file =
+
let doc = "Path to a file containing a series of commands." in
+
Arg.(
+
value
+
& opt (some file) None
+
& info [ "f"; "file" ] ~docv:"COMMAND_FILE" ~doc)
+
+
let main =
+
let run config cmd_file =
+
Eventloop.run @@ fun env ->
+
let cmd_file = Option.map (Eio.Path.( / ) env#fs) cmd_file in
+
let dir = state_dir env#fs "shelter" in
+
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 =
+
[
+
`P
+
"Shelter is a shell session shim to help control uncertainty when \
+
working from the terminal";
+
]
+
in
+
let doc = "Shelter: version-controlled shell sessions" in
+
let info = Cmd.info ~man ~doc "main" in
+
(Cmd.v info t, t, info)
+
+
let passthrough =
+
let run config cmd_file =
+
Eventloop.run @@ fun env ->
+
let cmd_file = Option.map (Eio.Path.( / ) env#fs) cmd_file in
+
let dir = state_dir env#fs "passthrough" in
+
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
+
Cmd.v info t
+
+
let extract_commands =
+
let run cmd_file =
+
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
+
let t = Term.(const run $ cmd_file) in
+
let info = Cmd.info "extract" in
+
Cmd.v info t
+
+
let cmds =
+
let cmd, term, info = main in
+
let cmds = [ cmd; passthrough; extract_commands ] in
+
Cmd.group ~default:term info cmds
+
+
let () =
+
Fmt_tty.setup_std_outputs ();
+
exit (Cmd.eval cmds)
+4
src/shelter/lib/dune
···
+
(library
+
(name shelter)
+
(public_name shelter)
+
(libraries cmdliner irmin-git.unix eio.unix eio linenoise void repr morbig))
+50
src/shelter/lib/engine.ml
···
+
module type S = sig
+
type config
+
(** A configuration *)
+
+
val config_term : config Cmdliner.Term.t
+
(** A cmdliner term for constructing a config *)
+
+
type action
+
(** An action to run *)
+
+
val action : action Repr.t
+
val action_of_command : string -> action
+
+
type entry
+
+
type ctx
+
(** 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 ->
+
entry History.t ->
+
ctx
+
(** [init store] will be called before entering the shell loop. You may wish
+
to setup history completions etc. with LNoise. *)
+
+
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,
+
[ `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 *)
+
+
val prompt : Eio.Process.exit_status -> entry History.t -> string
+
(** [prompt previous_exit_code history] generates a prompt from the current
+
[history] *)
+
end
+17
src/shelter/lib/history.ml
···
+
module type S = sig
+
type t
+
(** A single history entry *)
+
+
include Irmin.Contents.S with type t := t
+
end
+
+
type 'entry t =
+
| Store :
+
((module Irmin.S
+
with type t = 'a
+
and type Schema.Branch.t = string
+
and type Schema.Contents.t = 'entry
+
and type Schema.Path.t = string list
+
and type Schema.Path.step = string)
+
* 'a)
+
-> 'entry t
+6
src/shelter/lib/passthrough/dune
···
+
(library
+
(name shelter_passthrough)
+
(public_name shelter.passthrough)
+
(preprocess
+
(pps ppx_repr))
+
(libraries shelter))
+64
src/shelter/lib/passthrough/shelter_passthrough.ml
···
+
open Eio
+
+
type error = string
+
+
let pp_error = Fmt.string
+
+
type config = unit
+
+
let config_term = Cmdliner.Term.const ()
+
+
type action = Exec of string [@@deriving repr]
+
+
let action = action_t
+
let action_of_command cmd = Exec cmd
+
+
type entry = string [@@derviving repr]
+
+
let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
+
+
let prompt _ _ =
+
Fmt.(styled (`Fg `Red) string) Format.str_formatter "shelter-p> ";
+
Format.flush_str_formatter ()
+
+
let history_key = [ "history" ]
+
let key () = history_key @ [ string_of_float @@ Unix.gettimeofday () ]
+
+
type ctx = unit
+
+
let init _ _
+
(Shelter.History.Store ((module S), store) : entry Shelter.History.t) =
+
match S.list store history_key with
+
| [] -> ()
+
| xs ->
+
let rec loop acc = function
+
| `Contents (v, _meta) :: next -> loop (v :: acc) next
+
| _ :: next -> loop acc next
+
| [] -> List.rev acc
+
in
+
let entries =
+
loop [] (List.map (fun (_, tree) -> S.Tree.to_concrete tree) xs)
+
in
+
List.iter (fun v -> LNoise.history_add v |> ignore) entries
+
+
let run (() : config) ~stdout:_ _fs clock proc
+
( ((Shelter.History.Store ((module S), store) : entry Shelter.History.t) as
+
full_store),
+
() ) (Exec command) =
+
let info () =
+
S.Info.v ~message:"shelter" (Eio.Time.now clock |> Int64.of_float)
+
in
+
let cmd =
+
String.split_on_char ' ' command
+
|> List.filter (fun v -> not (String.equal "" v))
+
in
+
Switch.run @@ fun sw ->
+
try
+
let proc = Eio.Process.spawn ~sw proc cmd in
+
let res = Eio.Process.await proc in
+
if res = `Exited 0 then (
+
S.set_exn ~info store (key ()) command;
+
let _ : (unit, string) result = LNoise.history_add command in
+
Ok (full_store, ()))
+
else Shelter.process_error (Eio.Process.Child_error res)
+
with Eio.Exn.Io (Eio.Process.E e, _) -> Shelter.process_error e
+1
src/shelter/lib/passthrough/shelter_passthrough.mli
···
+
include Shelter.Engine.S with type entry = string
+57
src/shelter/lib/script.ml
···
+
module Cst = Morbig.CST
+
+
let redirect_to_string = function
+
| Cst.IoRedirect_IoFile { value = io_file; _ } -> (
+
match io_file with
+
| Cst.IoFile_Great_FileName
+
{ value = Cst.Filename_Word { value = Cst.Word (w, _); _ }; _ } ->
+
Fmt.str "> %s" w
+
| Cst.IoFile_DGreat_FileName
+
{ value = Cst.Filename_Word { value = Cst.Word (w, _); _ }; _ } ->
+
Fmt.str ">> %s" w
+
| _ -> failwith "Redirect Unsupported")
+
| _ -> failwith "IO Redirect Unsupported"
+
+
let cmd_suffix_to_list s =
+
let rec loop = function
+
| Cst.CmdSuffix_Word { value = Cst.Word (s, _); _ } -> [ s ]
+
| Cst.CmdSuffix_CmdSuffix_Word (suff, { value = Cst.Word (s, _); _ }) ->
+
s :: loop suff.value
+
| Cst.CmdSuffix_CmdSuffix_IoRedirect (suff, { value = redirect; _ }) ->
+
let sf = loop suff.value in
+
redirect_to_string redirect :: sf
+
| _ -> failwith "Unsupported!"
+
in
+
loop s |> List.rev |> String.concat " "
+
+
let of_cmd (c : Cst.command) =
+
match c with
+
| Cst.Command_SimpleCommand simple -> (
+
match simple.value with
+
| Cst.SimpleCommand_CmdName
+
{ value = Cst.CmdName_Word { value = Cst.Word (w, _); _ }; _ } ->
+
w
+
| Cst.SimpleCommand_CmdName_CmdSuffix
+
( { value = Cst.CmdName_Word { value = Cst.Word (w, _); _ }; _ },
+
{ value = suff; _ } ) ->
+
let s = cmd_suffix_to_list suff in
+
w ^ " " ^ s
+
| _ -> failwith "Unsupported")
+
| _ -> failwith "Unsupported"
+
+
let cmds_to_strings =
+
let v =
+
object
+
inherit [_] Morbig.CSTVisitors.reduce
+
method zero = []
+
method plus = List.append
+
method! visit_command acc c = of_cmd c :: acc
+
end
+
in
+
v#visit_program []
+
+
let to_commands file =
+
let contents = Eio.Path.load file in
+
let name = Eio.Path.native_exn file |> Filename.basename in
+
let ast = Morbig.parse_string name contents in
+
cmds_to_strings ast
+34
src/shelter/lib/shelter/config.ml
···
+
type t = {
+
no_diffing : bool;
+
no_ebpf : bool;
+
no_runc : bool;
+
image : string;
+
shell : string;
+
}
+
+
let cmdliner =
+
let open Cmdliner in
+
let no_diffing =
+
let doc = "Disable diffing." in
+
Arg.(value & flag & info [ "no-diffing" ] ~doc)
+
in
+
let no_ebpf =
+
let doc = "Disable eBPF." in
+
Arg.(value & flag & info [ "no-ebpf" ] ~doc)
+
in
+
let image =
+
let doc = "Base image to start Shelter with." in
+
Arg.(value & opt string "alpine" & info [ "image" ] ~doc)
+
in
+
let shell =
+
let doc = "Path to the shell (e.g. /bin/ash)" in
+
Arg.(value & opt string "/bin/ash" & info [ "shell" ] ~doc)
+
in
+
let no_runc =
+
let doc = "Disable RUNC and use Void processes." in
+
Arg.(value & flag & info [ "no-runc" ] ~doc)
+
in
+
Term.(
+
const (fun no_diffing no_ebpf no_runc image shell ->
+
{ no_diffing; no_ebpf; no_runc; image; shell })
+
$ no_diffing $ no_ebpf $ no_runc $ image $ shell)
+105
src/shelter/lib/shelter/diff.ml
···
+
type diff =
+
| Modified of string
+
| Created of string
+
| Renamed of string * string
+
| Removed of string
+
[@@deriving repr]
+
+
let path = function
+
| Modified p -> p
+
| Created p -> p
+
| Renamed (p, _) -> p
+
| Removed p -> p
+
+
type t = diff list [@@deriving repr]
+
+
let truncate_path s =
+
match Astring.String.cut ~sep:"rootfs" s with Some (_, p) -> p | None -> s
+
+
let parse_row = function
+
| [ "M"; s ] ->
+
let path = truncate_path s in
+
if String.equal path "" then None else Some (Modified path)
+
| [ "+"; s ] ->
+
let path = truncate_path s in
+
if String.equal path "" then None else Some (Created path)
+
| [ "R"; a; b ] ->
+
let a_path = truncate_path a in
+
let b_path = truncate_path b in
+
Some (Renamed (a_path, b_path))
+
| [ "-"; s ] ->
+
let path = truncate_path s in
+
if String.equal path "" then None else Some (Removed path)
+
| s ->
+
Fmt.invalid_arg "Unknown ZFS diff: %a"
+
(Fmt.list ~sep:Fmt.comma Fmt.string)
+
s
+
+
let of_zfs s : t =
+
let lines = String.split_on_char '\n' s in
+
let tsv =
+
List.map (String.split_on_char '\t') lines
+
|> List.map (List.filter (fun s -> not (String.equal "" s)))
+
|> List.filter (function [] -> false | _ -> true)
+
in
+
List.filter_map parse_row tsv
+
+
type tree = Leaf of diff | Dir of string * tree list
+
+
let rec insert modified path_components tree =
+
match (path_components, tree) with
+
| [], _ -> tree
+
| [ file ], nodes ->
+
if List.exists (function Leaf f -> path f = file | _ -> false) nodes
+
then nodes
+
else
+
let diff =
+
match modified with
+
| Modified _ -> Modified file
+
| Created _ -> Created file
+
| Renamed (_, to_) -> Renamed (file, to_)
+
| Removed _ -> Removed file
+
in
+
Leaf diff :: nodes
+
| dir :: rest, nodes ->
+
let rec insert_into_dir acc = function
+
| [] -> Dir (dir, insert modified rest []) :: List.rev acc
+
| Dir (name, children) :: tl when name = dir ->
+
List.rev_append acc (Dir (name, insert modified rest children) :: tl)
+
| x :: tl -> insert_into_dir (x :: acc) tl
+
in
+
insert_into_dir [] nodes
+
+
let to_tree (diffs : diff list) =
+
let paths =
+
List.map (fun v -> (v, String.split_on_char '/' (path v))) diffs
+
in
+
List.fold_left (fun acc (m, p) -> insert m p acc) [] paths
+
+
let leaves =
+
let rec loop acc acc2 = function
+
| Leaf (Modified v) -> Modified (Filename.concat acc v) :: acc2
+
| Leaf (Created v) -> Created (Filename.concat acc v) :: acc2
+
| Leaf (Removed v) -> Removed (Filename.concat acc v) :: acc2
+
| Leaf (Renamed (r1, r2)) -> Renamed (Filename.concat acc r1, r2) :: acc2
+
| Dir (p, cs) ->
+
List.fold_left (fun lvs v -> loop (Filename.concat acc p) lvs v) acc2 cs
+
in
+
loop "" []
+
+
let pp_diff fmt = function
+
| Modified v -> Fmt.(styled (`Fg `Yellow) string) fmt ("~ /" ^ v)
+
| Created v -> Fmt.(styled (`Fg `Green) string) fmt ("+ /" ^ v)
+
| Removed v -> Fmt.(styled (`Fg `Red) string) fmt ("- /" ^ v)
+
| Renamed (v, _) -> Fmt.(styled (`Fg `Magenta) string) fmt ("| /" ^ v)
+
+
let pp fmt diffs =
+
let tree = to_tree diffs in
+
let lvs =
+
List.fold_left (fun acc v -> leaves v @ acc) [] tree
+
|> List.filter (fun v ->
+
not
+
(String.starts_with ~prefix:"shelter" (path v)
+
|| String.starts_with ~prefix:"tmp" (path v)))
+
in
+
Fmt.pf fmt "%a" Fmt.(list ~sep:Format.pp_force_newline pp_diff) lvs
+13
src/shelter/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 ppx_blob))
+
(libraries shelter cid void zfs))
+53
src/shelter/lib/shelter/fetch.ml
···
+
let ( / ) = Eio.Path.( / )
+
let replace_slash s = String.split_on_char '/' s |> String.concat "-"
+
+
let get_user proc image =
+
Eio.Process.parse_out proc Eio.Buf_read.take_all
+
[
+
"docker";
+
"image";
+
"inspect";
+
"--format";
+
{|{{.Config.User}}|};
+
"--";
+
image;
+
]
+
|> String.trim
+
+
let get_env proc image =
+
Eio.Process.parse_out proc Eio.Buf_read.take_all
+
[
+
"docker";
+
"image";
+
"inspect";
+
"--format";
+
{|{{range .Config.Env}}{{print . "\x00"}}{{end}}|};
+
"--";
+
image;
+
]
+
|> String.split_on_char '\x00'
+
+
let get_image ~dir ~proc image =
+
let container_id =
+
Eio.Process.parse_out proc Eio.Buf_read.take_all
+
[ "docker"; "create"; "--"; image ]
+
|> String.trim
+
in
+
let tar = replace_slash image ^ ".tar.gz" in
+
let dir_s = Eio.Path.native_exn dir in
+
let () =
+
Eio.Process.run proc
+
[ "docker"; "export"; container_id; "-o"; Filename.concat dir_s tar ]
+
in
+
Eio.Path.mkdir ~perm:0o777 (dir / "rootfs");
+
let () =
+
Eio.Process.run proc
+
[
+
"tar";
+
"-xf";
+
Filename.concat dir_s tar;
+
"-C";
+
Filename.concat dir_s "rootfs";
+
]
+
in
+
Filename.concat dir_s "rootfs"
src/shelter/lib/shelter/opentrace

This is a binary file and will not be displayed.

+509
src/shelter/lib/shelter/runc.ml
···
+
(* From Obuilder see License file at end of document *)
+
let ( / ) = Eio.Path.( / )
+
let ( // ) = Filename.concat
+
+
type config = { fast_sync : bool }
+
+
let get_machine () =
+
let ch = Unix.open_process_in "uname -m" in
+
let arch = input_line ch in
+
match Unix.close_process_in ch with
+
| Unix.WEXITED 0 -> String.trim arch
+
| _ -> failwith "Failed to get arch with 'uname -m'"
+
+
let get_arches () =
+
if Sys.unix then
+
match get_machine () with
+
| "x86_64" -> [ "SCMP_ARCH_X86_64"; "SCMP_ARCH_X86"; "SCMP_ARCH_X32" ]
+
| "aarch64" -> [ "SCMP_ARCH_AARCH64"; "SCMP_ARCH_ARM" ]
+
| _ -> []
+
else []
+
+
let secret_file id = "secret-" ^ string_of_int id
+
+
module Json_config = struct
+
let mount ?(options = []) ~ty ~src dst =
+
`Assoc
+
[
+
("destination", `String dst);
+
("type", `String ty);
+
("source", `String src);
+
("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) ]
+
+
(* This is a subset of the capabilities that Docker uses by default.
+
These control what root can do in the container.
+
If the init process is non-root, permitted, effective and ambient sets are cleared.
+
See capabilities(7) for full details. *)
+
let default_linux_caps =
+
[
+
"CAP_CHOWN";
+
(* Make arbitrary changes to file UIDs and GIDs *)
+
"CAP_DAC_OVERRIDE";
+
(* Bypass file read, write, and execute permission checks. *)
+
"CAP_FSETID";
+
(* Set SUID/SGID bits. *)
+
"CAP_FOWNER";
+
(* Bypass permission checks. *)
+
"CAP_MKNOD";
+
(* Create special files using mknod. *)
+
"CAP_SETGID";
+
(* Make arbitrary manipulations of process GIDs. *)
+
"CAP_SETUID";
+
(* Make arbitrary manipulations of process UIDs. *)
+
"CAP_SETFCAP";
+
(* Set arbitrary capabilities on a file. *)
+
"CAP_SETPCAP";
+
(* Add any capability from bounding set to inheritable set. *)
+
"CAP_SYS_CHROOT";
+
(* Use chroot. *)
+
"CAP_KILL";
+
(* Bypass permission checks for sending signals. *)
+
"CAP_AUDIT_WRITE";
+
(* Write records to kernel auditing log. *)
+
"CAP_BPF";
+
"CAP_PERFMON";
+
(* BPF operations *)
+
(* Allowed by Docker, but disabled here (because we use host networking):
+
"CAP_NET_RAW"; (* Use RAW and PACKET sockets / bind to any address *)
+
"CAP_NET_BIND_SERVICE"; (* Bind a socket to Internet domain privileged ports. *)
+
*)
+
]
+
+
let seccomp_syscalls ~fast_sync =
+
if fast_sync then
+
[
+
`Assoc
+
[
+
(* Sync calls are pointless for the builder, because if the computer crashes then we'll
+
just throw the build dir away and start again. And btrfs sync is really slow.
+
Based on https://bblank.thinkmo.de/using-seccomp-to-filter-sync-operations.html
+
Note: requires runc >= v1.0.0-rc92. *)
+
( "names",
+
strings
+
[
+
"fsync";
+
"fdatasync";
+
"msync";
+
"sync";
+
"syncfs";
+
"sync_file_range";
+
] );
+
("action", `String "SCMP_ACT_ERRNO");
+
("errnoRet", `Int 0);
+
(* Return error "success" *)
+
];
+
]
+
else []
+
+
let seccomp_policy =
+
let fields =
+
[
+
("defaultAction", `String "SCMP_ACT_ALLOW");
+
("syscalls", `List (seccomp_syscalls ~fast_sync:true));
+
]
+
in
+
`Assoc fields
+
+
type config = {
+
cwd : string;
+
argv : string list;
+
hostname : string;
+
network : string list;
+
user : int * int;
+
env : string list;
+
mounts : mount list;
+
entrypoint : string option;
+
}
+
+
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 [ ("uid", `Int uid); ("gid", `Int gid) ]
+
in
+
let network_ns =
+
match network with
+
| [ "host" ] -> []
+
| [] -> [ "network" ]
+
| xs ->
+
Fmt.failwith "Unsupported network configuration %a"
+
Fmt.Dump.(list string)
+
xs
+
in
+
let namespaces = network_ns @ [ "pid"; "ipc"; "uts"; "mount" ] in
+
`Assoc
+
[
+
("ociVersion", `String "1.0.1-dev");
+
( "process",
+
`Assoc
+
[
+
("terminal", `Bool true);
+
("user", user);
+
("args", strings argv);
+
("env", strings env);
+
("cwd", `String cwd);
+
( "capabilities",
+
`Assoc
+
[
+
("bounding", strings default_linux_caps);
+
(* Limits capabilities gained on execve. *)
+
("effective", strings default_linux_caps);
+
(* Checked by kernel to decide access *)
+
("inheritable", strings default_linux_caps);
+
(* Preserved across an execve (if root, or cap in ambient set) *)
+
("permitted", strings default_linux_caps);
+
(* Limiting superset for the effective capabilities *)
+
] );
+
( "rlimits",
+
`List
+
[
+
`Assoc
+
[
+
("type", `String "RLIMIT_NOFILE");
+
("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);
+
] );
+
( "root",
+
`Assoc
+
[
+
("path", `String (results_dir // "rootfs"));
+
("readonly", `Bool false);
+
] );
+
("hostname", `String hostname);
+
( "mounts",
+
`List
+
(mount "/proc"
+
~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 "/sys/kernel/debug" ~ty:"debugfs" ~src:"debug"
+
~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
+
[
+
("namespaces", `List (List.map namespace namespaces));
+
( "maskedPaths",
+
strings
+
[
+
"/proc/acpi";
+
"/proc/asound";
+
"/proc/kcore";
+
"/proc/keys";
+
"/proc/latency_stats";
+
"/proc/timer_list";
+
"/proc/timer_stats";
+
"/proc/sched_debug";
+
"/sys/firmware";
+
"/proc/scsi";
+
] );
+
( "readonlyPaths",
+
strings
+
[
+
"/proc/bus";
+
"/proc/fs";
+
"/proc/irq";
+
"/proc/sys";
+
"/proc/sysrq-trigger";
+
] );
+
("seccomp", seccomp_policy);
+
] );
+
]
+
end
+
+
let next_id = ref 0
+
+
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 buf = Cstruct.create 4096 in
+
let copy () ~src =
+
try
+
while true do
+
match Eio.Flow.single_read src buf with
+
| i ->
+
let bufs = [ Cstruct.sub buf 0 i ] in
+
Eio.Fiber.both
+
(fun () -> Eio.Flow.write other bufs)
+
(fun () -> Sink.copy ~src:(Eio.Flow.cstruct_source bufs) t)
+
done
+
with End_of_file -> ()
+
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.(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");
+
Eio.Path.save ~create:(`If_missing 0o644) (eio_tmp / "hosts")
+
"127.0.0.1 localhost builder";
+
let id = string_of_int !next_id in
+
incr next_id;
+
let cmd = [ "runc"; "run"; id ] in
+
let stdout =
+
to_other_sink_as_well ~other:env#stdout
+
(log :> Eio.Flow.sink_ty Eio.Flow.sink)
+
in
+
Eio.Process.spawn ~sw ~stdout ~stderr:env#stdout env#proc ~cwd:eio_tmp cmd
+
+
(*
+
Apache License
+
Version 2.0, January 2004
+
https://www.apache.org/licenses/
+
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+
1. Definitions.
+
+
"License" shall mean the terms and conditions for use, reproduction,
+
and distribution as defined by Sections 1 through 9 of this document.
+
+
"Licensor" shall mean the copyright owner or entity authorized by
+
the copyright owner that is granting the License.
+
+
"Legal Entity" shall mean the union of the acting entity and all
+
other entities that control, are controlled by, or are under common
+
control with that entity. For the purposes of this definition,
+
"control" means (i) the power, direct or indirect, to cause the
+
direction or management of such entity, whether by contract or
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
+
outstanding shares, or (iii) beneficial ownership of such entity.
+
+
"You" (or "Your") shall mean an individual or Legal Entity
+
exercising permissions granted by this License.
+
+
"Source" form shall mean the preferred form for making modifications,
+
including but not limited to software source code, documentation
+
source, and configuration files.
+
+
"Object" form shall mean any form resulting from mechanical
+
transformation or translation of a Source form, including but
+
not limited to compiled object code, generated documentation,
+
and conversions to other media types.
+
+
"Work" shall mean the work of authorship, whether in Source or
+
Object form, made available under the License, as indicated by a
+
copyright notice that is included in or attached to the work
+
(an example is provided in the Appendix below).
+
+
"Derivative Works" shall mean any work, whether in Source or Object
+
form, that is based on (or derived from) the Work and for which the
+
editorial revisions, annotations, elaborations, or other modifications
+
represent, as a whole, an original work of authorship. For the purposes
+
of this License, Derivative Works shall not include works that remain
+
separable from, or merely link (or bind by name) to the interfaces of,
+
the Work and Derivative Works thereof.
+
+
"Contribution" shall mean any work of authorship, including
+
the original version of the Work and any modifications or additions
+
to that Work or Derivative Works thereof, that is intentionally
+
submitted to Licensor for inclusion in the Work by the copyright owner
+
or by an individual or Legal Entity authorized to submit on behalf of
+
the copyright owner. For the purposes of this definition, "submitted"
+
means any form of electronic, verbal, or written communication sent
+
to the Licensor or its representatives, including but not limited to
+
communication on electronic mailing lists, source code control systems,
+
and issue tracking systems that are managed by, or on behalf of, the
+
Licensor for the purpose of discussing and improving the Work, but
+
excluding communication that is conspicuously marked or otherwise
+
designated in writing by the copyright owner as "Not a Contribution."
+
+
"Contributor" shall mean Licensor and any individual or Legal Entity
+
on behalf of whom a Contribution has been received by Licensor and
+
subsequently incorporated within the Work.
+
+
2. Grant of Copyright License. Subject to the terms and conditions of
+
this License, each Contributor hereby grants to You a perpetual,
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+
copyright license to reproduce, prepare Derivative Works of,
+
publicly display, publicly perform, sublicense, and distribute the
+
Work and such Derivative Works in Source or Object form.
+
+
3. Grant of Patent License. Subject to the terms and conditions of
+
this License, each Contributor hereby grants to You a perpetual,
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+
(except as stated in this section) patent license to make, have made,
+
use, offer to sell, sell, import, and otherwise transfer the Work,
+
where such license applies only to those patent claims licensable
+
by such Contributor that are necessarily infringed by their
+
Contribution(s) alone or by combination of their Contribution(s)
+
with the Work to which such Contribution(s) was submitted. If You
+
institute patent litigation against any entity (including a
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
+
or a Contribution incorporated within the Work constitutes direct
+
or contributory patent infringement, then any patent licenses
+
granted to You under this License for that Work shall terminate
+
as of the date such litigation is filed.
+
+
4. Redistribution. You may reproduce and distribute copies of the
+
Work or Derivative Works thereof in any medium, with or without
+
modifications, and in Source or Object form, provided that You
+
meet the following conditions:
+
+
(a) You must give any other recipients of the Work or
+
Derivative Works a copy of this License; and
+
+
(b) You must cause any modified files to carry prominent notices
+
stating that You changed the files; and
+
+
(c) You must retain, in the Source form of any Derivative Works
+
that You distribute, all copyright, patent, trademark, and
+
attribution notices from the Source form of the Work,
+
excluding those notices that do not pertain to any part of
+
the Derivative Works; and
+
+
(d) If the Work includes a "NOTICE" text file as part of its
+
distribution, then any Derivative Works that You distribute must
+
include a readable copy of the attribution notices contained
+
within such NOTICE file, excluding those notices that do not
+
pertain to any part of the Derivative Works, in at least one
+
of the following places: within a NOTICE text file distributed
+
as part of the Derivative Works; within the Source form or
+
documentation, if provided along with the Derivative Works; or,
+
within a display generated by the Derivative Works, if and
+
wherever such third-party notices normally appear. The contents
+
of the NOTICE file are for informational purposes only and
+
do not modify the License. You may add Your own attribution
+
notices within Derivative Works that You distribute, alongside
+
or as an addendum to the NOTICE text from the Work, provided
+
that such additional attribution notices cannot be construed
+
as modifying the License.
+
+
You may add Your own copyright statement to Your modifications and
+
may provide additional or different license terms and conditions
+
for use, reproduction, or distribution of Your modifications, or
+
for any such Derivative Works as a whole, provided Your use,
+
reproduction, and distribution of the Work otherwise complies with
+
the conditions stated in this License.
+
+
5. Submission of Contributions. Unless You explicitly state otherwise,
+
any Contribution intentionally submitted for inclusion in the Work
+
by You to the Licensor shall be under the terms and conditions of
+
this License, without any additional terms or conditions.
+
Notwithstanding the above, nothing herein shall supersede or modify
+
the terms of any separate license agreement you may have executed
+
with Licensor regarding such Contributions.
+
+
6. Trademarks. This License does not grant permission to use the trade
+
names, trademarks, service marks, or product names of the Licensor,
+
except as required for reasonable and customary use in describing the
+
origin of the Work and reproducing the content of the NOTICE file.
+
+
7. Disclaimer of Warranty. Unless required by applicable law or
+
agreed to in writing, Licensor provides the Work (and each
+
Contributor provides its Contributions) on an "AS IS" BASIS,
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+
implied, including, without limitation, any warranties or conditions
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+
PARTICULAR PURPOSE. You are solely responsible for determining the
+
appropriateness of using or redistributing the Work and assume any
+
risks associated with Your exercise of permissions under this License.
+
+
8. Limitation of Liability. In no event and under no legal theory,
+
whether in tort (including negligence), contract, or otherwise,
+
unless required by applicable law (such as deliberate and grossly
+
negligent acts) or agreed to in writing, shall any Contributor be
+
liable to You for damages, including any direct, indirect, special,
+
incidental, or consequential damages of any character arising as a
+
result of this License or out of the use or inability to use the
+
Work (including but not limited to damages for loss of goodwill,
+
work stoppage, computer failure or malfunction, or any and all
+
other commercial damages or losses), even if such Contributor
+
has been advised of the possibility of such damages.
+
+
9. Accepting Warranty or Additional Liability. While redistributing
+
the Work or Derivative Works thereof, You may choose to offer,
+
and charge a fee for, acceptance of support, warranty, indemnity,
+
or other liability obligations and/or rights consistent with this
+
License. However, in accepting such obligations, You may act only
+
on Your own behalf and on Your sole responsibility, not on behalf
+
of any other Contributor, and only if You agree to indemnify,
+
defend, and hold each Contributor harmless for any liability
+
incurred by, or claims asserted against, such Contributor by reason
+
of your accepting any such warranty or additional liability.
+
+
END OF TERMS AND CONDITIONS
+
+
Copyright 2020 Thomas Leonard
+
+
Licensed under the Apache License, Version 2.0 (the "License");
+
you may not use this file except in compliance with the License.
+
You may obtain a copy of the License at
+
+
https://www.apache.org/licenses/LICENSE-2.0
+
+
Unless required by applicable law or agreed to in writing, software
+
distributed under the License is distributed on an "AS IS" BASIS,
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
See the License for the specific language governing permissions and
+
limitations under the License. *)
+592
src/shelter/lib/shelter/shelter_main.ml
···
+
open Eio
+
module Store = Store
+
module H = Shelter.History
+
+
type error = string
+
+
let pp_error = Fmt.string
+
+
module History = struct
+
type mode = Void.mode
+
+
let mode_t =
+
Repr.map Repr.string
+
(function
+
| "R" -> Void.R | "RW" -> Void.RW | _ -> failwith "Malformed Void.mode")
+
(function Void.R -> "R" | Void.RW -> "RW")
+
+
type post = { diff : Diff.t; time : int64 } [@@deriving repr]
+
+
type pre = {
+
mode : mode;
+
build : Store.Build.t;
+
args : string list;
+
env : string list;
+
cwd : string;
+
user : int * int;
+
}
+
[@@deriving repr]
+
(** Needed for execution *)
+
+
type t = { pre : pre; post : post } [@@deriving repr]
+
+
let merge = Irmin.Merge.(default (Repr.option t))
+
end
+
+
type config = Config.t
+
+
let config_term = Config.cmdliner
+
+
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
+
(* Undo the last command *)
+
| Undo
+
(* Replay the current branch onto another *)
+
| Replay of string
+
(* Display info *)
+
| Info of [ `Current | `History ]
+
(* Error state *)
+
| Unknown of string list
+
[@@deriving repr]
+
+
let split_and_remove_empty s =
+
String.split_on_char ' ' s |> List.filter (fun v -> not (String.equal "" v))
+
+
let action = action_t
+
+
let shelter_action = function
+
| "mode" :: [ "r" ] -> Set_mode R
+
| "mode" :: [ "rw" ] -> Set_mode RW
+
| "session" :: [ m ] -> Set_session m
+
| "replay" :: [ onto ] -> Replay onto
+
| [ "info" ] -> Info `Current
+
| [ "undo" ] -> Undo
+
| [ "history" ] -> Info `History
+
| other -> Unknown other
+
+
let action_of_command cmd =
+
match split_and_remove_empty cmd with
+
| "@" :: rest -> shelter_action rest
+
| args -> Exec args
+
+
let () = Fmt.set_style_renderer Format.str_formatter `Ansi_tty
+
let history_key = [ "history" ]
+
let key clock = history_key @ [ string_of_float @@ Eio.Time.now clock ]
+
+
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
+
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 history s with [] -> default () | (_, hd) :: _ -> f hd
+
+
let text c = Fmt.(styled (`Fg c) string)
+
+
let sessions (H.Store ((module S), store) : entry H.t) =
+
S.Branch.list (S.repo store)
+
+
let commit ~message clock (H.Store ((module S), store) : entry H.t) v =
+
let info () = S.Info.v ~message (Eio.Time.now clock |> Int64.of_float) in
+
S.set_exn ~info store (key clock) v
+
+
let which_branch ((H.Store ((module S), session) : entry H.t) as s) =
+
let branches = sessions s in
+
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
+
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) =
+
match
+
List.filter_map (S.Commit.of_hash (S.repo session))
+
@@ S.Commit.parents (S.Head.get session)
+
with
+
| [] -> s
+
| p :: _ ->
+
S.Head.set session p;
+
s
+
+
(* Fork a new session from an existing one *)
+
let fork (H.Store ((module S), session) : entry H.t) new_branch =
+
let repo = S.repo session in
+
match (S.Head.find session, S.Branch.find repo new_branch) with
+
| _, Some _ ->
+
Error (new_branch ^ " already exists, try @ session " ^ new_branch)
+
| None, _ -> Error "Current branch needs at least one commit"
+
| Some commit, None ->
+
let new_store = S.of_branch (S.repo session) new_branch in
+
S.Branch.set repo new_branch commit;
+
let store = H.Store ((module S), new_store) in
+
Ok store
+
+
(* Fork a new session from an existing one *)
+
let display_history (s : entry H.t) =
+
let pp_diff fmt d = if d = [] then () else Fmt.pf fmt "\n%a%!" Diff.pp d in
+
let pp_entry fmt (e : entry) =
+
Fmt.pf fmt "%-10s %s%a"
+
Fmt.(str "%a" (styled (`Fg `Yellow) uint64_ns_span) e.post.time)
+
(String.concat " " e.pre.args)
+
pp_diff e.post.diff
+
in
+
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 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_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) =
+
Fmt.pf Format.str_formatter "%a%a%a : { mode: %a }> " pp_status status
+
(text `Yellow) "shelter" pp_sesh sesh (text `Red)
+
(if e.pre.mode = R then "r" else "rw");
+
Format.flush_str_formatter ()
+
in
+
with_latest store ~default:prompt prompt_entry
+
+
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.pre = { History.args; _ }; _ }) ->
+
LNoise.history_add (String.concat " " args) |> ignore)
+
(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 savedTio = Unix.tcgetattr Unix.stdin in
+
let tio =
+
{
+
savedTio with
+
(* input modes *)
+
c_ignpar = true;
+
c_istrip = false;
+
c_inlcr = false;
+
c_igncr = false;
+
c_ixon = false;
+
(* c_ixany = false; *)
+
(* c_iuclc = false; *)
+
c_ixoff = false;
+
(* output modes *)
+
c_opost = false;
+
(* control modes *)
+
c_isig = false;
+
c_icanon = false;
+
c_echo = false;
+
c_echoe = false;
+
c_echok = false;
+
c_echonl = false;
+
(* c_iexten = false; *)
+
+
(* special characters *)
+
c_vmin = 1;
+
c_vtime = 0;
+
}
+
in
+
Unix.tcsetattr Unix.stdin TCSADRAIN tio;
+
let start, res =
+
Switch.run @@ fun sw ->
+
let log =
+
Eio.Path.open_out ~sw ~create:(`Or_truncate 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
+
+
(* restore tio *)
+
Unix.tcsetattr Unix.stdin TCSADRAIN savedTio;
+
+
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) ~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 pre = { entry.pre with mode } };
+
Ok (s, 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" (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" (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 =
+
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
+
in
+
let latest =
+
with_latest
+
~default:(fun () -> None)
+
s
+
(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)
+
sessions (text `Green) sesh
+
Fmt.(option string)
+
latest
+
Fmt.(vbox ~indent:2 @@ list pp_commit)
+
commits;
+
Ok (s, ctx)
+
| Exec [] -> Ok (s, ctx)
+
| Undo -> Ok (reset_hard s, ctx)
+
| Replay branch -> replay config s ctx fs clock proc stdout branch
+
| Info `History ->
+
display_history s;
+
Ok (s, ctx)
+
| Exec command -> (
+
let entry =
+
with_latest
+
~default:(fun () ->
+
History.
+
{
+
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.id
+
in
+
let entry = { entry with pre = { entry.pre with args = command } } in
+
try
+
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)
+41
src/shelter/lib/shelter/shelter_main.mli
···
+
module Store = Store
+
+
module History : sig
+
type mode = Void.mode
+
type post = { diff : Diff.t; time : int64 } [@@deriving repr]
+
+
type pre = {
+
mode : Void.mode;
+
build : Store.Build.t;
+
args : string list;
+
env : string list;
+
cwd : string;
+
user : int * int;
+
}
+
[@@deriving repr]
+
(** Needed for execution *)
+
+
type t = { pre : pre; post : post } [@@deriving repr]
+
+
include Irmin.Contents.S with type t := t
+
end
+
+
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
+
(* Undo the last command *)
+
| Undo
+
(* Replay the current branch onto another *)
+
| Replay of string
+
(* Display info *)
+
| Info of [ `Current | `History ]
+
(* Error state *)
+
| Unknown of string list
+
[@@deriving repr]
+
+
include Shelter.Engine.S with type entry = History.t and type action := action
+212
src/shelter/lib/shelter/store.ml
···
+
(* A store a bit like OBuilder's but a little simplified
+
for our purposes *)
+
module Build = struct
+
type cid = Cid.t
+
+
let cid_of_string s =
+
match Cid.of_string s with
+
| Ok v -> v
+
| Error (`Msg m) -> failwith m
+
| Error (`Unsupported _) -> failwith "unsupported cid"
+
+
let cid_t = Repr.map Repr.string cid_of_string Cid.to_string
+
+
type t = Image of string | Build of cid [@@deriving repr]
+
end
+
+
type path = string list
+
+
type t = {
+
fs : Eio.Fs.dir_ty Eio.Path.t;
+
proc : Eio_unix.Process.mgr_ty Eio_unix.Process.mgr;
+
zfs : Zfs.Handle.t;
+
pool : string;
+
}
+
+
module Datasets : sig
+
type dataset = private string
+
type snapshot = private string
+
+
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 ( / ) a b = a ^ "/" ^ b
+
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 =
+
let exists = Zfs.exists t.zfs (dataset :> string) typ in
+
if not exists then Zfs.create t.zfs dataset typ;
+
let dataset = Zfs.open_ t.zfs dataset typ in
+
Fun.protect ~finally:(fun () -> Zfs.close dataset) (fun () -> f dataset)
+
+
let mount_dataset ?(typ = Zfs.Types.dataset) t (dataset : Datasets.dataset) =
+
match Zfs.is_mounted t.zfs (dataset :> string) with
+
| Some _ -> ()
+
| None -> with_dataset ~typ t (dataset :> string) @@ fun d -> Zfs.mount d
+
+
let mount_snapshot ?(typ = Zfs.Types.snapshot) t (dataset : Datasets.snapshot) =
+
match Zfs.is_mounted t.zfs (dataset :> string) with
+
| Some _ -> ()
+
| None -> with_dataset ~typ t (dataset :> string) @@ fun d -> Zfs.mount d
+
+
let unmount_dataset t (dataset : Datasets.dataset) =
+
match Zfs.is_mounted t.zfs (dataset :> string) with
+
| None -> ()
+
| Some _ ->
+
with_dataset t (dataset :> string) @@ fun d ->
+
let () = Zfs.unmount d in
+
()
+
+
let create_dataset t (dataset : Datasets.dataset) =
+
with_dataset t (dataset :> string) (fun _ -> ())
+
+
let create_and_mount t (dataset : Datasets.dataset) =
+
create_dataset t dataset;
+
mount_dataset t dataset
+
+
let init fs proc pool =
+
let zfs = Zfs.init () in
+
Zfs.debug zfs true;
+
let t =
+
{
+
fs :> Eio.Fs.dir_ty Eio.Path.t;
+
proc :> Eio_unix.Process.mgr_ty Eio_unix.Process.mgr;
+
zfs;
+
pool;
+
}
+
in
+
create_and_mount t (Datasets.builds t.pool);
+
create_and_mount t (Datasets.tools t.pool);
+
t
+
+
let snapshot t (snap : Datasets.snapshot) =
+
let exists = Zfs.exists t.zfs (snap :> string) Zfs.Types.snapshot in
+
if not exists then Zfs.snapshot t.zfs (snap :> string) true
+
+
let destroy t (d : Datasets.dataset) =
+
with_dataset t (d :> string) @@ fun ds -> Zfs.destroy ds false
+
+
let clone t (snap : Datasets.snapshot) (tgt : Datasets.dataset) =
+
with_dataset ~typ:Zfs.Types.snapshot t (snap :> string) @@ fun src ->
+
Zfs.clone src (tgt :> string)
+
+
let read_all fd =
+
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
+
| n ->
+
Buffer.add_bytes buf (Bytes.sub bytes 0 n);
+
loop ()
+
in
+
loop ()
+
+
let cid s =
+
let hash =
+
Multihash_digestif.of_cstruct `Sha2_256 (Cstruct.of_string s)
+
|> Result.get_ok
+
in
+
Cid.v ~version:`Cidv1 ~base:`Base32 ~codec:`Raw ~hash
+
+
let not_empty s = not String.(equal empty s)
+
+
let get_uid_gid ~username rootfs =
+
let passwd =
+
Eio.Path.load Eio.Path.(rootfs / "etc" / "passwd")
+
|> String.split_on_char '\n'
+
|> List.map (String.split_on_char ':')
+
|> List.map (List.filter not_empty)
+
in
+
List.find_map
+
(function
+
| user :: _ :: uid :: gid :: _ when String.equal user username ->
+
Some (int_of_string uid, int_of_string gid)
+
| _ -> None)
+
passwd
+
+
let fetch t image =
+
let cid = cid image in
+
let cids = cid |> Cid.to_string in
+
let dataset = Datasets.build t.pool cids in
+
let username = Fetch.get_user t.proc image in
+
let dir = Eio.Path.(t.fs / ("/" ^ (Datasets.build t.pool cids :> string))) in
+
if Zfs.exists t.zfs (dataset :> string) Zfs.Types.dataset then
+
( cid,
+
Fetch.get_env t.proc image,
+
get_uid_gid ~username Eio.Path.(dir / "rootfs") )
+
else (
+
create_and_mount t dataset;
+
let _dir : string = Fetch.get_image ~dir ~proc:t.proc image in
+
snapshot t (Datasets.snapshot dataset);
+
( cid,
+
Fetch.get_env t.proc image,
+
get_uid_gid ~username Eio.Path.(dir / "rootfs") ))
+
+
module Run = struct
+
let with_build t cid fn =
+
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
+
mount_dataset t ds;
+
fn ("/" ^ (ds :> string))
+
+
let diff t (data : Datasets.snapshot) (snap : Datasets.snapshot) output =
+
let data_fs =
+
String.sub (data :> string) 0 (String.index (data :> string) '@')
+
in
+
let snap_fs =
+
String.sub (snap :> string) 0 (String.index (snap :> string) '@')
+
in
+
if Option.is_none (Zfs.is_mounted t.zfs data_fs) then
+
with_dataset t data_fs Zfs.mount;
+
if Option.is_none (Zfs.is_mounted t.zfs snap_fs) then
+
with_dataset t snap_fs Zfs.mount;
+
with_dataset ~typ:Zfs.Types.filesystem t data_fs @@ fun zh ->
+
let () =
+
try
+
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
+
let diff = Eio.Path.load output in
+
Diff.of_zfs diff
+
+
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
+
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;
+
match with_build t new_cid fn with
+
| Error _ as v ->
+
destroy t tgt;
+
(v, [])
+
| Ok _ as v ->
+
snapshot t tgt_snap;
+
let d = diff t src_snap tgt_snap output in
+
(v, d))
+
end
+1
src/shelter/lib/shelter/tools.ml
···
+
let opentrace = [%blob "./opentrace"]
+73
src/shelter/lib/shelter.ml
···
+
module History = History
+
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_git_unix.FS.KV (H)
+
+
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 =
+
let prompt = Engine.prompt exit_code store in
+
match LNoise.linenoise prompt with
+
| None -> ()
+
| Some input -> (
+
let action = Engine.action_of_command input in
+
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 (`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)
+
+
let command_file_to_actions cf =
+
Eio.Path.load cf |> String.split_on_char '\n'
+
|> List.map Engine.action_of_command
+
+
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
+
| Some file -> (
+
let actions = command_file_to_actions file in
+
let store = History.Store ((module Store), store) in
+
let initial_ctx = Engine.init fs proc store in
+
let folder (store, ctx, exit_code) action =
+
if exit_code <> `Exited 0 then (store, ctx, exit_code)
+
else
+
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 (`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 =
+
List.fold_left folder (store, initial_ctx, `Exited 0) actions
+
in
+
match exit_code with
+
| `Exited 0 -> ()
+
| `Exited n | `Signaled n ->
+
Fmt.epr "%a\n%!" Eio.Process.pp_status exit_code;
+
exit n)
+
| None -> run config ~stdout fs clock proc store
+
end
+4
src/shelterd/dune
···
+
(executable
+
(name main)
+
(public_name shelterd)
+
(libraries eio_main shelter_common capnp-rpc-unix index.unix))
+134
src/shelterd/main.ml
···
+
open Shelter_common
+
open Capnp_rpc
+
+
module Admin = struct
+
module Secret = Capnp_rpc_net.Restorer.Id
+
+
let add_user t restorer name =
+
match Store.lookup t name with
+
| Some _ -> Fmt.failwith "User %s already exists!" name
+
| None -> (
+
let secret = Store.add_client t name in
+
match Capnp_rpc_net.Restorer.restore restorer secret with
+
| Ok v -> v
+
| Error exn ->
+
Fmt.failwith "%a" Capnp_rpc_proto.Error.pp (`Exception exn))
+
+
let remove_user t name = Store.remove t name
+
+
let v sr restorer t =
+
let add_user = add_user t restorer in
+
let remove_user = remove_user t in
+
Admin.v ~add_user ~remove_user sr
+
end
+
+
open Capnp_rpc_net
+
+
let export ~secrets_dir ~vat ~name id =
+
let ( / ) = Filename.concat in
+
let path = secrets_dir / (name ^ ".cap") in
+
Capnp_rpc_unix.Cap_file.save_service vat id path |> or_fail;
+
Logs.app (fun f -> f "Wrote capability reference to %S" path)
+
+
let daemon capnp services store secrets_dir =
+
let restore = Restorer.of_table services in
+
let admin_id = Capnp_rpc_unix.Vat_config.derived_id capnp "admin" in
+
let admin =
+
let sr = Capnp_rpc_net.Restorer.Table.sturdy_ref services admin_id in
+
Admin.v sr restore store
+
in
+
Restorer.Table.add services admin_id admin;
+
Eio.Switch.run @@ fun sw ->
+
let vat = Capnp_rpc_unix.serve capnp ~sw ~restore in
+
export ~secrets_dir ~vat ~name:"admin" admin_id;
+
Logs.app (fun f -> f "shelterd running...");
+
Eio.Promise.await (Eio.Promise.create () |> fst)
+
+
open Cmdliner
+
+
let setup_log style_renderer level =
+
Fmt_tty.setup_std_outputs ?style_renderer ();
+
Logs.set_level level;
+
Logs.set_reporter (Logs_fmt.reporter ());
+
()
+
+
let setup_log =
+
let docs = Manpage.s_common_options in
+
Term.(
+
const setup_log $ Fmt_cli.style_renderer ~docs () $ Logs_cli.level ~docs ())
+
+
let admin =
+
Arg.required
+
@@ Arg.opt Arg.(some file) None
+
@@ Arg.info ~doc:"Path of the admin capability." ~docv:"ADDR"
+
[ "c"; "connect" ]
+
+
let username =
+
Arg.required
+
@@ Arg.pos 0 Arg.(some string) None
+
@@ Arg.info ~doc:"The name of the new user to add." ~docv:"NAME" []
+
+
let daemon env =
+
let doc = "run the shelter daemon" in
+
let man =
+
[
+
`S Manpage.s_description;
+
`P "The shelter daemon provides a way to run sessions for shelter users.";
+
]
+
in
+
let info = Cmd.info ~man "daemon" ~doc in
+
let daemon () capnp =
+
let make_sturdy = Capnp_rpc_unix.Vat_config.sturdy_uri capnp in
+
let connect = Obj.magic () in
+
let load ~validate:_ ~sturdy_ref =
+
let sr = Capnp_rpc.Sturdy_ref.cast sturdy_ref in
+
Restorer.grant (User.v sr connect)
+
in
+
let loader = Store.create ~make_sturdy ~load "shelter.index" in
+
Eio.Switch.run @@ fun sw ->
+
let services = Restorer.Table.of_loader ~sw (module Store) loader in
+
daemon capnp services loader.store "./secrets"
+
in
+
let term =
+
Term.(const daemon $ setup_log $ Capnp_rpc_unix.Vat_config.cmd env)
+
in
+
(Cmd.v info term, term)
+
+
let add_cmd env =
+
let doc = "add a new client" in
+
let man =
+
[
+
`S Manpage.s_description;
+
`P
+
"Add a new client and get a capablity back to use for that client to \
+
run shelter sessions.";
+
]
+
in
+
let info = Cmd.info ~man "add" ~doc in
+
let add () cap_path name =
+
Eio.Switch.run @@ fun sw ->
+
let vat = Capnp_rpc_unix.client_only_vat ~sw env#net in
+
let sr = Capnp_rpc_unix.Cap_file.load vat cap_path |> or_fail in
+
Capnp_rpc_unix.with_cap_exn sr @@ fun service ->
+
let cap = Shelter_common.Admin.add_user service name in
+
Capability.with_ref cap @@ fun client ->
+
let uri = Persistence.save_exn client in
+
Fmt.pr "%a" Uri.pp uri
+
in
+
Cmd.v info Term.(const add $ setup_log $ admin $ username)
+
+
let () =
+
Eio_main.run @@ fun env ->
+
let doc = "Shelterd" in
+
let man =
+
[
+
`S Manpage.s_authors;
+
`P "Patrick Ferris";
+
`S Manpage.s_bugs;
+
`P "Email bug reports to <patrick@sirref.org>.";
+
]
+
in
+
let info = Cmd.info ~doc ~man "shelterd" in
+
let daemon_cmd, daemon_term = daemon env in
+
exit
+
(Cmd.eval @@ Cmd.group ~default:daemon_term info [ daemon_cmd; add_cmd env ])
+71
src/shelterd/store.ml
···
+
(* let () = Mirage_crypto_rng_lwt.initialize (module Mirage_crypto_rng.Fortuna) *)
+
let hash_size = 256
+
+
module Fixed_string = Index.Key.String_fixed (struct
+
let length = 256
+
end)
+
+
module I = Index_unix.Make (Fixed_string) (Fixed_string) (Index.Cache.Noop)
+
module Secret = Capnp_rpc_net.Restorer.Id
+
+
type t = {
+
store : I.t;
+
make_sturdy : Secret.t -> Uri.t;
+
load :
+
validate:(unit -> bool) ->
+
sturdy_ref:[ `Generic ] Capnp_rpc.Sturdy_ref.t ->
+
Capnp_rpc_net.Restorer.resolution;
+
}
+
+
let create ~make_sturdy ~load path =
+
let store = I.v ~log_size:4096 path in
+
{ store; make_sturdy; load }
+
+
let pad_name name =
+
let diff = hash_size - String.length name in
+
if diff >= 0 then String.make diff ' ' ^ name else failwith "Name too long!"
+
+
let add_client t name =
+
let name = String.trim name in
+
let secret = Secret.generate () in
+
let hash = Secret.digest `SHA256 secret in
+
let name = pad_name name in
+
let store_secret = pad_name hash in
+
I.replace t name store_secret;
+
I.replace t store_secret name;
+
I.merge t;
+
secret
+
+
let lookup t name =
+
let name = pad_name name in
+
try Some (I.find t name) with Not_found -> None
+
+
let lookup_by_hash t digest =
+
try Some (I.find t (pad_name digest)) with Not_found -> None
+
+
let remove t name =
+
let name = String.trim name in
+
let padded_name = pad_name name in
+
I.filter t (fun (k, _) -> k = padded_name);
+
I.merge t
+
+
let list t =
+
let lst = ref [] in
+
I.iter (fun k _ -> lst := String.trim k :: !lst) t;
+
List.stable_sort String.compare !lst
+
+
module type T = Capnp_rpc_net.Restorer.LOADER
+
+
let hash _ = `SHA256
+
let make_sturdy t = t.make_sturdy
+
+
let validate t digest () =
+
match lookup t.store digest with None -> false | Some _ -> true
+
+
let load t self digest =
+
Logs.info (fun f -> f "Looking up %s" digest);
+
match lookup_by_hash t.store digest with
+
| None -> Capnp_rpc_net.Restorer.unknown_service_id
+
| Some _ ->
+
t.load ~validate:(validate t digest)
+
~sturdy_ref:(Capnp_rpc.Sturdy_ref.cast self)
+1 -1
vendor/zfs/src/function_description.ml
···
let is_mounted =
foreign "is_mounted"
-
(Types.libzfs_handle_t @-> string @-> ptr string @-> returning bool)
+
(Types.libzfs_handle_t @-> string @-> ptr (ptr char) @-> returning bool)
let diff =
foreign "zfs_show_diffs"
+9 -5
vendor/zfs/src/zfs.ml
···
let exists handle path (type_ : Types.t) = C.Functions.exists handle path type_
+
let null_string = Ctypes.(coerce (ptr void) (ptr char) null)
+
let is_mounted handle path =
-
let where = Ctypes.allocate Ctypes.string "" in
-
let v = C.Functions.is_mounted handle path where in
-
if not v then None else Some (Ctypes.( !@ ) where)
-
-
let null_string = Ctypes.(coerce (ptr void) (ptr char) null)
+
let where = Ctypes.(coerce (ptr void) (ptr char) null) in
+
let where_ptr = Ctypes.(allocate (ptr char) where) in
+
let v = C.Functions.is_mounted handle path where_ptr in
+
if not v then None else
+
let v = Ctypes.( !@ ) where_ptr in
+
let s = Ctypes.string_from_ptr v ~length:256 in
+
Some s
let mount ?mount_opts ?(mount_flags = 0) dataset =
let res = C.Functions.mount dataset mount_opts mount_flags in