2020-12-02 13:33:15 +00:00
|
|
|
let src = Logs.Src.create "builder-web" ~doc:"Builder_web"
|
|
|
|
module Log = (val Logs.src_log src : Logs.LOG)
|
|
|
|
|
2020-12-08 10:49:26 +00:00
|
|
|
open Lwt.Syntax
|
|
|
|
open Lwt_result.Infix
|
2020-12-02 13:33:15 +00:00
|
|
|
|
2022-02-02 14:50:44 +00:00
|
|
|
let sprintf = Printf.sprintf
|
|
|
|
|
2021-01-08 12:47:17 +00:00
|
|
|
let pp_error ppf = function
|
|
|
|
| #Caqti_error.connect as e -> Caqti_error.pp ppf e
|
|
|
|
| #Model.error as e -> Model.pp_error ppf e
|
2021-02-16 17:33:04 +00:00
|
|
|
| `Wrong_version (application_id, version) ->
|
|
|
|
if application_id = Builder_db.application_id
|
|
|
|
then Format.fprintf ppf "Wrong database version: %Ld" version
|
|
|
|
else Format.fprintf ppf "Wrong database application id: %ld" application_id
|
2021-01-08 12:47:17 +00:00
|
|
|
|
2021-03-24 13:49:23 +00:00
|
|
|
let init_datadir datadir =
|
2021-10-20 09:10:43 +00:00
|
|
|
let ( let* ) = Result.bind and ( let+ ) x f = Result.map f x in
|
|
|
|
let* exists = Bos.OS.Dir.exists datadir in
|
|
|
|
let* () =
|
|
|
|
if exists
|
|
|
|
then Ok ()
|
|
|
|
else Error (`Msg "Datadir does not exist")
|
|
|
|
in
|
|
|
|
let+ _ = Bos.OS.Dir.create ~path:false (Model.staging datadir) in
|
|
|
|
()
|
2021-03-24 13:49:23 +00:00
|
|
|
|
2021-06-01 14:06:36 +00:00
|
|
|
let init dbpath datadir =
|
2021-10-20 09:10:43 +00:00
|
|
|
Result.bind (init_datadir datadir) @@ fun () ->
|
2021-06-01 14:06:36 +00:00
|
|
|
Lwt_main.run (
|
|
|
|
Caqti_lwt.connect
|
|
|
|
(Uri.make ~scheme:"sqlite3" ~path:dbpath ~query:["create", ["false"]] ())
|
|
|
|
>>= fun (module Db : Caqti_lwt.CONNECTION) ->
|
|
|
|
Db.find Builder_db.get_application_id () >>= fun application_id ->
|
|
|
|
Db.find Builder_db.get_version () >>= (fun version ->
|
|
|
|
if (application_id, version) = Builder_db.(application_id, current_version)
|
|
|
|
then Lwt_result.return ()
|
|
|
|
else Lwt_result.fail (`Wrong_version (application_id, version)))
|
|
|
|
>>= fun () ->
|
|
|
|
Model.cleanup_staging datadir (module Db))
|
2020-12-02 13:33:15 +00:00
|
|
|
|
2021-06-25 16:43:47 +00:00
|
|
|
let pp_exec ppf ((job : Builder.script_job), uuid, _, _, _, _, _) =
|
2021-01-21 11:02:07 +00:00
|
|
|
Format.fprintf ppf "%s(%a)" job.Builder.name Uuidm.pp uuid
|
|
|
|
|
2020-12-07 11:24:09 +00:00
|
|
|
let safe_seg path =
|
|
|
|
if Fpath.is_seg path && not (Fpath.is_rel_seg path)
|
|
|
|
then Ok (Fpath.v path)
|
2021-10-20 09:10:43 +00:00
|
|
|
else Fmt.kstr (fun s -> Error (`Msg s)) "unsafe path %S" path
|
2020-12-07 11:24:09 +00:00
|
|
|
|
2021-01-06 13:28:10 +00:00
|
|
|
(* mime lookup with orb knowledge *)
|
|
|
|
let mime_lookup path =
|
2021-01-08 12:47:17 +00:00
|
|
|
match Fpath.to_string path with
|
2021-01-06 13:28:10 +00:00
|
|
|
| "build-environment" | "opam-switch" | "system-packages" ->
|
|
|
|
"text/plain"
|
2021-01-08 12:47:17 +00:00
|
|
|
| _ ->
|
|
|
|
if Fpath.has_ext "build-hashes" path
|
2021-01-06 13:28:10 +00:00
|
|
|
then "text/plain"
|
2021-01-08 12:47:17 +00:00
|
|
|
else if Fpath.is_prefix Fpath.(v "bin/") path
|
2021-01-06 13:28:10 +00:00
|
|
|
then "application/octet-stream"
|
2021-01-08 12:47:17 +00:00
|
|
|
else Magic_mime.lookup (Fpath.to_string path)
|
2021-01-06 13:28:10 +00:00
|
|
|
|
2022-02-09 15:23:05 +00:00
|
|
|
let string_of_html =
|
|
|
|
Format.asprintf "%a" (Tyxml.Html.pp ())
|
|
|
|
|
|
|
|
let not_found req =
|
|
|
|
let path = "/" ^ String.concat "/" (Dream.path req) in
|
|
|
|
let referer = Dream.header "referer" req in
|
|
|
|
Views.page_not_found ~path ~referer
|
|
|
|
|> string_of_html |> Dream.html ~status:`Not_Found
|
|
|
|
|
2021-06-07 13:52:37 +00:00
|
|
|
let or_error_response r =
|
|
|
|
let* r = r in
|
|
|
|
match r with
|
|
|
|
| Ok response -> Lwt.return response
|
2022-02-09 15:23:05 +00:00
|
|
|
| Error (text, `Not_Found) ->
|
|
|
|
Views.resource_not_found ~text
|
|
|
|
|> string_of_html |> Dream.html ~status:`Not_Found
|
2021-06-07 13:52:37 +00:00
|
|
|
| Error (text, status) -> Dream.respond ~status text
|
|
|
|
|
2021-12-13 19:21:43 +00:00
|
|
|
let default_log_warn ~status e =
|
|
|
|
Log.warn (fun m -> m "%s: %a" (Dream.status_to_string status) pp_error e)
|
|
|
|
|
|
|
|
let if_error
|
|
|
|
?(status = `Internal_Server_Error)
|
|
|
|
?(log = default_log_warn ~status)
|
|
|
|
message r =
|
2021-06-07 13:52:37 +00:00
|
|
|
let* r = r in
|
|
|
|
match r with
|
2021-06-09 09:48:51 +00:00
|
|
|
| Error `Not_found ->
|
2021-06-09 14:25:00 +00:00
|
|
|
Lwt_result.fail ("Resource not found", `Not_Found)
|
2021-06-07 13:52:37 +00:00
|
|
|
| Error (#Model.error as e) ->
|
|
|
|
log e;
|
|
|
|
Lwt_result.fail (message, status)
|
|
|
|
| Ok _ as r -> Lwt.return r
|
|
|
|
|
|
|
|
let get_uuid s =
|
|
|
|
Lwt.return
|
|
|
|
(if String.length s = 36 then
|
|
|
|
match Uuidm.of_string s with
|
|
|
|
| Some uuid -> Ok uuid
|
2021-06-09 09:48:51 +00:00
|
|
|
| None -> Error ("Bad uuid", `Bad_Request)
|
|
|
|
else Error ("Bad uuid", `Bad_Request))
|
2021-06-07 13:52:37 +00:00
|
|
|
|
2021-12-13 16:27:33 +00:00
|
|
|
let dream_svg ?status ?code ?headers body =
|
|
|
|
Dream.response ?status ?code ?headers body
|
|
|
|
|> Dream.with_header "Content-Type" "image/svg+xml"
|
|
|
|
|> Lwt.return
|
|
|
|
|
2022-02-21 11:11:06 +00:00
|
|
|
let add_routes datadir configdir =
|
2021-06-01 14:06:36 +00:00
|
|
|
let datadir_global = Dream.new_global ~name:"datadir" (fun () -> datadir) in
|
|
|
|
|
2022-02-02 22:03:16 +00:00
|
|
|
let builds req =
|
2021-06-29 14:59:08 +00:00
|
|
|
Dream.sql req Model.jobs_with_section_synopsis
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Error getting jobs"
|
2021-06-07 13:52:37 +00:00
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting jobs: %a" pp_error e))
|
|
|
|
>>= fun jobs ->
|
|
|
|
List.fold_right
|
2021-06-29 14:59:08 +00:00
|
|
|
(fun (job_id, job_name, section, synopsis) r ->
|
2021-06-07 13:52:37 +00:00
|
|
|
r >>= fun acc ->
|
2021-11-08 10:55:11 +00:00
|
|
|
Dream.sql req (Model.platforms_of_job job_id) >>= fun ps ->
|
|
|
|
List.fold_right (fun platform r ->
|
|
|
|
r >>= fun acc ->
|
|
|
|
Dream.sql req (Model.build_with_main_binary job_id platform) >>= function
|
|
|
|
| Some (build, artifact) ->
|
|
|
|
Lwt_result.return ((platform, build, artifact) :: acc)
|
|
|
|
| None ->
|
|
|
|
Log.warn (fun m -> m "Job without builds: %s" job_name);
|
|
|
|
Lwt_result.return acc)
|
|
|
|
ps (Lwt_result.return []) >>= fun platform_builds ->
|
|
|
|
let v = (job_name, synopsis, platform_builds) in
|
|
|
|
let section = Option.value ~default:"Uncategorized" section in
|
|
|
|
Lwt_result.return (Utils.String_map.add_or_create section v acc))
|
2021-06-07 13:52:37 +00:00
|
|
|
jobs
|
2021-06-29 14:59:08 +00:00
|
|
|
(Lwt_result.return Utils.String_map.empty)
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Error getting jobs"
|
2021-06-07 13:52:37 +00:00
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting jobs: %a" pp_error e))
|
|
|
|
>>= fun jobs ->
|
2022-02-02 22:03:16 +00:00
|
|
|
Views.Builds.make jobs |> string_of_html |> Dream.html |> Lwt_result.ok
|
2020-12-02 13:33:15 +00:00
|
|
|
in
|
|
|
|
|
2020-12-07 09:17:49 +00:00
|
|
|
let job req =
|
2021-06-01 14:06:36 +00:00
|
|
|
let job_name = Dream.param "job" req in
|
2021-11-08 10:55:11 +00:00
|
|
|
let platform = Dream.query "platform" req in
|
|
|
|
(Dream.sql req (Model.job_and_readme job_name) >>= fun (job_id, readme) ->
|
|
|
|
Dream.sql req (Model.builds_grouped_by_output job_id platform) >|= fun builds ->
|
|
|
|
(readme, builds))
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Error getting job"
|
2021-06-07 13:52:37 +00:00
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting job: %a" pp_error e))
|
2021-06-30 12:47:30 +00:00
|
|
|
>>= fun (readme, builds) ->
|
2022-02-03 12:27:22 +00:00
|
|
|
builds
|
2022-02-07 13:51:56 +00:00
|
|
|
|> Views.Job.make ~failed:false ~job_name ~platform ~readme
|
2022-02-03 12:27:22 +00:00
|
|
|
|> string_of_html |> Dream.html |> Lwt_result.ok
|
2021-11-17 16:39:49 +00:00
|
|
|
in
|
|
|
|
|
|
|
|
let job_with_failed req =
|
|
|
|
let job_name = Dream.param "job" req in
|
|
|
|
let platform = Dream.query "platform" req in
|
|
|
|
(Dream.sql req (Model.job_and_readme job_name) >>= fun (job_id, readme) ->
|
|
|
|
Dream.sql req (Model.builds_grouped_by_output_with_failed job_id platform) >|= fun builds ->
|
|
|
|
(readme, builds))
|
|
|
|
|> if_error "Error getting job"
|
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting job: %a" pp_error e))
|
|
|
|
>>= fun (readme, builds) ->
|
2022-02-03 12:27:22 +00:00
|
|
|
builds
|
|
|
|
|> Views.Job.make ~failed:true ~job_name ~platform ~readme
|
|
|
|
|> string_of_html |> Dream.html |> Lwt_result.ok
|
2020-12-07 09:17:49 +00:00
|
|
|
in
|
|
|
|
|
2021-06-02 12:23:40 +00:00
|
|
|
let redirect_latest req =
|
|
|
|
let job_name = Dream.param "job" req in
|
2021-11-08 10:55:11 +00:00
|
|
|
let platform = Dream.query "platform" req in
|
2021-06-02 12:23:40 +00:00
|
|
|
let path = Dream.path req |> String.concat "/" in
|
2021-06-09 09:48:51 +00:00
|
|
|
(Dream.sql req (Model.job_id job_name) >>= Model.not_found >>= fun job_id ->
|
2021-11-08 10:55:11 +00:00
|
|
|
Dream.sql req (Model.latest_successful_build_uuid job_id platform))
|
2021-06-07 13:52:37 +00:00
|
|
|
>>= Model.not_found
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Error getting job" >>= fun build ->
|
2021-06-07 13:52:37 +00:00
|
|
|
Dream.redirect req
|
2021-10-18 12:44:19 +00:00
|
|
|
(Fmt.str "/job/%s/build/%a/%s" job_name Uuidm.pp build path)
|
2021-06-07 13:52:37 +00:00
|
|
|
|> Lwt_result.ok
|
2021-06-02 12:23:40 +00:00
|
|
|
in
|
|
|
|
|
2021-09-08 09:10:30 +00:00
|
|
|
let redirect_main_binary req =
|
|
|
|
let job_name = Dream.param "job" req
|
|
|
|
and build = Dream.param "build" req in
|
|
|
|
get_uuid build >>= fun uuid ->
|
|
|
|
Dream.sql req (Model.build uuid)
|
|
|
|
|> if_error "Error getting job build"
|
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting job build: %a" pp_error e))
|
|
|
|
>>= fun (_id, build) ->
|
|
|
|
match build.Builder_db.Build.main_binary with
|
|
|
|
| None -> Lwt_result.fail ("Resource not found", `Not_Found)
|
|
|
|
| Some main_binary ->
|
|
|
|
Dream.sql req (Model.build_artifact_by_id main_binary)
|
|
|
|
|> if_error "Error getting main binary" >>= fun main_binary ->
|
|
|
|
Dream.redirect req
|
2021-10-18 12:44:19 +00:00
|
|
|
(Fmt.str "/job/%s/build/%a/f/%a" job_name Uuidm.pp uuid
|
2021-09-08 09:10:30 +00:00
|
|
|
Fpath.pp main_binary.Builder_db.filepath)
|
|
|
|
|> Lwt_result.ok
|
|
|
|
in
|
|
|
|
|
2022-01-24 13:35:59 +00:00
|
|
|
let visualization_cmd args =
|
2022-02-02 14:50:44 +00:00
|
|
|
let cmd_list = "builder-viz" :: args in
|
|
|
|
let cmd = "", Array.of_list cmd_list in
|
|
|
|
let pin =
|
|
|
|
Lwt_process.open_process_in
|
|
|
|
~stdin:`Dev_null ~stderr:`Dev_null
|
|
|
|
~timeout:15.
|
|
|
|
cmd
|
2022-01-24 12:34:04 +00:00
|
|
|
in
|
2022-02-02 14:50:44 +00:00
|
|
|
let* output = Lwt_io.read pin#stdout
|
|
|
|
and* exit_status = pin#status in
|
|
|
|
match exit_status with
|
|
|
|
| Unix.WEXITED 0 -> Lwt_result.return output
|
|
|
|
| Unix.WEXITED _ | Unix.WSIGNALED _ |Unix.WSTOPPED _ ->
|
|
|
|
let cmd_str = String.concat " " cmd_list in
|
|
|
|
`Msg (sprintf "Error when running cmd: '%s'" cmd_str)
|
|
|
|
|> Lwt_result.fail
|
2022-01-24 12:34:04 +00:00
|
|
|
in
|
2022-01-24 13:35:59 +00:00
|
|
|
|
2022-01-26 17:03:48 +00:00
|
|
|
let treemap_visualization_cmd ~debug_elf_path ~elf_size =
|
|
|
|
[ "treemap"; debug_elf_path; Int.to_string elf_size ]
|
2022-01-24 13:35:59 +00:00
|
|
|
|> visualization_cmd
|
|
|
|
in
|
|
|
|
|
|
|
|
let dependencies_visualization_cmd ~opam_switch_path =
|
|
|
|
[ "dependencies"; opam_switch_path ]
|
|
|
|
|> visualization_cmd
|
|
|
|
in
|
2022-02-01 11:43:07 +00:00
|
|
|
|
2022-01-24 13:35:59 +00:00
|
|
|
let job_build_viztreemap req =
|
2021-12-06 15:40:20 +00:00
|
|
|
let _job_name = Dream.param "job" req
|
|
|
|
and build = Dream.param "build" req in
|
|
|
|
get_uuid build >>= fun uuid ->
|
2021-12-14 12:59:35 +00:00
|
|
|
(
|
2022-01-28 10:00:51 +00:00
|
|
|
Dream.sql req (Model.build uuid) >>= fun (_id, build) ->
|
2021-12-15 16:17:56 +00:00
|
|
|
Model.not_found build.Builder_db.Build.main_binary >>= fun main_binary_id ->
|
|
|
|
Dream.sql req (Model.build_artifact_by_id main_binary_id) >>= fun main_binary ->
|
|
|
|
let debug_binary_path = Fpath.(base main_binary.Builder_db.filepath + "debug") in
|
|
|
|
(* lookup debug_binary_path artifact *)
|
|
|
|
Dream.sql req (Model.build_artifact uuid debug_binary_path) >>= fun debug_binary ->
|
|
|
|
Lwt_result.return (debug_binary, main_binary))
|
2021-12-06 15:40:20 +00:00
|
|
|
|> if_error "Error getting job build"
|
2021-12-13 19:21:43 +00:00
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting job build: %a" pp_error e))
|
2021-12-15 16:17:56 +00:00
|
|
|
>>= fun (debug_binary, main_binary) ->
|
2022-01-24 12:34:04 +00:00
|
|
|
let elf_size = main_binary.Builder_db.size in
|
2021-12-06 15:40:20 +00:00
|
|
|
let datadir = Dream.global datadir_global req in
|
2022-01-26 17:03:48 +00:00
|
|
|
let debug_elf_path = Fpath.(
|
2022-01-24 12:34:04 +00:00
|
|
|
datadir // debug_binary.Builder_db.localpath
|
|
|
|
|> to_string
|
|
|
|
) in
|
2022-02-02 14:50:44 +00:00
|
|
|
treemap_visualization_cmd ~debug_elf_path ~elf_size
|
|
|
|
|> if_error "Failed to generate treemap visualization"
|
|
|
|
>>= fun svg_html ->
|
2021-12-13 16:27:33 +00:00
|
|
|
Lwt_result.ok (Dream.html svg_html)
|
2021-12-06 15:40:20 +00:00
|
|
|
in
|
|
|
|
|
2022-01-24 13:35:59 +00:00
|
|
|
let job_build_vizdependencies req =
|
|
|
|
let _job_name = Dream.param "job" req
|
|
|
|
and build = Dream.param "build" req in
|
|
|
|
get_uuid build >>= fun uuid ->
|
2022-01-28 09:15:38 +00:00
|
|
|
let opam_switch_path = Fpath.(v "opam-switch") in
|
|
|
|
Dream.sql req (Model.build_artifact uuid opam_switch_path)
|
2022-01-24 13:35:59 +00:00
|
|
|
|> if_error "Error getting job build"
|
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting job data: %a" pp_error e))
|
|
|
|
>>= fun opam_switch ->
|
|
|
|
let datadir = Dream.global datadir_global req in
|
|
|
|
let opam_switch_path = Fpath.(
|
|
|
|
datadir // opam_switch.Builder_db.localpath
|
|
|
|
|> to_string
|
|
|
|
) in
|
2022-02-02 14:50:44 +00:00
|
|
|
dependencies_visualization_cmd ~opam_switch_path
|
|
|
|
|> if_error "Failed to generate dependencies visualization"
|
|
|
|
>>= fun svg_html ->
|
2022-01-24 13:35:59 +00:00
|
|
|
Lwt_result.ok (Dream.html svg_html)
|
|
|
|
in
|
|
|
|
|
2021-01-08 12:47:17 +00:00
|
|
|
let job_build req =
|
2021-06-01 14:06:36 +00:00
|
|
|
let job_name = Dream.param "job" req
|
|
|
|
and build = Dream.param "build" req in
|
2021-06-07 13:52:37 +00:00
|
|
|
get_uuid build >>= fun uuid ->
|
2021-12-02 13:58:44 +00:00
|
|
|
Dream.sql req (fun conn ->
|
|
|
|
Model.build uuid conn >>= fun (build_id, build) ->
|
|
|
|
Model.build_artifacts build_id conn >>= fun artifacts ->
|
|
|
|
Model.builds_with_same_input_and_same_main_binary build_id conn >>= fun same_input_same_output ->
|
|
|
|
Model.builds_with_different_input_and_same_main_binary build_id conn >>= fun different_input_same_output ->
|
|
|
|
Model.builds_with_same_input_and_different_main_binary build_id conn >>= fun same_input_different_output ->
|
|
|
|
Model.latest_successful_build build.job_id (Some build.Builder_db.Build.platform) conn >>= fun latest ->
|
|
|
|
Model.next_successful_build_different_output build_id conn >>= fun next ->
|
|
|
|
Model.previous_successful_build_different_output build_id conn >|= fun previous ->
|
2021-12-02 14:49:45 +00:00
|
|
|
(build, artifacts, same_input_same_output, different_input_same_output, same_input_different_output, latest, next, previous)
|
2021-12-02 13:58:44 +00:00
|
|
|
)
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Error getting job build"
|
2021-06-07 13:52:37 +00:00
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting job build: %a" pp_error e))
|
2021-12-02 14:49:45 +00:00
|
|
|
>>= fun (build, artifacts, same_input_same_output, different_input_same_output, same_input_different_output, latest, next, previous) ->
|
2022-02-02 22:03:16 +00:00
|
|
|
Views.Job_build.make
|
2022-01-25 13:58:25 +00:00
|
|
|
~name:job_name
|
|
|
|
~build
|
|
|
|
~artifacts
|
|
|
|
~same_input_same_output
|
|
|
|
~different_input_same_output
|
|
|
|
~same_input_different_output
|
|
|
|
~latest ~next ~previous
|
2021-07-06 10:23:29 +00:00
|
|
|
|> string_of_html |> Dream.html |> Lwt_result.ok
|
2020-12-07 09:17:49 +00:00
|
|
|
in
|
|
|
|
|
2021-01-08 12:47:17 +00:00
|
|
|
let job_build_file req =
|
2021-06-01 15:43:55 +00:00
|
|
|
let datadir = Dream.global datadir_global req in
|
2021-06-01 14:06:36 +00:00
|
|
|
let _job_name = Dream.param "job" req
|
|
|
|
and build = Dream.param "build" req
|
|
|
|
and filepath = Dream.path req |> String.concat "/" in
|
2021-06-04 10:52:35 +00:00
|
|
|
let if_none_match = Dream.header "if-none-match" req in
|
2020-12-22 12:45:54 +00:00
|
|
|
(* XXX: We don't check safety of [file]. This should be fine however since
|
|
|
|
* we don't use [file] for the filesystem but is instead used as a key for
|
|
|
|
* lookup in the data table of the 'full' file. *)
|
2021-06-07 13:52:37 +00:00
|
|
|
get_uuid build >>= fun build ->
|
2021-10-20 09:10:43 +00:00
|
|
|
Fpath.of_string filepath |> Lwt_result.lift
|
2021-06-07 13:52:37 +00:00
|
|
|
|> if_error ~status:`Not_Found "File not found" >>= fun filepath ->
|
|
|
|
Dream.sql req (Model.build_artifact build filepath)
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Error getting build artifact" >>= fun file ->
|
2021-06-07 13:52:37 +00:00
|
|
|
let etag = Base64.encode_string (Cstruct.to_string file.Builder_db.sha256) in
|
|
|
|
match if_none_match with
|
|
|
|
| Some etag' when etag = etag' ->
|
|
|
|
Dream.empty `Not_Modified |> Lwt_result.ok
|
|
|
|
| _ ->
|
|
|
|
Model.build_artifact_data datadir file
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Error getting build artifact"
|
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting build artifact data for file %a in %a: %a"
|
|
|
|
Fpath.pp file.Builder_db.filepath Fpath.pp file.Builder_db.localpath
|
|
|
|
pp_error e)) >>= fun data ->
|
2021-06-07 13:52:37 +00:00
|
|
|
let headers = [
|
|
|
|
"Content-Type", mime_lookup file.Builder_db.filepath;
|
|
|
|
"ETag", etag;
|
|
|
|
] in
|
|
|
|
Dream.respond ~headers data |> Lwt_result.ok
|
2020-12-21 10:21:10 +00:00
|
|
|
in
|
|
|
|
|
2021-09-09 16:06:08 +00:00
|
|
|
let job_build_static_file (file : [< `Console | `Script ]) req =
|
|
|
|
let datadir = Dream.global datadir_global req in
|
|
|
|
let _job_name = Dream.param "job" req
|
|
|
|
and build = Dream.param "build" req in
|
|
|
|
get_uuid build >>= fun build ->
|
|
|
|
(match file with
|
|
|
|
| `Console ->
|
|
|
|
Dream.sql req (Model.build_console_by_uuid datadir build)
|
|
|
|
| `Script ->
|
|
|
|
Dream.sql req (Model.build_script_by_uuid datadir build))
|
|
|
|
|> if_error "Error getting data"
|
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting script or console data for build %a: %a"
|
|
|
|
Uuidm.pp build pp_error e)) >>= fun data ->
|
|
|
|
let headers = [ "Content-Type", "text/plain" ] in
|
|
|
|
Dream.respond ~headers data |> Lwt_result.ok
|
|
|
|
in
|
|
|
|
|
2021-11-17 14:02:04 +00:00
|
|
|
let failed_builds req =
|
|
|
|
let platform = Dream.query "platform" req in
|
2021-11-17 15:54:13 +00:00
|
|
|
let to_int default s = Option.(value ~default (bind s int_of_string_opt)) in
|
|
|
|
let start = to_int 0 (Dream.query "start" req) in
|
|
|
|
let count = to_int 10 (Dream.query "count" req) in
|
|
|
|
Dream.sql req (Model.failed_builds ~start ~count platform)
|
2021-11-17 14:02:04 +00:00
|
|
|
|> if_error "Error getting data"
|
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error getting failed builds: %a"
|
|
|
|
pp_error e)) >>= fun builds ->
|
2021-11-17 15:54:13 +00:00
|
|
|
Views.failed_builds ~start ~count builds |> string_of_html |> Dream.html |> Lwt_result.ok
|
2021-11-17 14:02:04 +00:00
|
|
|
in
|
|
|
|
|
2021-09-14 16:39:57 +00:00
|
|
|
let job_build_tar req =
|
|
|
|
let datadir = Dream.global datadir_global req in
|
|
|
|
let _job_name = Dream.param "job" req
|
|
|
|
and build = Dream.param "build" req in
|
|
|
|
get_uuid build >>= fun build ->
|
|
|
|
Dream.sql req (Model.build build)
|
|
|
|
|> if_error "Error getting build" >>= fun (build_id, build) ->
|
|
|
|
Dream.sql req (Model.build_artifacts build_id)
|
|
|
|
|> if_error "Error getting artifacts" >>= fun artifacts ->
|
|
|
|
Ptime.diff build.finish Ptime.epoch |> Ptime.Span.to_int_s
|
|
|
|
|> Option.to_result ~none:(`Msg "bad finish time") |> Result.map Int64.of_int
|
|
|
|
|> Lwt.return |> if_error "Internal server error" >>= fun finish ->
|
|
|
|
Dream.stream ~headers:["Content-Type", "application/x-tar"]
|
|
|
|
(Dream_tar.tar_response datadir finish artifacts)
|
|
|
|
|> Lwt_result.ok
|
|
|
|
in
|
|
|
|
|
2021-01-20 21:50:35 +00:00
|
|
|
let upload req =
|
2021-06-01 14:06:36 +00:00
|
|
|
let* body = Dream.body req in
|
2021-06-07 13:52:37 +00:00
|
|
|
Builder.Asn.exec_of_cs (Cstruct.of_string body) |> Lwt.return
|
|
|
|
|> if_error ~status:`Bad_Request "Bad request"
|
|
|
|
~log:(fun e ->
|
|
|
|
Log.warn (fun m -> m "Received bad builder ASN.1: %a" pp_error e))
|
2021-06-25 16:43:47 +00:00
|
|
|
>>= fun ((({ name ; _ } : Builder.script_job), uuid, _, _, _, _, _) as exec) ->
|
2021-06-07 13:52:37 +00:00
|
|
|
Log.debug (fun m -> m "Received build %a" pp_exec exec);
|
2021-06-08 14:54:23 +00:00
|
|
|
Authorization.authorized req name
|
|
|
|
|> if_error ~status:`Forbidden "Forbidden" >>= fun () ->
|
2021-06-07 13:52:37 +00:00
|
|
|
Dream.sql req (Model.build_exists uuid)
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Internal server error"
|
2021-06-07 13:52:37 +00:00
|
|
|
~log:(fun e ->
|
|
|
|
Log.warn (fun m -> m "Error saving build %a: %a" pp_exec exec pp_error e))
|
|
|
|
>>= function
|
|
|
|
| true ->
|
|
|
|
Log.warn (fun m -> m "Build with same uuid exists: %a" pp_exec exec);
|
|
|
|
Dream.respond ~status:`Conflict
|
2021-10-18 12:44:19 +00:00
|
|
|
(Fmt.str "Build with same uuid exists: %a\n" Uuidm.pp uuid)
|
2021-06-07 13:52:37 +00:00
|
|
|
|> Lwt_result.ok
|
|
|
|
| false ->
|
|
|
|
let datadir = Dream.global datadir_global req in
|
2021-06-09 11:54:24 +00:00
|
|
|
(Lwt.return (Dream.local Authorization.user_info_local req |>
|
|
|
|
Option.to_result ~none:(`Msg "no authenticated user")) >>= fun (user_id, _) ->
|
2022-02-21 11:11:06 +00:00
|
|
|
Dream.sql req (Model.add_build ~configdir ~datadir user_id exec))
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Internal server error"
|
2021-06-07 13:52:37 +00:00
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error saving build %a: %a" pp_exec exec pp_error e))
|
|
|
|
>>= fun () -> Dream.respond "" |> Lwt_result.ok
|
2021-01-20 21:50:35 +00:00
|
|
|
in
|
|
|
|
|
2021-02-02 08:34:21 +00:00
|
|
|
let hash req =
|
2021-06-07 13:52:37 +00:00
|
|
|
Dream.query "sha256" req |> Option.to_result ~none:(`Msg "Missing sha256 query parameter") |> Lwt.return
|
|
|
|
|> if_error ~status:`Bad_Request "Bad request" >>= fun hash_hex ->
|
|
|
|
begin try Hex.to_cstruct (`Hex hash_hex) |> Lwt_result.return
|
|
|
|
with Invalid_argument e -> Lwt_result.fail (`Msg ("Bad hex: " ^ e))
|
|
|
|
end
|
|
|
|
|> if_error ~status:`Bad_Request "Bad request" >>= fun hash ->
|
2021-06-09 09:48:51 +00:00
|
|
|
Dream.sql req (Model.build_hash hash) >>= Model.not_found
|
|
|
|
|> if_error "Internal server error" >>= fun (job_name, build) ->
|
|
|
|
Dream.redirect req
|
2021-10-18 12:44:19 +00:00
|
|
|
(Fmt.str "/job/%s/build/%a/" job_name Uuidm.pp build.Builder_db.Build.uuid)
|
2021-06-09 09:48:51 +00:00
|
|
|
|> Lwt_result.ok
|
2021-02-02 08:34:21 +00:00
|
|
|
in
|
|
|
|
|
2021-11-12 13:01:20 +00:00
|
|
|
let compare_builds req =
|
2021-06-01 15:43:55 +00:00
|
|
|
let datadir = Dream.global datadir_global req in
|
2021-06-01 14:06:36 +00:00
|
|
|
let build_left = Dream.param "build_left" req in
|
|
|
|
let build_right = Dream.param "build_right" req in
|
2021-06-07 13:52:37 +00:00
|
|
|
get_uuid build_left >>= fun build_left ->
|
|
|
|
get_uuid build_right >>= fun build_right ->
|
2021-12-02 13:58:44 +00:00
|
|
|
Dream.sql req (fun conn ->
|
|
|
|
Model.build build_left conn >>= fun (_id, build_left) ->
|
|
|
|
Model.build build_right conn >>= fun (_id, build_right) ->
|
|
|
|
Model.build_artifact build_left.Builder_db.Build.uuid (Fpath.v "opam-switch") conn >>=
|
|
|
|
Model.build_artifact_data datadir >>= fun switch_left ->
|
|
|
|
Model.build_artifact build_left.Builder_db.Build.uuid (Fpath.v "build-environment") conn >>=
|
|
|
|
Model.build_artifact_data datadir >>= fun build_env_left ->
|
|
|
|
Model.build_artifact build_left.Builder_db.Build.uuid (Fpath.v "system-packages") conn >>=
|
|
|
|
Model.build_artifact_data datadir >>= fun system_packages_left ->
|
|
|
|
Model.build_artifact build_right.Builder_db.Build.uuid (Fpath.v "opam-switch") conn >>=
|
|
|
|
Model.build_artifact_data datadir >>= fun switch_right ->
|
|
|
|
Model.build_artifact build_right.Builder_db.Build.uuid (Fpath.v "build-environment") conn >>=
|
|
|
|
Model.build_artifact_data datadir >>= fun build_env_right ->
|
|
|
|
Model.build_artifact build_right.Builder_db.Build.uuid (Fpath.v "system-packages") conn >>=
|
|
|
|
Model.build_artifact_data datadir >>= fun system_packages_right ->
|
|
|
|
Model.job_name build_left.job_id conn >>= fun job_left ->
|
|
|
|
Model.job_name build_right.job_id conn >|= fun job_right ->
|
|
|
|
(job_left, job_right, build_left, build_right,
|
|
|
|
switch_left, build_env_left, system_packages_left,
|
|
|
|
switch_right, build_env_right, system_packages_right))
|
2021-06-09 09:48:51 +00:00
|
|
|
|> if_error "Internal server error"
|
2021-07-06 08:34:17 +00:00
|
|
|
>>= fun (job_left, job_right, build_left, build_right,
|
|
|
|
switch_left, build_env_left, system_packages_left,
|
|
|
|
switch_right, build_env_right, system_packages_right) ->
|
|
|
|
let env_diff = Utils.compare_env build_env_left build_env_right
|
|
|
|
and pkg_diff = Utils.compare_pkgs system_packages_left system_packages_right
|
|
|
|
in
|
2021-06-07 13:52:37 +00:00
|
|
|
let switch_left = OpamFile.SwitchExport.read_from_string switch_left
|
|
|
|
and switch_right = OpamFile.SwitchExport.read_from_string switch_right in
|
2022-02-03 12:27:22 +00:00
|
|
|
let opam_diff = Opamdiff.compare switch_left switch_right in
|
|
|
|
Views.compare_builds
|
|
|
|
~job_left ~job_right
|
|
|
|
~build_left ~build_right
|
|
|
|
~env_diff
|
|
|
|
~pkg_diff
|
|
|
|
~opam_diff
|
2021-06-07 13:52:37 +00:00
|
|
|
|> string_of_html |> Dream.html |> Lwt_result.ok
|
2021-02-10 13:43:32 +00:00
|
|
|
in
|
|
|
|
|
2021-06-09 14:26:05 +00:00
|
|
|
let upload_binary req =
|
|
|
|
let job = Dream.param "job" req in
|
2021-11-03 14:40:04 +00:00
|
|
|
let platform = Dream.param "platform" req in
|
2021-10-26 11:24:17 +00:00
|
|
|
let binary_name =
|
|
|
|
Dream.query "binary_name" req
|
|
|
|
|> Option.map Fpath.of_string
|
|
|
|
|> Option.value ~default:(Ok Fpath.(v job + "bin"))
|
|
|
|
in
|
|
|
|
if_error "Bad request" ~status:`Bad_Request (Lwt.return binary_name) >>=
|
|
|
|
fun binary_name ->
|
2021-06-09 14:26:05 +00:00
|
|
|
let* body = Dream.body req in
|
|
|
|
Authorization.authorized req job
|
|
|
|
|> if_error ~status:`Forbidden "Forbidden" >>= fun () ->
|
|
|
|
let uuid = Uuidm.v4_gen (Random.State.make_self_init ()) () in
|
|
|
|
Dream.sql req (Model.build_exists uuid)
|
|
|
|
|> if_error "Internal server error"
|
|
|
|
~log:(fun e ->
|
|
|
|
Log.warn (fun m -> m "Error saving binary %S: %a" job pp_error e))
|
|
|
|
>>= function
|
|
|
|
| true ->
|
|
|
|
Log.warn (fun m -> m "Build %S with same uuid exists: %a" job Uuidm.pp uuid);
|
|
|
|
Dream.respond ~status:`Conflict
|
2021-10-18 12:44:19 +00:00
|
|
|
(Fmt.str "Build with same uuid exists: %a\n" Uuidm.pp uuid)
|
2021-06-09 14:26:05 +00:00
|
|
|
|> Lwt_result.ok
|
|
|
|
| false ->
|
|
|
|
let datadir = Dream.global datadir_global req in
|
|
|
|
let exec =
|
|
|
|
let now = Ptime_clock.now () in
|
2021-11-03 14:40:04 +00:00
|
|
|
({ Builder.name = job ; platform ; script = "" }, uuid, [], now, now, Builder.Exited 0,
|
2021-10-26 11:24:17 +00:00
|
|
|
[ (Fpath.(v "bin" // binary_name), body) ])
|
2021-06-09 14:26:05 +00:00
|
|
|
in
|
|
|
|
(Lwt.return (Dream.local Authorization.user_info_local req |>
|
|
|
|
Option.to_result ~none:(`Msg "no authenticated user")) >>= fun (user_id, _) ->
|
2022-02-21 11:11:06 +00:00
|
|
|
Dream.sql req (Model.add_build ~configdir ~datadir user_id exec))
|
2021-06-09 14:26:05 +00:00
|
|
|
|> if_error "Internal server error"
|
|
|
|
~log:(fun e -> Log.warn (fun m -> m "Error saving build %a: %a" pp_exec exec pp_error e))
|
|
|
|
>>= fun () -> Dream.respond "" |> Lwt_result.ok
|
|
|
|
in
|
|
|
|
|
2021-06-07 13:52:37 +00:00
|
|
|
let w f req = or_error_response (f req) in
|
|
|
|
|
2021-06-01 14:06:36 +00:00
|
|
|
Dream.router [
|
2022-02-02 22:03:16 +00:00
|
|
|
Dream.get "/" (w builds);
|
2021-06-07 13:52:37 +00:00
|
|
|
Dream.get "/job/:job/" (w job);
|
2021-11-17 16:39:49 +00:00
|
|
|
Dream.get "/job/:job/failed/" (w job_with_failed);
|
2021-06-07 13:52:37 +00:00
|
|
|
Dream.get "/job/:job/build/latest/**" (w redirect_latest);
|
|
|
|
Dream.get "/job/:job/build/:build/" (w job_build);
|
|
|
|
Dream.get "/job/:job/build/:build/f/**" (w job_build_file);
|
2021-09-08 09:10:30 +00:00
|
|
|
Dream.get "/job/:job/build/:build/main-binary" (w redirect_main_binary);
|
2022-01-24 13:35:59 +00:00
|
|
|
Dream.get "/job/:job/build/:build/viztreemap" (w job_build_viztreemap);
|
|
|
|
Dream.get "/job/:job/build/:build/vizdependencies" (w job_build_vizdependencies);
|
2021-09-09 16:06:08 +00:00
|
|
|
Dream.get "/job/:job/build/:build/script" (w (job_build_static_file `Script));
|
|
|
|
Dream.get "/job/:job/build/:build/console" (w (job_build_static_file `Console));
|
2021-11-17 14:02:04 +00:00
|
|
|
Dream.get "/failed-builds/" (w failed_builds);
|
2021-09-14 16:39:57 +00:00
|
|
|
Dream.get "/job/:job/build/:build/all.tar" (w job_build_tar);
|
2021-06-07 13:52:37 +00:00
|
|
|
Dream.get "/hash" (w hash);
|
2021-11-12 13:01:20 +00:00
|
|
|
Dream.get "/compare/:build_left/:build_right/" (w compare_builds);
|
2021-06-08 14:54:23 +00:00
|
|
|
Dream.post "/upload" (Authorization.authenticate (w upload));
|
2021-11-03 14:40:04 +00:00
|
|
|
Dream.post "/job/:job/platform/:platform/upload" (Authorization.authenticate (w upload_binary));
|
2020-12-07 09:17:49 +00:00
|
|
|
]
|