aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--desmoctl.scm281
-rw-r--r--examples/apply-config-eval7
-rw-r--r--examples/build-manifest9
-rw-r--r--examples/desmorc4
5 files changed, 244 insertions, 61 deletions
diff --git a/README.md b/README.md
index 1257f5f..d3e2c39 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,10 @@ Install dependencies:
chicken-install -from-list requirements.list
+In addition to Chicken dependencies, you need to have these available in PATH:
+
+ tar
+
## Building and running
Run the tool in the interpreter:
diff --git a/desmoctl.scm b/desmoctl.scm
index 42a8c1f..cbc4cea 100644
--- a/desmoctl.scm
+++ b/desmoctl.scm
@@ -13,6 +13,7 @@
http-client
intarweb
uri-common
+ shell
)
;;;;;;;;;;;;;;;
@@ -56,17 +57,33 @@
(with-exception-handler (lambda (e) (k (catcher e)))
fn))))
+(inline-tests
+ (test-group "try-catch"
+ (test "returns fn return value when no exn thrown" 'done
+ (try-catch (lambda (e) 'caught) (lambda () 'done)))
+ (test "returns catcher return value when exn thrown" 'caught
+ (try-catch (lambda (e) 'caught) (lambda () (car '()))))))
+
(define default-cfg
`((user-cfg-path
- ,(string-append (get-environment-variable "HOME") "/" ".desmorc"))
+ . ,(string-append (get-environment-variable "HOME") "/" ".desmorc"))
(mgmt-api-url
- "http://localhost:9939")
- (mgmt-api-key "")))
+ . "http://localhost:9939")
+ (mgmt-api-key
+ . "")
+ ))
(define (is-flag-like? args)
(let ([first-char (string-ref (car args) 0)])
(eq? #\- first-char)))
+(inline-tests
+ (test-group "is-flag-like?"
+ (test "returns true for valid list" #t
+ (is-flag-like?'("-c" "desmo.scm")))
+ (test "returns false for invalid list" #f
+ (is-flag-like? '("apply")))))
+
(define (assert-parse-or-exit result)
(if (eq? (car result) 'parse-error)
(begin (print (cadr result))
@@ -76,9 +93,27 @@
(define (to-json-string datum)
(with-output-to-string (lambda () (write-json datum))))
+(inline-tests
+ (test-group "to-json-string"
+ (test "transforms alist to json object" "{\"a\":123}"
+ (to-json-string '((a . 123))))
+ (test "transforms vector to json array" "[1,2,3]"
+ (to-json-string #(1 2 3)))
+ (test "transforms bool to json bool" "true"
+ (to-json-string #t))))
+
(define (from-json-string json)
(read-json json))
+(inline-tests
+ (test-group "from-json-string"
+ (test "transforms json object to alist" '((a . 123))
+ (from-json-string "{\"a\":123}"))
+ (test "transforms json array to vector" #(1 2 3)
+ (from-json-string "[1,2,3]"))
+ (test "transforms json bool to bool" #(#t)
+ (from-json-string "[true]"))))
+
(define (alist-keys alist)
(map (lambda (elem) (symbol->string (car elem))) alist))
@@ -87,6 +122,11 @@
""
(string-append s (string-repeat s (- n 1)))))
+(inline-tests
+ (test-group "string-repeat"
+ (test "should return repeated string" "foofoofoo"
+ (string-repeat "foo" 3))))
+
(define (pretty-print-alists alists)
(define (pp alist)
(define (pp-pair pair)
@@ -99,39 +139,28 @@
"No data"
(for-each pp alists)))
+(define (string-empty? s)
+ (= (string-length s) 0))
+
(inline-tests
- (test-group "utils"
- (test-group "try-catch"
- (test "returns fn return value when no exn thrown" 'done
- (try-catch (lambda (e) 'caught) (lambda () 'done)))
- (test "returns catcher return value when exn thrown" 'caught
- (try-catch (lambda (e) 'caught) (lambda () (car '())))))
-
- (test-group "is-flag-like?"
- (test "returns true for valid list" #t
- (is-flag-like?'("-c" "desmo.scm")))
- (test "returns false for invalid list" #f
- (is-flag-like? '("apply"))))
-
- (test-group "to-json-string"
- (test "transforms alist to json object" "{\"a\":123}"
- (to-json-string '((a . 123))))
- (test "transforms vector to json array" "[1,2,3]"
- (to-json-string #(1 2 3)))
- (test "transforms bool to json bool" "true"
- (to-json-string #t)))
-
- (test-group "from-json-string"
- (test "transforms json object to alist" '((a . 123))
- (from-json-string "{\"a\":123}"))
- (test "transforms json array to vector" #(1 2 3)
- (from-json-string "[1,2,3]"))
- (test "transforms json bool to bool" #(#t)
- (from-json-string "[true]")))
+ (test-group "string-empty?"
+ (test "should return true for empty string" #t
+ (string-empty? ""))
+ (test "should return false for non-empty string" #f
+ (string-empty? "foobar"))))
- (test-group "string-repeat"
- (test "should return repeated string" "foofoofoo"
- (string-repeat "foo" 3)))))
+(define (shell-command-capture cmd)
+ (match (capture ,cmd)
+ [#!eof '(shell-err eof)]
+ [(? string-empty?) '(shell-err empty)]
+ [other `(shell-ok ,other)]))
+
+(inline-tests
+ (test-group "shell-command-capture"
+ (test "successful command should return shell-ok" '(shell-ok "123\n")
+ (shell-command-capture "echo 123"))
+ (test "failing command should return shell-err" 'shell-err
+ (car (shell-command-capture "cmd-does-not-exist")))))
;;;;;;;;;;;;;;;;;
;; API adapter ;;
@@ -160,14 +189,14 @@
#f read-string))
(define (post-prison post-fn cfg prison-json)
- (let* ((api-url (cadr (assoc 'mgmt-api-url cfg)))
- (api-key (cadr (assoc 'mgmt-api-key cfg)))
+ (let* ((api-url (cdr (assoc 'mgmt-api-url cfg)))
+ (api-key (cdr (assoc 'mgmt-api-key cfg)))
(req-url (string-append api-url "/prisons")))
(post-fn req-url api-key prison-json)))
(define (get-prisons get-fn cfg)
- (let* ((api-url (cadr (assoc 'mgmt-api-url cfg)))
- (api-key (cadr (assoc 'mgmt-api-key cfg)))
+ (let* ((api-url (cdr (assoc 'mgmt-api-url cfg)))
+ (api-key (cdr (assoc 'mgmt-api-key cfg)))
(req-url (string-append api-url "/prisons")))
(get-fn req-url api-key)))
@@ -176,12 +205,12 @@
(test-group "get-prisons"
(test "should create correct query" '("http://example.com/prisons" "bar")
(get-prisons mock-get-fn
- '((mgmt-api-url "http://example.com") (mgmt-api-key "bar")))))
+ '((mgmt-api-url . "http://example.com") (mgmt-api-key . "bar")))))
(test-group "post-prison"
(test "should create correct query" '("http://example.com/prisons" "bar" "{\"a\":\"b\"}")
(post-prison mock-post-fn
- '((mgmt-api-url "http://example.com") (mgmt-api-key "bar"))
+ '((mgmt-api-url . "http://example.com") (mgmt-api-key . "bar"))
"{\"a\":\"b\"}")))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -190,14 +219,21 @@
(define apply-usage-string
(format (string-append
- "Usage: desmoctl apply [-f path] SUBCOMMAND~%"
+ "Usage: desmoctl apply [-f|-F path] SUBCOMMAND~%"
"~%"
"Apply the config at PATH.~%"
+ "Flags:~%"
+ " -f path Path to the prison config~%"
+ " -F path Path to a Scheme program that evaluates to the prison config-%."
"~%"
"Subcommands:~%"
" help Show this help text"
)))
+(define default-apply-cfg
+ '((apply-config-eval
+ . #f)))
+
(define (parse-apply-flags opts args)
"Returns an alist of opts"
(match args
@@ -206,7 +242,11 @@
`(parse-error ,apply-usage-string)
`(parse-ok ,opts))]
[("-f" path . rest)
- (parse-apply-flags (cons `(apply-config-path ,path) opts)
+ (parse-apply-flags (cons `(apply-config-path . ,path) opts)
+ rest)]
+ [("-F" path . rest)
+ (parse-apply-flags (append `((apply-config-path . ,path)
+ (apply-config-eval . #t)) opts)
rest)]
[(? is-flag-like?)
`(parse-error ,apply-usage-string)]
@@ -215,12 +255,12 @@
[_
`(parse-error ,apply-usage-string)]))
-(define (parse-subcommand-apply args)
- (match (parse-apply-flags '() args)
+(define (parse-subcommand-apply apply-cfg args)
+ (match (parse-apply-flags apply-cfg args)
[('parse-ok 'subcmd-apply-help)
`(parse-ok subcmd-apply-help ())]
- [('parse-ok . rest)
- `(parse-ok subcmd-apply ,(car rest))]
+ [('parse-ok opts)
+ `(parse-ok subcmd-apply ,opts)]
[other
other]))
@@ -237,26 +277,35 @@
(car (parse-apply-flags '() '()))))
(test-group "parse-subcommand-apply"
- (test "returns parse-ok for help command" '(parse-ok subcmd-apply-help ())
- (parse-subcommand-apply '("help")))
- (test "returns parse-ok for -f foo.txt" '(parse-ok subcmd-apply ((apply-config-path "foo.txt")))
- (parse-subcommand-apply '("-f" "foo.txt"))))))
+ (test "returns parse-ok for help command"
+ '(parse-ok subcmd-apply-help ())
+ (parse-subcommand-apply '() '("help")))
+ (test "returns parse-ok for -f foo.txt"
+ '(parse-ok subcmd-apply ((apply-config-path . "foo.txt")
+ (apply-config-eval . #t)))
+ (parse-subcommand-apply '() '("-F" "foo.txt"))))))
(define (run-apply cfg)
- (define apply-config-content
- (let* ((path (cadr (assoc 'apply-config-path cfg)))
+ (define content-raw
+ (let* ((path (or (cdr (assoc 'apply-config-path cfg))
+ (begin (print "Error: no path to apply config supplied")
+ (exit 1))))
(catcher (lambda (e)
- (print (format "Error: no user config found at ~A" path))
+ (print (format "Error: no apply config found at ~A" path))
(exit 1)))
(open-apply-cfg (lambda () (read (open-input-file path)))))
(try-catch catcher open-apply-cfg)))
+ (define content (if (cdr (assoc 'apply-config-eval cfg))
+ (eval content-raw)
+ content-raw))
+
(if *debug?*
(begin
(debug-print "apply cfg:")
- (pretty-print apply-config-content)))
+ (pretty-print content)))
- (define apply-lst (vector->list apply-config-content))
+ (define apply-lst (vector->list content))
(print (format "Applying all defined prisons (~A)..." (length apply-lst)))
@@ -297,6 +346,117 @@
(pretty-print-alists prisons))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Parse and eval "build" subcommand ;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define default-build-cfg
+ '((build-manifest-path
+ . "desmo.manifest")
+ (build-manifest-eval
+ . #f)
+ (build-archive-path
+ . "desmo_archive.txz")))
+
+(define build-usage-string
+ (format (string-append
+ "Usage: desmoctl build [-f|-F path] [-o path] SUBCOMMAND~%"
+ "~%"
+ "Build a desmo workload archive according to the build manifest.~%"
+ "~%"
+ "Flags:~%"
+ " -f path Path to the build manifest~%"
+ " -F path Path to a Scheme program that evaluates to the build manifest-%"
+ " -o path Output archive path"
+ "~%"
+ "Subcommands:~%"
+ " help Show this help text"
+ )))
+
+(define (parse-build-flags opts args)
+ "Returns an alist of opts"
+ (match args
+ [()
+ (if (null? opts)
+ `(parse-error ,build-usage-string)
+ `(parse-ok ,opts))]
+ [("-f" path . rest)
+ (parse-build-flags (cons `(build-manifest-path . ,path) opts)
+ rest)]
+ [("-F" path . rest)
+ (parse-build-flags (append `((build-manifest-eval . ,path)
+ (build-manifest-eval . #t)) opts)
+ rest)]
+ [("-o" path . rest)
+ (parse-build-flags (cons `(build-archive-path . ,path) opts)
+ rest)]
+ [(? is-flag-like?)
+ `(parse-error ,build-usage-string)]
+ [("help")
+ `(parse-ok subcmd-build-help)]
+ [_
+ `(parse-error ,build-usage-string)]))
+
+(define (parse-subcommand-build build-cfg args)
+ (match (parse-build-flags build-cfg args)
+ [('parse-ok 'subcmd-build-help)
+ `(parse-ok subcmd-build-help ())]
+ [('parse-ok opts)
+ `(parse-ok subcmd-build ,opts)]
+ [other
+ other]))
+
+(define (run-build cfg)
+ (print "Reading build manifest...")
+
+ (define manifest-path
+ (cdr (or (assoc 'build-manifest-path cfg)
+ (begin (print "Error: no path to build manifest supplied")
+ (exit 1)))))
+
+ (define content-raw
+ (let* ((catcher (lambda (e)
+ (print (format "Error: no build manifest found at ~A" manifest-path))
+ (exit 1)))
+ (open-build-manifest (lambda () (read (open-input-file manifest-path)))))
+ (try-catch catcher open-build-manifest)))
+
+ (define content (if (cdr (assoc 'build-manifest-eval cfg))
+ (eval content-raw)
+ content-raw))
+
+ (if *debug?*
+ (begin (debug-print "manifest content:")
+ (pretty-print content)))
+
+ (define files-list
+ (let ((pair (assoc 'files content)))
+ (if pair
+ (vector->list (cdr pair))
+ '())))
+
+ (define manifest-json
+ (to-json-string content))
+
+ (debug-print (format "manifest-json: ~A" manifest-json))
+
+ (define archive-path (cdr (assoc 'build-archive-path cfg)))
+
+ (define tar-cmd
+ (apply string-append "2>&1 tar cvf " archive-path " " manifest-path " " files-list))
+
+ (debug-print "tar-cmd:")
+ (debug-print tar-cmd)
+
+ (print (format "Building archive ~A..." archive-path))
+
+ (define tar-output (shell-command-capture tar-cmd))
+
+ (match (car tar-output)
+ ['shell-ok (print (format "Archive ~A built." archive-path))]
+ [other (print (format "Failed to build archive ~A." archive-path))
+ (exit 1)]))
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Parse flags and subcommand ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -336,12 +496,12 @@
(define (parse-subcommand args)
(match args
- [("build")
- '(parse-ok subcmd-build ())]
+ [("build" . rest)
+ (parse-subcommand-build default-build-cfg rest)]
[("push")
'(parse-ok subcmd-push ())]
[("apply" . rest)
- (parse-subcommand-apply rest)]
+ (parse-subcommand-apply default-apply-cfg rest)]
[("status")
'(parse-ok subcmd-status ())]
[("logs")
@@ -373,6 +533,9 @@
(define (eval-subcommand subcmd cfg)
(match subcmd
+ ['subcmd-build (run-build cfg)]
+ ['subcmd-push (print "TODO push")
+ (exit 1)]
['subcmd-apply (run-apply cfg)]
['subcmd-status (run-status cfg)]
['subcmd-logs (print "TODO logs")
@@ -399,7 +562,7 @@
(cadr top-level-flags-parse-result))
(define user-cfg
- (let* ((path (cadr (assoc 'user-cfg-path cfg-with-flags)))
+ (let* ((path (cdr (assoc 'user-cfg-path cfg-with-flags)))
(catcher (lambda (e)
(print (format "Note: no user config found at ~A" path))
'()))
diff --git a/examples/apply-config-eval b/examples/apply-config-eval
new file mode 100644
index 0000000..80678cb
--- /dev/null
+++ b/examples/apply-config-eval
@@ -0,0 +1,7 @@
+(let ((replicas 9))
+
+ `#(((name . "foobar")
+ (base . "14.0-RELEASE-base")
+ (packages . #())
+ (commands . #())
+ (replicas . ,(+ 1 replicas)))))
diff --git a/examples/build-manifest b/examples/build-manifest
new file mode 100644
index 0000000..d0c0e56
--- /dev/null
+++ b/examples/build-manifest
@@ -0,0 +1,9 @@
+((packages
+ . #("nginx"))
+ (files
+ . #("nginx.conf"))
+ (build-commands
+ . #("sysrc nginx_enable=YES"))
+ (start-command
+ . "nginx -c nginx.conf"))
+
diff --git a/examples/desmorc b/examples/desmorc
index 53ff809..9841c81 100644
--- a/examples/desmorc
+++ b/examples/desmorc
@@ -1,2 +1,2 @@
-((mgmt-api-url "http://localhost:9939")
- (mgmt-api-key "foobar"))
+((mgmt-api-url . "http://localhost:9939")
+ (mgmt-api-key . "foobar"))