Browse code
(init)
Ed Langley authored on 31/03/2018 05:44:30
Showing 3 changed files
Showing 3 changed files
0 | 4 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,20 @@ |
1 |
+;;; -*- Mode:Lisp; Syntax:ANSI-Common-Lisp; Package: ASDF-USER -*- |
|
2 |
+(in-package :asdf-user) |
|
3 |
+ |
|
4 |
+#+sbcl |
|
5 |
+(sb-posix:setenv "CC" "clang" 1) |
|
6 |
+ |
|
7 |
+(defsystem :cloud-watcher |
|
8 |
+ :description "" |
|
9 |
+ :author "Ed L <edward@elangley.org>" |
|
10 |
+ :license "MIT" |
|
11 |
+ :depends-on (#:alexandria |
|
12 |
+ #:uiop |
|
13 |
+ #:serapeum |
|
14 |
+ #:fwoar.lisputils |
|
15 |
+ #:net.didierverna.clon |
|
16 |
+ #:cl-base64 |
|
17 |
+ #:aws-sdk/services/cloudformation |
|
18 |
+ #:should-test) |
|
19 |
+ :serial t |
|
20 |
+ :components ((:file "main"))) |
0 | 21 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,187 @@ |
1 |
+;; MIT license Edward Langley (c) 2018 |
|
2 |
+ |
|
3 |
+(defpackage :cloud-watcher.cli |
|
4 |
+ (:use :cl :net.didierverna.clon) |
|
5 |
+ (:export options |
|
6 |
+ #:*cloud-watcher-synopsis*)) |
|
7 |
+ |
|
8 |
+(defpackage :cloud-watcher.main |
|
9 |
+ (:import-from :cloud-watcher.cli #:*cloud-watcher-synopsis*) |
|
10 |
+ (:use :cl :fw.lu :alexandria :st) |
|
11 |
+ (:export main dump)) |
|
12 |
+ |
|
13 |
+(in-package :cloud-watcher.cli) |
|
14 |
+ |
|
15 |
+(defparameter *cloud-watcher-synopsis* |
|
16 |
+ (defsynopsis (:postfix "ARGS...") |
|
17 |
+ (group (:header "actions") |
|
18 |
+ (flag :short-name "p" :long-name "parameters" :description "show stack parameters") |
|
19 |
+ (flag :short-name "o" :long-name "outputs" :description "show stack outputs") |
|
20 |
+ (flag :short-name "w" :long-name "watch" :description "watch a cloudformation stack until it's done processing") |
|
21 |
+ (flag :short-name "s" :long-name "start") |
|
22 |
+ (flag :short-name "u" :long-name "update")) |
|
23 |
+ (group (:header "misc") |
|
24 |
+ (flag :long-name "help")))) |
|
25 |
+ |
|
26 |
+(in-package :cloud-watcher.main) |
|
27 |
+ |
|
28 |
+(eval-when (:execute :load-toplevel :compile-toplevel) |
|
29 |
+ (net.didierverna.clon:nickname-package)) |
|
30 |
+ |
|
31 |
+(define-condition invalid-result (error) |
|
32 |
+ ()) |
|
33 |
+ |
|
34 |
+(defun extract-list (aws-result) |
|
35 |
+ (mapcar (destructuring-lambda ((list-item-marker . items)) |
|
36 |
+ (if (string= list-item-marker "member") |
|
37 |
+ items |
|
38 |
+ (error 'invalid-result))) |
|
39 |
+ aws-result)) |
|
40 |
+ |
|
41 |
+(defclass stack () |
|
42 |
+ ((%outputs :initarg :outputs :reader outputs) |
|
43 |
+ (%capabilities :initarg :capabilities :reader capabilities) |
|
44 |
+ (%creation-time :initarg :creation-time :reader creation-time) |
|
45 |
+ (%notification-arns :initarg :notification-arns :reader notification-arns) |
|
46 |
+ (%stack-id :initarg :stack-id :reader stack-id) |
|
47 |
+ (%stack-name :initarg :stack-name :reader stack-name) |
|
48 |
+ (%description :initarg :description :reader description) |
|
49 |
+ (%stack-status :initarg :stack-status :reader stack-status) |
|
50 |
+ (%disable-rollback :initarg :disable-rollback :reader disable-rollback) |
|
51 |
+ (%tags :initarg :tags :reader tags) |
|
52 |
+ (%rollback-configuration :initarg :rollback-configuration :reader rollback-configuration) |
|
53 |
+ (%drift-information :initarg :drift-information :reader drift-information) |
|
54 |
+ (%enable-termination-protection :initarg :enable-termination-protection :reader enable-termination-protection) |
|
55 |
+ (%parameters :initarg :parameters :reader parameters))) |
|
56 |
+ |
|
57 |
+(defun find-all-indices (pred str &optional accum (start (or (car accum) 0))) |
|
58 |
+ (check-type pred function) |
|
59 |
+ (check-type str string) |
|
60 |
+ (check-type start fixnum) |
|
61 |
+ |
|
62 |
+ (tagbody start-loop |
|
63 |
+ (if (= start (length str)) |
|
64 |
+ (return-from find-all-indices (nreverse accum)) |
|
65 |
+ (let ((pos (position-if pred str :start start))) |
|
66 |
+ (if pos |
|
67 |
+ (progn (psetf accum (cons pos accum) |
|
68 |
+ start (1+ pos)) |
|
69 |
+ (go start-loop)) |
|
70 |
+ (return-from find-all-indices (nreverse accum))))))) |
|
71 |
+ |
|
72 |
+(defun decamelize (v) |
|
73 |
+ (let* ((indices (or (find-all-indices #'upper-case-p v) '(0))) |
|
74 |
+ (parts (mapcar (serapeum:op (string-downcase (subseq v _ _))) |
|
75 |
+ (if (= (car indices) 0) indices (cons 0 indices)) |
|
76 |
+ (if (/= (car indices) 0) indices (append (cdr indices) |
|
77 |
+ (list (length v))))))) |
|
78 |
+ (serapeum:string-join parts "-"))) |
|
79 |
+ |
|
80 |
+(deftest decamelize () |
|
81 |
+ (should be equal "a" (decamelize "A")) |
|
82 |
+ (should be equal "a" (decamelize "a")) |
|
83 |
+ (should be equal "outputs" (decamelize "Outputs")) |
|
84 |
+ (should be equal "outputs-outputs" (decamelize "OutputsOutputs")) |
|
85 |
+ (should be equal "a-b-c" (decamelize "ABC"))) |
|
86 |
+ |
|
87 |
+(defun transform-by-function-map (function-map key value &optional (default-transform 'identity)) |
|
88 |
+ (funcall (gethash key |
|
89 |
+ function-map |
|
90 |
+ default-transform) |
|
91 |
+ value)) |
|
92 |
+ |
|
93 |
+(defun alist-to-initargs (alist value-map) |
|
94 |
+ (mapcan (destructuring-lambda ((key . value)) |
|
95 |
+ (let* ((key (string-case:string-case (key :default key) |
|
96 |
+ ("NotificationARNs" "NotificationArns"))) |
|
97 |
+ (initarg (make-keyword (string-upcase (decamelize key))))) |
|
98 |
+ (list initarg |
|
99 |
+ (transform-by-function-map value-map |
|
100 |
+ initarg |
|
101 |
+ value)))) |
|
102 |
+ alist)) |
|
103 |
+ |
|
104 |
+(defun extract-stack (aws-result) |
|
105 |
+ (flet ((as-keyword (v) (alexandria:make-keyword (car v)))) |
|
106 |
+ (apply #'make-instance 'stack |
|
107 |
+ (alist-to-initargs aws-result |
|
108 |
+ (fw.lu:alist-string-hash-table |
|
109 |
+ `((:outputs . extract-listq) |
|
110 |
+ (:capabilities . extract-list) |
|
111 |
+ (:creation-time . car) |
|
112 |
+ (:notification-arns . car) |
|
113 |
+ (:stack-id . car) |
|
114 |
+ (:stack-name . car) |
|
115 |
+ (:description . car) |
|
116 |
+ (:stack-status . ,#'as-keyword) |
|
117 |
+ (:disable-rollback . car) |
|
118 |
+ (:tags . extract-list) |
|
119 |
+ (:rollback-configuration . car) |
|
120 |
+ (:drift-information . car) |
|
121 |
+ (:enable-termination-protection . car) |
|
122 |
+ (:parameters . extract-list))))))) |
|
123 |
+ |
|
124 |
+(defun stack-for-name (name) |
|
125 |
+ (let* ((aws-result (car (extract-list (cdar (aws/cloudformation:describe-stacks :stack-name name))))) |
|
126 |
+ (the-stack (extract-stack aws-result))) |
|
127 |
+ the-stack)) |
|
128 |
+ |
|
129 |
+(defun print-kvs (formatter stream data) |
|
130 |
+ (mapcar (destructuring-lambda (((_ k) (__ v))) |
|
131 |
+ (declare (ignore _ __)) |
|
132 |
+ (funcall formatter stream k v)) |
|
133 |
+ data)) |
|
134 |
+ |
|
135 |
+(defmacro tagged-kv-formatter (tag) |
|
136 |
+ `(formatter ,(format nil "~a ~~a ~~a~~%" tag))) |
|
137 |
+ |
|
138 |
+(defun stack-outputs (the-stack) |
|
139 |
+ (print-kvs (tagged-kv-formatter "OUTPUT") |
|
140 |
+ (outputs the-stack))) |
|
141 |
+ |
|
142 |
+(defun stack-parameters (the-stack) |
|
143 |
+ (print-kvs (tagged-kv-formatter "PARAMETERS") |
|
144 |
+ (parameters the-stack))) |
|
145 |
+ |
|
146 |
+(defun stack-parameters-main (name) |
|
147 |
+ (stack-parameters (stack-for-name name))) |
|
148 |
+ |
|
149 |
+(defun stack-outputs-main (name) |
|
150 |
+ (stack-outputs (stack-for-name name))) |
|
151 |
+ |
|
152 |
+(defun watch-stack (name) |
|
153 |
+ (format t "~&Watching ~s~2%" name) |
|
154 |
+ (let ((done? nil) |
|
155 |
+ (old-status nil)) |
|
156 |
+ (loop until done? |
|
157 |
+ for the-stack = (stack-for-name name) |
|
158 |
+ do |
|
159 |
+ (unless old-status |
|
160 |
+ (format t "~& PARAMETERS ~%============~%") |
|
161 |
+ (stack-parameters the-stack) |
|
162 |
+ (format t "~&============~2%")) |
|
163 |
+ |
|
164 |
+ (unless (eql old-status (stack-status the-stack)) |
|
165 |
+ (format t "~&STATUS ~a~%" (stack-status the-stack)) |
|
166 |
+ (setf old-status (stack-status the-stack))) |
|
167 |
+ |
|
168 |
+ (if (ends-with-subseq "COMPLETE" (symbol-name (stack-status the-stack))) |
|
169 |
+ (progn |
|
170 |
+ (format t "~2& OUTPUTS ~%=========~%") |
|
171 |
+ (stack-outputs the-stack) |
|
172 |
+ (format t "~&=========~%") |
|
173 |
+ (return))) |
|
174 |
+ (sleep 5))) |
|
175 |
+ (fresh-line)) |
|
176 |
+ |
|
177 |
+(defun main () |
|
178 |
+ (let* ((context (net.didierverna.clon:make-context :synopsis *cloud-watcher-synopsis*)) |
|
179 |
+ (files (clon:remainder :context context))) |
|
180 |
+ (cond ((clon:getopt :long-name "help") (clon:help)) |
|
181 |
+ ((clon:getopt :long-name "watch") (watch-stack (car files))) |
|
182 |
+ ((clon:getopt :long-name "outputs") (stack-outputs-main (car files))) |
|
183 |
+ ((clon:getopt :long-name "parameters") (stack-parameters-main (car files)))))) |
|
184 |
+ |
|
185 |
+(defun dump () |
|
186 |
+ "Create an executable with the command-line interface defined above." |
|
187 |
+ (clon:dump "cloud-watcher" main)) |