git.fiddlerwoaroof.com
Browse code

(init)

Ed Langley authored on 31/03/2018 05:44:30
Showing 3 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,3 @@
1
+*.fasl
2
+*~
3
+/cloud-watcher
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))