guix-patches
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[bug#27876] [PATCH] cuirass: add Hydra compatible HTTP API.


From: Mathieu Othacehe
Subject: [bug#27876] [PATCH] cuirass: add Hydra compatible HTTP API.
Date: Sun, 30 Jul 2017 12:07:59 +0200

* bin/evaluate.in (fill-job): New procedure.
(main): Use it to fill informations (nix-name, system) that will later be
added to database.
* doc/cuirass.texi (Sections)[Web API]: New section describing the HTTP API.
(Database)[Derivation]: Add system and nix_name fields.
(Database)[Builds]: Add id, status, timestamp, starttime and stoptime
fields. Remove output field.
(Database)[Outputs]: New table describing the build outputs.
* src/cuirass/base.scm (build-packages): Add new fields to build object before
adding it to database.
* src/cuirass/database.scm (db-get-build, db-get-builds): New procedures to get
a build by id from database and a list of builds using filter parameters
respectively.
* src/cuirass/http.scm (spec->json-string): Move it to utils.scm and rename it
  object->json-string.
(object->json-scm): Move it utils.scm.
(handle-*-request): New helpers procedures.
(request-parameters): New procedure to parse a request query.
(url-handler): Add new API's.
* src/cuirass/utils.scm (object->json-scm, object->json-string): Exported
procedures moved from http.scm.
* src/schema.sql (Outputs) : New table.
(Derivations): Add system and nix_name columns.
(Builds): Remove output column and add id, status, timestamp, starttime and
stoptime columns.
---

Hi,

Here's a first draft adding partial support for Hydra API in Cuirass.
It can be tested using curl or, better, with Emacs Guix.

The following elisp will change hydra url to a local running Cuirass server.

(setq guix-hydra-url "http://127.0.0.1:8080/";)

Then, it should be possible to use M-x guix-hydra-latest-builds.
The commands guix-hydra-jobsets and guix-hydra-queued-builds won't function
because the associated API's are not implemented yet.

There's a problem with /build/:build-id/log/raw API because it is trying to fork
while multiple threads are running (because of decompressed-port function). It 
seems
to work but a warning message is printed.

Thanks,

Mathieu

 bin/evaluate.in          |  18 +++-
 doc/cuirass.texi         | 242 ++++++++++++++++++++++++++++++++++++++++++++++-
 src/cuirass/base.scm     |  43 ++++++---
 src/cuirass/database.scm | 139 ++++++++++++++++++++++++---
 src/cuirass/http.scm     | 137 +++++++++++++++++++++------
 src/cuirass/utils.scm    |  22 ++++-
 src/schema.sql           |  17 +++-
 7 files changed, 558 insertions(+), 60 deletions(-)

diff --git a/bin/evaluate.in b/bin/evaluate.in
index d1d0767..858c34e 100644
--- a/bin/evaluate.in
+++ b/bin/evaluate.in
@@ -28,9 +28,22 @@ exec ${GUILE:address@hidden@} --no-auto-compile -e main -s 
"$0" "$@"
 (use-modules (cuirass)
              (ice-9 match)
              (ice-9 pretty-print)
+             (srfi srfi-26)
              (guix build utils)
+             (guix derivations)
              (guix store))
 
+(define (fill-job job eval-id)
+  "Given JOB assoc list, add EVAL-ID to it. Also process #:nix-name and
+  #:system from derivation stored in JOB."
+  (let ((drv (read-derivation-from-file
+              (assq-ref job #:derivation))))
+    ((compose
+      (cut acons #:eval-id eval-id <>)
+      (cut acons #:nix-name (derivation-name drv) <>)
+      (cut acons #:system (derivation-system drv) <>))
+     job)))
+
 (define* (main #:optional (args (command-line)))
   (match args
     ((command load-path guix-package-path cachedir specstr database)
@@ -73,8 +86,9 @@ exec ${GUILE:address@hidden@} --no-auto-compile -e main -s 
"$0" "$@"
              (pretty-print
               (map (lambda (thunk)
                      (let* ((job  (call-with-time-display thunk))
-                            ;; Keep track of SPEC id in the returned jobs.
-                            (job* (acons #:eval-id eval-id job)))
+                            ;; Fill job with informations that will later be
+                            ;; added to database.
+                            (job* (fill-job job eval-id)))
                        (db-add-derivation db job*)
                        job*))
                    thunks)
diff --git a/doc/cuirass.texi b/doc/cuirass.texi
index 12bc02f..2392e2f 100644
--- a/doc/cuirass.texi
+++ b/doc/cuirass.texi
@@ -11,6 +11,7 @@ This manual is for Cuirass version @value{VERSION}, a build 
automation
 server.
 
 Copyright @copyright{} 2016, 2017 Mathieu Lirzin
+Copyright @copyright{} 2017 Mathieu Othacehe
 
 @quotation
 Permission is granted to copy, distribute and/or modify this document
@@ -56,6 +57,7 @@ Tutorial sections:
 Reference sections:
 * Invocation::                  How to run Cuirass.
 * Database::                    About the database schema.
+* Web API::                     Description of the Web API.
 
 * Contributing::                Your help needed!
 * GNU Free Documentation License::  The license of this manual.
@@ -312,6 +314,13 @@ This field holds the @code{id} of an evaluation from the
 
 @item job_name
 This text field holds the name of the job.
+
address@hidden system
+This text field holds the system name of the derivation.
+
address@hidden nix_name
+This text field holds the name of the derivation.
+
 @end table
 
 @section Builds
@@ -322,6 +331,9 @@ that builds are not in a one to one relationship with 
derivations in
 order to keep track of non-deterministic compilations.
 
 @table @code
address@hidden id
+This is an automatically incrementing numeric identifier.
+
 @item derivation
 This text field holds the absolute name of the derivation file that
 resulted in this build.
@@ -334,11 +346,233 @@ belongs.
 @item log
 This text field holds the absolute file name of the build log file.
 
address@hidden output
-This text field holds the absolute directory name of the build output or
address@hidden if the build failed.
address@hidden status
+This integer field holds the build status of the derivation.
+
address@hidden timestamp
+This integer field holds a timestamp taken at build creation time.
+
address@hidden starttime
+This integer field holds a timestamp taken at build start time.
+Currently, it has the same value as the @code{timestamp} above.
+
address@hidden stoptime
+This integer field holds a timestamp taken at build stop time.
+Currently, it has the same value as the @code{timestamp} above.
+
address@hidden table
+
address@hidden Outputs
address@hidden outputs, database
+
+This table keep tracks for every eventual build outputs. Each build
+stored in @code{Builds} table may have zero (if it has failed), one or
+multiple outputs.
+
address@hidden @code
address@hidden build
+This field holds the @code{id} of a build from the
address@hidden table.
+
address@hidden name
+This text field holds the name of the output.
+
address@hidden path
+This text field holds the path of the output.
+
address@hidden table
+
address@hidden 
*********************************************************************
address@hidden Web API
address@hidden Web API
address@hidden web api
+
+Cuirass web API is derived from Hydra one, see 
@url{https://github.com/NixOS/hydra/blob/master/doc/manual/api.xml, Hydra API 
description}.
+
+For now only a subset of this API is implemented.
+
address@hidden API description.
address@hidden description, json
+
address@hidden Build informations.
+
+It is possible to query Cuirass web server for build informations. The
+dedicated API is @code{"/build/:build-id"} where @code{build-id} is the
+unique id associated to the build in database.
+
+For instance, querying a local Cuirass web server can be done with
address@hidden and @code{jq} to format the JSON response :
+
address@hidden
+$ curl -s "http://localhost:8080/build/2"; | jq
+
address@hidden
+  "id": 2,
+  "project": "guix",
+  "jobset": "master",
+  "job": "acpica-20150410-job",
+  "timestamp": 1501347493,
+  "starttime": 1501347493,
+  "stoptime": 1501347493,
+  "buildoutputs": @{
+    "out": @{
+      "path": "/gnu/store/6g3njhfzqpdm335s7qhvmwvs5l7gcbq1-acpica-20150410"
+    @}
+  @},
+  "system": "x86_64-linux",
+  "nixname": "acpica-20150410",
+  "buildstatus": 0,
+  "busy": 0,
+  "priority": 0,
+  "finished": 1,
+  "buildproducts": null,
+  "releasename": null,
+  "buildinputs_builds": null
address@hidden
address@hidden example
+
+If requested @code{build-id} is not known, the HTTP code 404 is
+answered with a JSON error message. For example :
+
address@hidden
+$ curl -s "http://localhost:8080/build/fff";
+
address@hidden"error" : "Build with ID fff doesn't exist."@}
address@hidden example
+
+The nominal output is a JSON object whose fields are described
+hereafter.
+
address@hidden @code
address@hidden id
+The unique build id.
+
address@hidden project
+The associated specification name, as a string.
+
address@hidden jobset
+The associated specification branch, as a string.
+
address@hidden job
+The associated job-name, as a string.
+
address@hidden timestamp
+Timestamp taken at build creation time.
+
address@hidden starttime
+Timestamp taken at build start time.
+
address@hidden stoptime
+Timestamp taken at build stop time.
+
address@hidden buildoutputs
+Build outputs as a JSON object. The keys names are referring to the
+eventual output names. The associated value is another JSON object which
+only key is @code{path}. @code{path} value is the output directory in
+store as a string.
+
address@hidden system
+System name of the build, as a string.
+
address@hidden nixname
+Derivation name, as a string.
+
address@hidden buildstatus
+Build status, as an integer. Possible values are :
+
address@hidden
+0 -> succeded
+1 -> failed
+2 -> failed dependency
+3 -> failed other
+4 -> cancelled
address@hidden example
+
address@hidden busy
+Whether the build is pending, as an integer (not implemented yet).
+
address@hidden priority
+Build priority, as an integer (not implemented yet).
+
address@hidden finished
+Build finished, as an integer (not implemented yet : always 1).
+
address@hidden buildproducts
+Build products in store as a JSON object (not implemented yet).
+
address@hidden releasename
+Unknown, not implemented yet.
+
address@hidden buildinputs_builds
+Inputs used for the build, as a JSON object (not implemented yet).
+
 @end table
 
address@hidden Build raw log output.
+
+It is possible to ask Cuirass for the raw build output log with the API
address@hidden"/build/:build-id/log/raw"} where @code{build-id} is the
+unique id associated to the build in database.
+
+The output is a raw text, for example :
+
address@hidden
+$ curl http://localhost:8080/build/2/log/raw
+
+starting phase `set-SOURCE-DATE-EPOCH'
+phase `set-SOURCE-DATE-EPOCH' succeeded after 0.0 seconds
+starting phase `set-paths'
+...
address@hidden example
+
+If requested @code{build-id} is not known, the HTTP code 404 is
+answered with a JSON error message. For example :
+
address@hidden
+$ curl -s "http://localhost:8080/build/fff/log/raw";
+
address@hidden"error" : "Build with ID fff doesn't exist."@}
address@hidden example
+
address@hidden Latest builds.
+
+The list of latest builds can be obtained with the API
address@hidden"/api/latestbuilds"}.  The output is a JSON array of
+builds. Builds are represented as in @code{"/build/:build-id"} API.
+
+This request accepts a mandatory parameter and multiple optional ones.
+
address@hidden @code
address@hidden nr
+Limit query result to nr elements. This parameter is @emph{mandatory}.
+
address@hidden project
+Filter query result to builds with the given @code{project}.
+
address@hidden jobset
+Filter query result to builds with the given @code{jobset}.
+
address@hidden job
+Filter query result to builds with the given @code{job} name.
+
address@hidden system
+Filter query result to builds with the given @code{system}.
+
address@hidden table
+
+For example, to ask for the ten last builds :
+
address@hidden
+$ curl "http://localhost:8080/api/latestbuilds?nr=10";
address@hidden example
+
+or the five last builds which project is ``guix'' and jobset ``master' :
+
address@hidden
+$ curl "http://localhost:8080/api/latestbuilds?nr=5&project=guix&jobset=master";
address@hidden example
+
+If no builds matching given parameters are found and empty JSON array is 
returned.
 
 @c *********************************************************************
 @node Contributing
@@ -346,7 +580,7 @@ This text field holds the absolute directory name of the 
build output or
 
 Everyone is welcome to contribute to Cuirass.  You can report bugs, send
 patches and share your ideas with others by sending emails the
address@hidden@@framalistes.org, mailing list}.
address@hidden@@gnu.org, mailing list}.
 
 Development is done using the Git distributed version control system.
 Thus, access to the repository is not strictly necessary.  We welcome
diff --git a/src/cuirass/base.scm b/src/cuirass/base.scm
index 326a530..15d2284 100644
--- a/src/cuirass/base.scm
+++ b/src/cuirass/base.scm
@@ -30,6 +30,7 @@
   #:use-module (ice-9 popen)
   #:use-module (ice-9 rdelim)
   #:use-module (ice-9 receive)
+  #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-19)
   #:use-module (srfi srfi-34)
   #:export (;; Procedures.
@@ -154,25 +155,41 @@ directory and the sha1 of the top level commit in this 
directory."
 
 (define (build-packages store db jobs)
   "Build JOBS and return a list of Build results."
+
+  (define hydra-build-status
+    ;; Build status as expected by hydra compatible API's.
+    '((succeeded         . 0)
+      (failed            . 1)
+      (failed-dependency . 2)
+      (failed-other      . 3)
+      (cancelled         . 4)))
+
   (define (register job)
     (let* ((name     (assq-ref job #:job-name))
            (drv      (assq-ref job #:derivation))
            (eval-id  (assq-ref job #:eval-id))
            ;; XXX: How to keep logs from several attempts?
            (log      (log-file store drv))
-           (outputs  (match (derivation-path->output-paths drv)
-                       (((names . items) ...)
-                        (filter (lambda (item)
-                                  (valid-path? store item))
-                                items)))))
-      (for-each (lambda (output)
-                  (let ((build `((#:derivation . ,drv)
-                                 (#:eval-id . ,eval-id)
-                                 (#:log . ,log)
-                                 (#:output . ,output))))
-                    (db-add-build db build)))
-                outputs)
-      (format #t "~{~A ~}\n" outputs)
+           (outputs  (filter-map (lambda (res)
+                                   (match res
+                                     ((name . path)
+                                      (and (valid-path? store path)
+                                           `(,name . ,path)))))
+                                 (derivation-path->output-paths drv)))
+           (cur-time (time-second (current-time time-utc))))
+      (let ((build `((#:derivation . ,drv)
+                     (#:eval-id . ,eval-id)
+                     (#:log . ,log)
+                     (#:status .
+                      ,(match (length outputs)
+                         (0 (assq-ref hydra-build-status 'failed))
+                         (_ (assq-ref hydra-build-status 'succeeded))))
+                     (#:outputs . ,outputs)
+                     ;;; XXX: For now, we do not know start/stop build time.
+                     (#:timestamp . ,cur-time)
+                     (#:starttime . ,cur-time)
+                     (#:stoptime . ,cur-time))))
+        (db-add-build db build))
       build))
 
   ;; Pass all the jobs at once so we benefit from as much parallelism as
diff --git a/src/cuirass/database.scm b/src/cuirass/database.scm
index 804b8c2..5f60fac 100644
--- a/src/cuirass/database.scm
+++ b/src/cuirass/database.scm
@@ -1,5 +1,6 @@
 ;;; database.scm -- store evaluation and build results
 ;;; Copyright © 2016, 2017 Mathieu Lirzin <address@hidden>
+;;; Copyright © 2017 Mathieu Othacehe <address@hidden>
 ;;;
 ;;; This file is part of Cuirass.
 ;;;
@@ -21,6 +22,7 @@
   #:use-module (cuirass utils)
   #:use-module (ice-9 match)
   #:use-module (ice-9 rdelim)
+  #:use-module (srfi srfi-1)
   #:use-module (sqlite3)
   #:export (;; Procedures.
             assq-refs
@@ -35,6 +37,8 @@
             db-add-derivation
             db-get-derivation
             db-add-build
+            db-get-build
+            db-get-builds
             read-sql-file
             read-quoted-string
             sqlite-exec
@@ -147,10 +151,12 @@ INSERT OR IGNORE INTO Specifications (repo_name, url, 
load_path, file, \
 (define (db-add-derivation db job)
   "Store a derivation result in database DB and return its ID."
   (sqlite-exec db "\
-INSERT OR IGNORE INTO Derivations (derivation, job_name, evaluation)\
-  VALUES ('~A', '~A', '~A');"
+INSERT OR IGNORE INTO Derivations (derivation, job_name, system, nix_name, 
evaluation)\
+  VALUES ('~A', '~A', '~A', '~A', '~A');"
                (assq-ref job #:derivation)
                (assq-ref job #:job-name)
+               (assq-ref job #:system)
+               (assq-ref job #:nix-name)
                (assq-ref job #:eval-id)))
 
 (define (db-get-derivation db id)
@@ -182,15 +188,126 @@ string."
             (else (loop (cons char chars)))))))
 
 (define (db-add-build db build)
-  "Store BUILD in database DB."
-  (sqlite-exec db "\
-INSERT INTO Builds (derivation, evaluation, log, output)\
-  VALUES ('~A', '~A', '~A', '~A');"
-               (assq-ref build #:derivation)
-               (assq-ref build #:eval-id)
-               (assq-ref build #:log)
-               (assq-ref build #:output))
-  (last-insert-rowid db))
+  "Store BUILD in database DB. BUILS eventual outputs are stored
+in the OUTPUTS table."
+  (let* ((build-exec
+          (sqlite-exec db "\
+INSERT INTO Builds (derivation, evaluation, log, status, timestamp, starttime, 
stoptime)\
+  VALUES ('~A', '~A', '~A', '~A', '~A', '~A', '~A');"
+                       (assq-ref build #:derivation)
+                       (assq-ref build #:eval-id)
+                       (assq-ref build #:log)
+                       (assq-ref build #:status)
+                       (assq-ref build #:timestamp)
+                       (assq-ref build #:starttime)
+                       (assq-ref build #:stoptime)))
+         (build-id (last-insert-rowid db)))
+    (for-each (lambda (output)
+                (match output
+                  ((name . path)
+                   (sqlite-exec db "\
+INSERT INTO Outputs (build, name, path) VALUES ('~A', '~A', '~A');"
+                                build-id name path))))
+              (assq-ref build #:outputs))
+    build-id))
+
+(define (db-get-outputs db build-id)
+  "Retrieve the OUTPUTS of the build identified by BUILD-ID in DB database."
+  (let loop ((rows
+              (sqlite-exec db "SELECT name, path FROM Outputs WHERE 
build='~A';"
+                           build-id))
+             (outputs '()))
+    (match rows
+      (() outputs)
+      ((#(name path)
+        . rest)
+       (loop rest
+             (cons `(,name . ((#:path . ,path)))
+                   outputs))))))
+
+(define db-build-request "\
+SELECT Builds.id, Builds.timestamp, Builds.starttime, Builds.stoptime, 
Builds.log, Builds.status,\
+Derivations.job_name, Derivations.system, Derivations.nix_name,\
+Specifications.repo_name, Specifications.branch \
+FROM Builds \
+INNER JOIN Derivations ON Builds.derivation = Derivations.derivation and 
Builds.evaluation = Derivations.evaluation \
+INNER JOIN Evaluations ON Derivations.evaluation = Evaluations.id \
+INNER JOIN Specifications ON Evaluations.specification = 
Specifications.repo_name")
+
+(define (db-format-build db build)
+  (match build
+    (#(id timestamp starttime stoptime log status job-name system
+          nix-name repo-name branch)
+       `((#:id        . ,id)
+         (#:timestamp . ,timestamp)
+         (#:starttime . ,starttime)
+         (#:stoptime  . ,stoptime)
+         (#:log       . ,log)
+         (#:status    . ,status)
+         (#:job-name  . ,job-name)
+         (#:system    . ,system)
+         (#:nix-name  . ,nix-name)
+         (#:repo-name . ,repo-name)
+         (#:outputs   . ,(db-get-outputs db id))
+         (#:branch    . ,branch)))))
+
+(define (db-get-build db id)
+  "Retrieve a build in database DB which corresponds to ID."
+  (let ((res (sqlite-exec db (string-append db-build-request
+                                            " WHERE Builds.id='~A';") id)))
+    (match res
+      ((build)
+       (db-format-build db build))
+      (() #f))))
+
+(define (db-get-builds db filters)
+  "Retrieve all builds in database DB which are matched by given FILTERS.
+FILTERS is an assoc list which possible keys are 'project | 'jobset | 'job |
+'system | 'nr."
+
+  (define (format-where-clause filters)
+    (let ((where-clause
+           (filter-map
+            (lambda (param)
+              (match param
+                (('project project)
+                 (format #f "Specifications.repo_name='~A'" project))
+                (('jobset jobset)
+                 (format #f "Specifications.branch='~A'" jobset))
+                (('job job)
+                 (format #f "Derivations.job_name='~A'" job))
+                (('system system)
+                 (format #f "Derivations.system='~A'" system))
+                (_ #f)))
+            filters)))
+      (if (> (length where-clause) 0)
+          (string-append
+           "WHERE "
+           (string-join where-clause " AND "))
+          "")))
+
+  (define (format-order-clause filters)
+    (any
+     (lambda (param)
+       (match param
+         (('nr number)
+          (format #f "ORDER BY Builds.id DESC LIMIT '~A';" number))
+         (_ #f)))
+     filters))
+
+  (let loop ((rows
+              (sqlite-exec db (string-append
+                               db-build-request
+                               " "
+                               (format-where-clause filters)
+                               " "
+                               (format-order-clause filters))))
+             (outputs '()))
+    (match rows
+      (() outputs)
+      ((row . rest)
+       (loop rest
+             (cons (db-format-build db row) outputs))))))
 
 (define (db-get-stamp db spec)
   "Return a stamp corresponding to specification SPEC in database DB."
diff --git a/src/cuirass/http.scm b/src/cuirass/http.scm
index 33cd37b..976e24c 100644
--- a/src/cuirass/http.scm
+++ b/src/cuirass/http.scm
@@ -1,5 +1,6 @@
 ;;;; http.scm -- HTTP API
 ;;; Copyright © 2016 Mathieu Lirzin <address@hidden>
+;;; Copyright © 2017 Mathieu Othacehe <address@hidden>
 ;;;
 ;;; This file is part of Cuirass.
 ;;;
@@ -19,52 +20,134 @@
 (define-module (cuirass http)
   #:use-module (cuirass database)
   #:use-module (cuirass utils)
-  #:use-module (ice-9 hash-table)
+  #:use-module (guix build utils)
+  #:use-module (guix utils)
   #:use-module (ice-9 match)
   #:use-module (json)
   #:use-module (web request)
   #:use-module (web response)
   #:use-module (web server)
   #:use-module (web uri)
-  #:export (spec->json-string
-            run-cuirass-server))
+  #:export (run-cuirass-server))
 
-;;;
-;;; JSON format.
-;;;
+(define (build->hydra-build build)
+  "Convert BUILD to an assoc list matching hydra API format."
+  `((#:id . ,(assq-ref build #:id))
+    (#:project . ,(assq-ref build #:repo-name))
+    (#:jobset . ,(assq-ref build #:branch))
+    (#:job . ,(assq-ref build #:job-name))
+    (#:timestamp . ,(assq-ref build #:timestamp))
+    (#:starttime . ,(assq-ref build #:starttime))
+    (#:stoptime . ,(assq-ref build #:stoptime))
+    (#:buildoutputs . ,(assq-ref build #:outputs))
+    (#:system . ,(assq-ref build #:system))
+    (#:nixname . ,(assq-ref build #:nix-name))
+    (#:buildstatus . ,(assq-ref build #:status))
+
+    ;; TODO: Fill the fields above with correct values.
+    (#:busy . 0)
+    (#:priority . 0)
+    (#:finished . 1)
+    (#:buildproducts . #nil)
+    (#:releasename . #nil)
+    (#:buildinputs_builds . #nil)))
+
+(define (handle-build-request db build-id)
+  "Retrieve build identified by BUILD-ID in DB and convert it to hydra
+  format. Return #f is not build was found."
+  (let ((build (db-get-build db build-id)))
+    (and=> build build->hydra-build)))
 
-(define (object->json-scm obj)
-  "Prepare OBJ for JSON usage."
-  (cond ((string? obj)  obj)
-        ((number? obj)  obj)
-        ((boolean? obj) obj)
-        ((null? obj)    obj)
-        ((symbol? obj)  (symbol->string obj))
-        ((keyword? obj) (object->json-scm (keyword->symbol obj)))
-        ((alist? obj)   (alist->hash-table (map object->json-scm obj)))
-        ((pair? obj)    (cons (object->json-scm (car obj))
-                              (object->json-scm (cdr obj))))
-        (else           (object->string obj))))
-
-(define* (spec->json-string spec #:key pretty)
-  "Return SPEC as a JSON object."
-  (scm->json-string (object->json-scm spec) #:pretty pretty))
+(define (handle-builds-request db filters)
+  "Retrieve all builds matched by FILTERS in DB and convert them to hydra
+  format."
+  (let ((builds (db-get-builds db filters)))
+    (map build->hydra-build builds)))
+
+(define (handle-log-request db build-id)
+  "Retrieve the log file of the build identified by BUILD-ID in DB. Return a
+  lambda which PORT argument is an input port from which the content of the
+  log file can be read or #f if the log file is not readable."
+  (let* ((build (db-get-build db build-id))
+         (log (assq-ref build #:log))
+         (access (and (string? log)
+                      (access? log R_OK))))
+    (and access
+         (lambda (port)
+           (dump-port (decompressed-port 'bzip2 (open-input-file log))
+                      port)))))
+
+(define (request-parameters request)
+  "Parse the REQUEST query parameters and return them under the form
+  '((parameter value) ...)."
+  (let* ((uri (request-uri request))
+         (query (uri-query uri)))
+    (and query
+         (map (lambda (param)
+                (match (string-split param #\=)
+                  ((key param)
+                   (list (string->symbol key) param))))
+              (string-split query #\&)))))
 
 
 ;;;
 ;;; Web server.
 ;;;
+;;; The api is derived from the hydra one. It is partially described here :
+;;;
+;;; https://github.com/NixOS/hydra/blob/master/doc/manual/api.xml
+;;;
 
 (define (request-path-components request)
   (split-and-decode-uri-path (uri-path (request-uri request))))
 
 (define (url-handler request body db)
+
   (define* (respond response #:key body (db db))
     (values response body db))
+
+  (define-syntax-rule (respond-json body ...)
+    (respond '((content-type . (application/json)))
+             #:body body ...))
+
+  (define-syntax-rule (respond-text body ...)
+    (respond '((content-type . (text/plain)))
+             #:body body ...))
+
+  (define-syntax-rule (respond-json-with-error error-code message)
+    (respond
+     (build-response #:headers '((content-type . (application/json)))
+                     #:code error-code)
+     #:body
+     (object->json-string
+      `((error . ,message)))))
+
+  (define (respond-build-not-found build)
+    (respond-json-with-error
+     404
+     (format #f "Build with ID ~a doesn't exist." build)))
+
   (match (request-path-components request)
     (((or "jobsets" "specifications") . rest)
-     (respond '((content-type . (application/json)))
-              #:body (spec->json-string (car (db-get-specifications db)))))
+     (respond-json (object->json-string (car (db-get-specifications db)))))
+    (("build" build)
+     (let ((hydra-build (handle-build-request db build)))
+       (if hydra-build
+           (respond-json (object->json-string hydra-build))
+           (respond-build-not-found build))))
+    (("build" build "log" "raw")
+     (let ((log-response (handle-log-request db build)))
+       (if log-response
+           (respond-text log-response)
+           (respond-build-not-found build))))
+    (("api" "latestbuilds")
+     (let* ((params (request-parameters request))
+            ;; 'nr parameter is mandatory to limit query size.
+            (valid-params? (assq-ref params 'nr)))
+       (if valid-params?
+           (respond-json (object->json-string
+                          (handle-builds-request db params)))
+           (respond-json-with-error 500 "Parameter not defined!"))))
     (_
      (respond (build-response #:code 404)
               #:body (string-append "Resource not found: "
@@ -73,6 +156,6 @@
 (define* (run-cuirass-server db #:key (port 8080))
   (format (current-error-port) "listening on port ~A~%" port)
   (run-server url-handler
-              'http                     ;server implementation
-              `(#:port ,port)           ;implementation parameters
-              db))                      ;state
+              'http
+              `(#:port ,port)
+              db))
diff --git a/src/cuirass/utils.scm b/src/cuirass/utils.scm
index d966543..a932674 100644
--- a/src/cuirass/utils.scm
+++ b/src/cuirass/utils.scm
@@ -21,9 +21,29 @@
 (define-module (cuirass utils)
   #:use-module (ice-9 match)
   #:use-module (srfi srfi-1)
-  #:export (alist?))
+  #:use-module (json)
+  #:export (alist?
+            object->json-scm
+            object->json-string))
 
 (define (alist? obj)
   "Return #t if OBJ is an alist."
   (and (list? obj)
        (every pair? obj)))
+
+(define (object->json-scm obj)
+  "Prepare OBJ for JSON usage."
+  (cond ((string? obj)  obj)
+        ((number? obj)  obj)
+        ((boolean? obj) obj)
+        ((null? obj)    obj)
+        ((symbol? obj)  (symbol->string obj))
+        ((keyword? obj) (object->json-scm (keyword->symbol obj)))
+        ((alist? obj)   (map object->json-scm obj))
+        ((pair? obj)    (cons (object->json-scm (car obj))
+                              (object->json-scm (cdr obj))))
+        (else           (object->string obj))))
+
+(define* (object->json-string object #:key pretty)
+  "Return OBJECT as a JSON object."
+  (scm->json-string (object->json-scm object) #:pretty pretty))
diff --git a/src/schema.sql b/src/schema.sql
index 329d89d..0ee428c 100644
--- a/src/schema.sql
+++ b/src/schema.sql
@@ -31,18 +31,31 @@ CREATE TABLE Derivations (
   derivation    TEXT NOT NULL,
   evaluation    INTEGER NOT NULL,
   job_name      TEXT NOT NULL,
+  system        TEXT NOT NULL,
+  nix_name      TEXT NOT NULL,
   PRIMARY KEY (derivation, evaluation),
   FOREIGN KEY (evaluation) REFERENCES Evaluations (id)
 );
 
+CREATE TABLE Outputs (
+  build INTEGER NOT NULL,
+  name TEXT NOT NULL,
+  path TEXT NOT NULL,
+  PRIMARY KEY (build, name),
+  FOREIGN KEY (build) REFERENCES Builds (id)
+);
+
 -- Builds are not in a one to one relationship with derivations in order to
 -- keep track of non deterministic compilations.
 CREATE TABLE Builds (
+  id            INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
   derivation    TEXT NOT NULL,
   evaluation    INTEGER NOT NULL,
   log           TEXT NOT NULL,
-  output        TEXT,          -- NULL if build failed
-  PRIMARY KEY (derivation, evaluation, output),
+  status        INTEGER NOT NULL,
+  timestamp     INTEGER NOT NULL,
+  starttime     INTEGER NOT NULL,
+  stoptime      INTEGER NOT NULL,
   FOREIGN KEY (derivation) REFERENCES Derivations (derivation),
   FOREIGN KEY (evaluation) REFERENCES Evaluations (id)
 );
-- 
2.13.2






reply via email to

[Prev in Thread] Current Thread [Next in Thread]