add tags to jobs: section and synopsis, inferred from the latest successful build

This commit is contained in:
Robur 2021-06-29 14:59:08 +00:00
parent 0d1b00b13c
commit e45497e97c
10 changed files with 400 additions and 50 deletions

View file

@ -120,5 +120,6 @@ let () =
actions (module M20210608); actions (module M20210608);
actions (module M20210609); actions (module M20210609);
actions (module M20210625); actions (module M20210625);
actions (module M20210629);
]) ])
|> Cmdliner.Term.exit |> Cmdliner.Term.exit

View file

@ -1,4 +1,4 @@
(executable (executable
(public_name builder-migrations) (public_name builder-migrations)
(name builder_migrations) (name builder_migrations)
(libraries builder_db caqti caqti-driver-sqlite3 caqti.blocking cmdliner logs logs.cli logs.fmt )) (libraries builder_db caqti caqti-driver-sqlite3 caqti.blocking cmdliner logs logs.cli logs.fmt opam-format bos))

159
bin/migrations/m20210629.ml Normal file
View file

@ -0,0 +1,159 @@
let new_version = 9L and old_version = 8L
let identifier = "2021-06-29"
let migrate_doc = "add tag and job_tag table"
let rollback_doc = "remove tag and job tag table"
let tag =
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE tag (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
tag VARCHAR(255) NOT NULL UNIQUE
)
|}
let job_tag =
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE job_tag (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
tag INTEGER NOT NULL,
value VARCHAR(255) NOT NULL,
job INTEGER NOT NULL,
FOREIGN KEY(job) REFERENCES job(id),
FOREIGN KEY(tag) REFERENCES tag(id),
UNIQUE(tag, job)
)
|}
let jobs =
Caqti_request.collect
Caqti_type.unit
Builder_db.Rep.id
"SELECT id FROM job"
let latest_successful_build =
Caqti_request.find
Builder_db.Rep.id
Builder_db.Rep.id
{| SELECT b.id
FROM build b
WHERE b.job = ? AND b.result_kind = 0 AND b.result_code = 0
ORDER BY b.start_d DESC, b.start_ps DESC
LIMIT 1
|}
let build_artifacts =
Caqti_request.collect
Builder_db.Rep.id
Caqti_type.(tup2 Builder_db.Rep.fpath Builder_db.Rep.fpath)
{| SELECT a.filepath, a.localpath
FROM build_artifact a
WHERE a.build = ?
|}
let infer_section_and_synopsis artifacts =
let opam_switch =
match List.find_opt (fun (p, _) -> String.equal (Fpath.basename p) "opam-switch") artifacts with
| None -> None
| Some (_, data) -> Some (OpamFile.SwitchExport.read_from_string data)
in
let infer_synopsis switch =
let root = switch.OpamFile.SwitchExport.selections.OpamTypes.sel_roots in
if OpamPackage.Set.cardinal root <> 1 then
None
else
let root = OpamPackage.Set.choose root in
match OpamPackage.Name.Map.find_opt root.OpamPackage.name switch.OpamFile.SwitchExport.overlays with
| None -> None
| Some opam -> OpamFile.OPAM.synopsis opam
in
let infer_section_from_packages switch =
let influx = OpamPackage.Name.of_string "metrics-influx" in
if OpamPackage.Set.exists (fun p -> OpamPackage.Name.equal p.OpamPackage.name influx)
switch.OpamFile.SwitchExport.selections.OpamTypes.sel_installed
then
"Unikernel (monitoring)"
else
"Unikernel"
in
let infer_section_from_extension =
match List.find_opt (fun (p, _) -> Fpath.(is_prefix (v "bin/") p)) artifacts with
| None -> None
| Some (p, _) ->
match Fpath.get_ext p with
| ".deb" -> Some "Debian Package"
| ".txz" -> Some "FreeBSD Package"
| _ -> None
in
match opam_switch with
| None -> None, None
| Some opam_switch ->
let section =
match infer_section_from_extension with
| Some x -> x
| None -> infer_section_from_packages opam_switch
in
Some section, infer_synopsis opam_switch
let remove_tag =
Caqti_request.exec
Caqti_type.unit
"DROP TABLE tag"
let remove_job_tag =
Caqti_request.exec
Caqti_type.unit
"DROP TABLE job_tag"
let insert_tag =
Caqti_request.exec
Caqti_type.string
"INSERT INTO tag (tag) VALUES (?)"
let insert_job_tag =
Caqti_request.exec
Caqti_type.(tup3 Builder_db.Rep.id string Builder_db.Rep.id)
"INSERT INTO job_tag (tag, value, job) VALUES (?, ?, ?)"
let find_tag =
Caqti_request.find
Caqti_type.string
Builder_db.Rep.id
"SELECT id FROM tag where tag = ?"
open Rresult.R.Infix
let migrate datadir (module Db : Caqti_blocking.CONNECTION) =
Grej.check_version ~user_version:old_version (module Db) >>= fun () ->
Db.exec tag () >>= fun () ->
Db.exec job_tag () >>= fun () ->
Db.exec insert_tag "section" >>= fun () ->
Db.exec insert_tag "synopsis" >>= fun () ->
Db.find find_tag "section" >>= fun section_id ->
Db.find find_tag "synopsis" >>= fun synopsis_id ->
Db.collect_list jobs () >>= fun jobs ->
Grej.list_iter_result (fun job ->
Db.find latest_successful_build job >>= fun build ->
Db.collect_list build_artifacts build >>= fun artifacts ->
List.fold_left (fun acc (fpath, lpath) ->
acc >>= fun acc ->
Bos.OS.File.read Fpath.(append datadir lpath) >>= fun data ->
Ok ((fpath, data) :: acc))
(Ok [])
artifacts >>= fun files ->
let sec_syn = infer_section_and_synopsis files in
(match fst sec_syn with None -> Ok () | Some s -> Db.exec insert_job_tag (section_id, s, job)) >>= fun () ->
(match snd sec_syn with None -> Ok () | Some s -> Db.exec insert_job_tag (synopsis_id, s, job)))
jobs >>= fun () ->
Db.exec (Grej.set_version new_version) ()
let rollback _datadir (module Db : Caqti_blocking.CONNECTION) =
Grej.check_version ~user_version:new_version (module Db) >>= fun () ->
Db.exec remove_tag () >>= fun () ->
Db.exec remove_job_tag () >>= fun () ->
Db.exec (Grej.set_version old_version) ()

View file

@ -4,7 +4,7 @@ open Rep
let application_id = 1234839235l let application_id = 1234839235l
(* Please update this when making changes! *) (* Please update this when making changes! *)
let current_version = 8L let current_version = 9L
type id = Rep.id type id = Rep.id
@ -77,6 +77,18 @@ module Job = struct
Caqti_type.(tup2 id string) Caqti_type.(tup2 id string)
"SELECT id, name FROM job ORDER BY name ASC" "SELECT id, name FROM job ORDER BY name ASC"
let get_all_with_section_synopsis =
Caqti_request.collect
Caqti_type.unit
Caqti_type.(tup4 id string (option string) (option string))
{| SELECT j.id, j.name, section.value, synopsis.value
FROM job j, tag section_tag, tag synopsis_tag
LEFT JOIN job_tag section ON section.job = j.id AND section.tag = section_tag.id
LEFT JOIN job_tag synopsis ON synopsis.job = j.id AND synopsis.tag = synopsis_tag.id
WHERE section_tag.tag = 'section' AND synopsis_tag.tag = 'synopsis'
ORDER BY section.value, j.name ASC
|}
let try_add = let try_add =
Caqti_request.exec Caqti_request.exec
Caqti_type.string Caqti_type.string
@ -88,6 +100,72 @@ module Job = struct
"DELETE FROM job WHERE id = ?" "DELETE FROM job WHERE id = ?"
end end
module Tag = struct
let migrate =
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE tag (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
tag VARCHAR(255) NOT NULL UNIQUE
)
|}
let rollback =
Caqti_request.exec
Caqti_type.unit
"DROP TABLE IF EXISTS tag"
let get =
Caqti_request.find
id
Caqti_type.string
"SELECT tag FROM tag WHERE id = ?"
let get_id_by_name =
Caqti_request.find
Caqti_type.string
id
"SELECT id FROM tag WHERE tag = ?"
let try_add =
Caqti_request.exec
Caqti_type.string
"INSERT OR IGNORE INTO tag (tag) VALUES (?)"
end
module Job_tag = struct
let migrate =
Caqti_request.exec
Caqti_type.unit
{| CREATE TABLE job_tag (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
tag INTEGER NOT NULL,
value VARCHAR(255) NOT NULL,
job INTEGER NOT NULL,
FOREIGN KEY(job) REFERENCES job(id),
FOREIGN KEY(tag) REFERENCES tag(id),
UNIQUE(tag, job)
)
|}
let rollback =
Caqti_request.exec
Caqti_type.unit
"DROP TABLE IF EXISTS job_tag"
let add =
Caqti_request.exec
Caqti_type.(tup3 id string id)
"INSERT INTO job_tag (tag, value, job) VALUES (?, ?, ?)"
let get_value =
Caqti_request.find
Caqti_type.(tup2 id id)
Caqti_type.string
"SELECT value FROM job_tag WHERE tag = ? AND job = ?"
end
module Build_artifact = struct module Build_artifact = struct
let migrate = let migrate =
Caqti_request.exec Caqti_request.exec
@ -522,6 +600,8 @@ let migrate = [
Build_artifact.migrate; Build_artifact.migrate;
User.migrate; User.migrate;
Access_list.migrate; Access_list.migrate;
Tag.migrate;
Job_tag.migrate;
Caqti_request.exec Caqti_type.unit Caqti_request.exec Caqti_type.unit
"CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)"; "CREATE INDEX idx_build_job_start ON build(job, start_d DESC, start_ps DESC)";
set_current_version; set_current_version;
@ -529,6 +609,8 @@ let migrate = [
] ]
let rollback = [ let rollback = [
Job_tag.rollback;
Tag.rollback;
Access_list.rollback; Access_list.rollback;
User.rollback; User.rollback;
Build_artifact.rollback; Build_artifact.rollback;

View file

@ -57,12 +57,38 @@ module Job : sig
(string, id, [< `Many | `One | `Zero > `One `Zero ]) Caqti_request.t (string, id, [< `Many | `One | `Zero > `One `Zero ]) Caqti_request.t
val get_all : val get_all :
(unit, id * string, [ `Many | `One | `Zero ]) Caqti_request.t (unit, id * string, [ `Many | `One | `Zero ]) Caqti_request.t
val get_all_with_section_synopsis :
(unit, id * string * string option * string option, [ `Many | `One | `Zero ]) Caqti_request.t
val try_add : val try_add :
(string, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t (string, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val remove : val remove :
(id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t (id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
end end
module Tag : sig
val migrate :
(unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val rollback :
(unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val get :
(id, string, [< `Many | `One | `Zero > `One ]) Caqti_request.t
val get_id_by_name :
(string, id, [< `Many | `One | `Zero > `One ]) Caqti_request.t
val try_add :
(string, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
end
module Job_tag : sig
val migrate :
(unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val rollback :
(unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val add :
(id * string * id, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t
val get_value :
(id * id, string, [< `Many | `One | `Zero > `One ]) Caqti_request.t
end
module Build_artifact : sig module Build_artifact : sig
val migrate : val migrate :
(unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t (unit, unit, [< `Many | `One | `Zero > `Zero ]) Caqti_request.t

View file

@ -85,21 +85,24 @@ let add_routes datadir =
let datadir_global = Dream.new_global ~name:"datadir" (fun () -> datadir) in let datadir_global = Dream.new_global ~name:"datadir" (fun () -> datadir) in
let builder req = let builder req =
Dream.sql req Model.jobs (* TODO filter unsuccessful builds, ?failed=true *)
Dream.sql req Model.jobs_with_section_synopsis
|> if_error "Error getting jobs" |> if_error "Error getting jobs"
~log:(fun e -> Log.warn (fun m -> m "Error getting jobs: %a" pp_error e)) ~log:(fun e -> Log.warn (fun m -> m "Error getting jobs: %a" pp_error e))
>>= fun jobs -> >>= fun jobs ->
List.fold_right List.fold_right
(fun (job_id, job_name) r -> (fun (job_id, job_name, section, synopsis) r ->
r >>= fun acc -> r >>= fun acc ->
Dream.sql req (Model.build_meta job_id) >>= function Dream.sql req (Model.build_meta job_id) >>= function
| Some (latest_build, latest_artifact) -> | Some (latest_build, latest_artifact) ->
Lwt_result.return ((job_name, latest_build, latest_artifact) :: acc) let v = (job_name, synopsis, latest_build, latest_artifact) in
let section = Option.value ~default:"Failed" section in
Lwt_result.return (Utils.String_map.add_or_create section v acc)
| None -> | None ->
Log.warn (fun m -> m "Job without builds: %s" job_name); Log.warn (fun m -> m "Job without builds: %s" job_name);
Lwt_result.return acc) Lwt_result.return acc)
jobs jobs
(Lwt_result.return []) (Lwt_result.return Utils.String_map.empty)
|> if_error "Error getting jobs" |> if_error "Error getting jobs"
~log:(fun e -> Log.warn (fun m -> m "Error getting jobs: %a" pp_error e)) ~log:(fun e -> Log.warn (fun m -> m "Error getting jobs: %a" pp_error e))
>>= fun jobs -> >>= fun jobs ->

View file

@ -88,6 +88,9 @@ let job job (module Db : CONN) =
let jobs (module Db : CONN) = let jobs (module Db : CONN) =
Db.collect_list Builder_db.Job.get_all () Db.collect_list Builder_db.Job.get_all ()
let jobs_with_section_synopsis (module Db : CONN) =
Db.collect_list Builder_db.Job.get_all_with_section_synopsis ()
let job_name id (module Db : CONN) = let job_name id (module Db : CONN) =
Db.find Builder_db.Job.get id Db.find Builder_db.Job.get id
@ -185,10 +188,54 @@ let commit_files datadir staging_dir job_name uuid =
Lwt.return (Bos.OS.Dir.create job_dir) >>= fun _ -> Lwt.return (Bos.OS.Dir.create job_dir) >>= fun _ ->
Lwt.return (Bos.OS.Path.move staging_dir dest) Lwt.return (Bos.OS.Path.move staging_dir dest)
let infer_section_and_synopsis artifacts =
let opam_switch =
match List.find_opt (fun (p, _) -> String.equal (Fpath.basename p) "opam-switch") artifacts with
| None -> None
| Some (_, data) -> Some (OpamFile.SwitchExport.read_from_string data)
in
let infer_synopsis switch =
let root = switch.OpamFile.SwitchExport.selections.OpamTypes.sel_roots in
if OpamPackage.Set.cardinal root <> 1 then
None
else
let root = OpamPackage.Set.choose root in
match OpamPackage.Name.Map.find_opt root.OpamPackage.name switch.OpamFile.SwitchExport.overlays with
| None -> None
| Some opam -> OpamFile.OPAM.synopsis opam
in
let infer_section_from_packages switch =
let influx = OpamPackage.Name.of_string "metrics-influx" in
if OpamPackage.Set.exists (fun p -> OpamPackage.Name.equal p.OpamPackage.name influx)
switch.OpamFile.SwitchExport.selections.OpamTypes.sel_installed
then
"Unikernel (monitoring)"
else
"Unikernel"
in
let infer_section_from_extension =
match List.find_opt (fun (p, _) -> Fpath.(is_prefix (v "bin/") p)) artifacts with
| None -> None
| Some (p, _) ->
match Fpath.get_ext p with
| ".deb" -> Some "Debian Package"
| ".txz" -> Some "FreeBSD Package"
| _ -> None
in
match opam_switch with
| None -> None, None
| Some opam_switch ->
let section =
match infer_section_from_extension with
| Some x -> x
| None -> infer_section_from_packages opam_switch
in
Some section, infer_synopsis opam_switch
let add_build let add_build
datadir datadir
user_id user_id
(((job : Builder.script_job), uuid, console, start, finish, result, _) as exec) (((job : Builder.script_job), uuid, console, start, finish, result, raw_artifacts) as exec)
(module Db : CONN) = (module Db : CONN) =
let open Builder_db in let open Builder_db in
let job_name = job.Builder.name in let job_name = job.Builder.name in
@ -209,10 +256,23 @@ let add_build
Db.exec Job.try_add job_name >>= fun () -> Db.exec Job.try_add job_name >>= fun () ->
Db.find_opt Job.get_id_by_name job_name >>= fun job_id -> Db.find_opt Job.get_id_by_name job_name >>= fun job_id ->
Lwt.return (Option.to_result ~none:(`Msg "No such job id") job_id) >>= fun job_id -> Lwt.return (Option.to_result ~none:(`Msg "No such job id") job_id) >>= fun job_id ->
let section_tag = "section" in
Db.exec Tag.try_add section_tag >>= fun () ->
Db.find Tag.get_id_by_name section_tag >>= fun section_id ->
let synopsis_tag = "synopsis" in
Db.exec Tag.try_add synopsis_tag >>= fun () ->
Db.find Tag.get_id_by_name synopsis_tag >>= fun synopsis_id ->
Db.exec Build.add { Build.uuid; start; finish; result; Db.exec Build.add { Build.uuid; start; finish; result;
console; script = job.Builder.script; console; script = job.Builder.script;
main_binary = None; user_id; job_id } >>= fun () -> main_binary = None; user_id; job_id } >>= fun () ->
Db.find last_insert_rowid () >>= fun id -> Db.find last_insert_rowid () >>= fun id ->
let sec_syn = infer_section_and_synopsis raw_artifacts in
(match fst sec_syn with
| None -> Lwt_result.return ()
| Some section_v -> Db.exec Job_tag.add (section_id, section_v, id)) >>= fun () ->
(match snd sec_syn with
| None -> Lwt_result.return ()
| Some synopsis_v -> Db.exec Job_tag.add (synopsis_id, synopsis_v, id)) >>= fun () ->
List.fold_left List.fold_left
(fun r file -> (fun r file ->
r >>= fun () -> r >>= fun () ->

View file

@ -51,6 +51,9 @@ val job_id : string -> Caqti_lwt.connection ->
val jobs : Caqti_lwt.connection -> val jobs : Caqti_lwt.connection ->
((Builder_db.id * string) list, [> Caqti_error.call_or_retrieve ]) result Lwt.t ((Builder_db.id * string) list, [> Caqti_error.call_or_retrieve ]) result Lwt.t
val jobs_with_section_synopsis : Caqti_lwt.connection ->
((Builder_db.id * string * string option * string option) list, [> Caqti_error.call_or_retrieve ]) result Lwt.t
val job_name : Builder_db.id -> Caqti_lwt.connection -> val job_name : Builder_db.id -> Caqti_lwt.connection ->
(string, [> Caqti_error.call_or_retrieve ]) result Lwt.t (string, [> Caqti_error.call_or_retrieve ]) result Lwt.t

8
lib/utils.ml Normal file
View file

@ -0,0 +1,8 @@
module String_map = struct
include Map.Make(String)
let add_or_create key v t=
update key (function None -> Some [ v ] | Some xs -> Some (v :: xs)) t
end

View file

@ -95,49 +95,57 @@ let artifact ?(basename=false) job_name build { Builder_db.filepath; localpath =
let builder jobs = let builder section_job_map =
layout ~title:"Builder Web" layout ~title:"Reproducible OPAM builds"
[ h1 [txt "Builder web"]; ([ h1 [txt "Introduction to reproducible OPAM builds"];
form ~a:[a_action "/hash"; a_method `Get] p [ txt "This website offers binary MirageOS unikernels and supplementary OS packages." ];
[ p [ txt {|Following is a list of jobs that are built daily. A persistent link to the latest successful build is available as /job/*jobname*/build/latest/. You can also look up a binary by the resulting SHA 256. All builds can be reproduced with |} ;
label [ a ~a:[a_href "https://github.com/roburio/orb/"] [txt "orb"];
txt "Search artifact by SHA256"; txt "."
br (); ];
input ~a:[ form ~a:[a_action "/hash"; a_method `Get]
a_input_type `Search; [
a_id "sha256"; label [
a_name "sha256"; txt "Search artifact by SHA256";
] (); br ();
]; input ~a:[
input ~a:[ a_input_type `Search;
a_input_type `Submit; a_id "sha256";
a_value "Search"; a_name "sha256";
] (); ] ();
]; ];
p [ input ~a:[
txtf "We have currently %d jobs." a_input_type `Submit;
(List.length jobs); a_value "Search";
]; ] ();
ul (List.map (fun (job_name, latest_build, latest_artifact) -> ];
li ([ ] @
a ~a:[a_href ("job/" ^ job_name ^ "/")] Utils.String_map.fold (fun section jobs acc ->
[txt job_name]; acc @ [
txt " "; h2 [ txt section ];
check_icon latest_build.Builder_db.Build.Meta.result; ul (List.map (fun (job_name, synopsis, latest_build, latest_artifact) ->
br (); li ([
a ~a:[a_href (Fmt.strf "job/%s/build/%a/" job_name Uuidm.pp a ~a:[a_href ("job/" ^ job_name ^ "/")]
latest_build.Builder_db.Build.Meta.uuid)] [txt job_name];
[txtf "%a" (Ptime.pp_human ()) latest_build.Builder_db.Build.Meta.start]; txt " ";
txt " "; check_icon latest_build.Builder_db.Build.Meta.result;
] @ match latest_artifact with br ();
| Some main_binary -> txt (Option.value ~default:"" synopsis);
artifact ~basename:true job_name latest_build main_binary br ();
| None -> a ~a:[a_href (Fmt.strf "job/%s/build/%a/" job_name Uuidm.pp
[ latest_build.Builder_db.Build.Meta.uuid)]
txtf "Build failed"; [txtf "%a" (Ptime.pp_human ()) latest_build.Builder_db.Build.Meta.start];
])) txt " ";
jobs); ] @ match latest_artifact with
] | Some main_binary ->
artifact ~basename:true job_name latest_build main_binary
| None ->
[
txtf "Build failed";
])) jobs)
])
section_job_map
[])
let job name builds = let job name builds =
layout ~title:(Printf.sprintf "Job %s" name) layout ~title:(Printf.sprintf "Job %s" name)