Browse code
Refactor into files
Ed Langley authored on 11/04/2018 22:45:24
Showing 6 changed files
Showing 6 changed files
0 | 2 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,158 @@ |
1 |
+(defpackage :cloud-watcher.aws-result |
|
2 |
+ (:use :cl :fw.lu :alexandria :st) |
|
3 |
+ (:export |
|
4 |
+ #:stack |
|
5 |
+ #:outputs |
|
6 |
+ #:reader |
|
7 |
+ #:creation-time |
|
8 |
+ #:notification-arns |
|
9 |
+ #:stack-id |
|
10 |
+ #:stack-name |
|
11 |
+ #:description |
|
12 |
+ #:stack-status |
|
13 |
+ #:disable-rollback |
|
14 |
+ #:tags |
|
15 |
+ #:deletion-time |
|
16 |
+ #:rollback-configuration |
|
17 |
+ #:drift-information |
|
18 |
+ #:enable-termination-protection |
|
19 |
+ #:parameters |
|
20 |
+ #:start-date-time |
|
21 |
+ #:end-date-time |
|
22 |
+ #:creation-date-time |
|
23 |
+ #:extract-timeline |
|
24 |
+ #:extract-list |
|
25 |
+ #:extract-stack |
|
26 |
+ #:timeline)) |
|
27 |
+(in-package :cloud-watcher.aws-result) |
|
28 |
+ |
|
29 |
+(defun find-all-indices (pred str &optional accum (start (or (car accum) 0))) |
|
30 |
+ (check-type pred function) |
|
31 |
+ (check-type str string) |
|
32 |
+ (check-type start fixnum) |
|
33 |
+ |
|
34 |
+ (tagbody start-loop |
|
35 |
+ (if (= start (length str)) |
|
36 |
+ (return-from find-all-indices (nreverse accum)) |
|
37 |
+ (let ((pos (position-if pred str :start start))) |
|
38 |
+ (if pos |
|
39 |
+ (progn (psetf accum (cons pos accum) |
|
40 |
+ start (1+ pos)) |
|
41 |
+ (go start-loop)) |
|
42 |
+ (return-from find-all-indices (nreverse accum))))))) |
|
43 |
+ |
|
44 |
+(defun transform-by-function-map (function-map key value &optional (default-transform 'identity)) |
|
45 |
+ (funcall (gethash key |
|
46 |
+ function-map |
|
47 |
+ default-transform) |
|
48 |
+ value)) |
|
49 |
+ |
|
50 |
+(defun decamelize (v) |
|
51 |
+ (let* ((indices (or (find-all-indices #'upper-case-p v) '(0))) |
|
52 |
+ (parts (mapcar (serapeum:op (string-downcase (subseq v _ _))) |
|
53 |
+ (if (= (car indices) 0) indices (cons 0 indices)) |
|
54 |
+ (if (/= (car indices) 0) indices (append (cdr indices) |
|
55 |
+ (list (length v))))))) |
|
56 |
+ (serapeum:string-join parts "-"))) |
|
57 |
+ |
|
58 |
+(deftest decamelize () |
|
59 |
+ (should be equal "a" (decamelize "A")) |
|
60 |
+ (should be equal "a" (decamelize "a")) |
|
61 |
+ (should be equal "outputs" (decamelize "Outputs")) |
|
62 |
+ (should be equal "outputs-outputs" (decamelize "OutputsOutputs")) |
|
63 |
+ (should be equal "a-b-c" (decamelize "ABC"))) |
|
64 |
+ |
|
65 |
+ |
|
66 |
+(defmacro tagged-kv-formatter (tag) |
|
67 |
+ `(formatter ,(format nil "~a ~~a ~~a~~%" tag))) |
|
68 |
+ |
|
69 |
+(defun alist-to-initargs (alist value-map) |
|
70 |
+ (mapcan (destructuring-lambda ((key . value)) |
|
71 |
+ (let* ((key (string-case:string-case (key :default key) |
|
72 |
+ ("NotificationARNs" "NotificationArns"))) |
|
73 |
+ (initarg (make-keyword (string-upcase (decamelize key))))) |
|
74 |
+ (list initarg |
|
75 |
+ (transform-by-function-map value-map |
|
76 |
+ initarg |
|
77 |
+ value)))) |
|
78 |
+ alist)) |
|
79 |
+ |
|
80 |
+ |
|
81 |
+(defclass stack () |
|
82 |
+ ((%outputs :initarg :outputs :reader outputs) |
|
83 |
+ (%capabilities :initarg :capabilities :reader capabilities) |
|
84 |
+ (%creation-time :initarg :creation-time :reader creation-time) |
|
85 |
+ (%notification-arns :initarg :notification-arns :reader notification-arns) |
|
86 |
+ (%stack-id :initarg :stack-id :reader stack-id) |
|
87 |
+ (%stack-name :initarg :stack-name :reader stack-name) |
|
88 |
+ (%description :initarg :description :reader description) |
|
89 |
+ (%stack-status :initarg :stack-status :reader stack-status) |
|
90 |
+ (%disable-rollback :initarg :disable-rollback :reader disable-rollback) |
|
91 |
+ (%tags :initarg :tags :reader tags) |
|
92 |
+ (%deletion-time :initarg :deletion-time :reader deletion-time) |
|
93 |
+ (%rollback-configuration :initarg :rollback-configuration :reader rollback-configuration) |
|
94 |
+ (%drift-information :initarg :drift-information :reader drift-information) |
|
95 |
+ (%enable-termination-protection :initarg :enable-termination-protection :reader enable-termination-protection) |
|
96 |
+ (%parameters :initarg :parameters :reader parameters))) |
|
97 |
+ |
|
98 |
+(defclass timeline () |
|
99 |
+ ((%start-date-time :initarg :start-date-time :reader start-date-time) |
|
100 |
+ (%end-date-time :initarg :end-date-time :reader end-date-time) |
|
101 |
+ (%creation-date-time :initarg :creation-date-time :reader creation-date-time))) |
|
102 |
+ |
|
103 |
+(defun extract-timeline (aws-result) |
|
104 |
+ (labels ((cdr-assoc (key list) (cdr (assoc key list :test #'equal))) |
|
105 |
+ (get-in (keys alist) |
|
106 |
+ #+null |
|
107 |
+ (reduce (lambda (accum next) |
|
108 |
+ ()) |
|
109 |
+ ) |
|
110 |
+ (loop |
|
111 |
+ for key in keys |
|
112 |
+ for cur-alist = alist then accum |
|
113 |
+ for accum = (cdr-assoc key cur-alist) |
|
114 |
+ finally (return-from get-in accum)))) |
|
115 |
+ (apply #'make-instance 'timeline |
|
116 |
+ (alist-to-initargs (get-in '("Status" "Timeline") |
|
117 |
+ aws-result) |
|
118 |
+ (make-hash-table))))) |
|
119 |
+ |
|
120 |
+(defun extract-list (aws-result) |
|
121 |
+ (mapcar (destructuring-lambda ((list-item-marker . items)) |
|
122 |
+ (if (string= list-item-marker "member") |
|
123 |
+ items |
|
124 |
+ (error 'invalid-result))) |
|
125 |
+ aws-result)) |
|
126 |
+ |
|
127 |
+(defun extract-stack (aws-result) |
|
128 |
+ (flet ((as-keyword (v) (alexandria:make-keyword (car v)))) |
|
129 |
+ (apply #'make-instance 'stack |
|
130 |
+ (alist-to-initargs aws-result |
|
131 |
+ (fw.lu:alist-string-hash-table |
|
132 |
+ `((:outputs . extract-list) |
|
133 |
+ (:capabilities . extract-list) |
|
134 |
+ (:creation-time . car) |
|
135 |
+ (:notification-arns . car) |
|
136 |
+ (:stack-id . car) |
|
137 |
+ (:stack-name . car) |
|
138 |
+ (:description . car) |
|
139 |
+ (:stack-status . ,#'as-keyword) |
|
140 |
+ (:disable-rollback . car) |
|
141 |
+ (:tags . extract-list) |
|
142 |
+ (:rollback-configuration . car) |
|
143 |
+ (:drift-information . car) |
|
144 |
+ (:enable-termination-protection . car) |
|
145 |
+ (:parameters . extract-list))))))) |
|
146 |
+ |
|
147 |
+ |
|
148 |
+(defgeneric initialize-date (value) |
|
149 |
+ (:method ((value cons)) (local-time:parse-timestring (car value))) |
|
150 |
+ (:method (value) value)) |
|
151 |
+ |
|
152 |
+(defmethod initialize-instance :after ((instance timeline) &key) |
|
153 |
+ (with-slots ((s-sdt %start-date-time) |
|
154 |
+ (s-edt %end-date-time) |
|
155 |
+ (s-cdt %creation-date-time)) instance |
|
156 |
+ (setf s-sdt (initialize-date s-sdt) |
|
157 |
+ s-edt (initialize-date s-edt) |
|
158 |
+ s-cdt (initialize-date s-cdt)))) |
0 | 159 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,46 @@ |
1 |
+(defpackage :cloud-watcher.cli |
|
2 |
+ (:import-from :cloud-watcher.main :stack-parameters :stack-outputs :stack-for-name) |
|
3 |
+ (:import-from :cloud-watcher.aws-result :start-date-time :end-date-time) |
|
4 |
+ (:import-from :serapeum :op) |
|
5 |
+ (:import-from :clon :defsynopsis :group :flag :stropt) |
|
6 |
+ (:use :cl) |
|
7 |
+ (:export options |
|
8 |
+ #:*cloud-watcher-synopsis* |
|
9 |
+ #:dump)) |
|
10 |
+ |
|
11 |
+(in-package :cloud-watcher.cli) |
|
12 |
+ |
|
13 |
+(defparameter *cloud-watcher-synopsis* |
|
14 |
+ (defsynopsis (:postfix "ARGS...") |
|
15 |
+ (group (:header "actions") |
|
16 |
+ (flag :short-name "p" :long-name "parameters" :description "show stack parameters") |
|
17 |
+ (flag :short-name "o" :long-name "outputs" :description "show stack outputs") |
|
18 |
+ (flag :short-name "w" :long-name "watch" :description "watch a cloudformation stack until it's done processing") |
|
19 |
+ (flag :short-name "s" :long-name "start") |
|
20 |
+ (stropt :long-name "aws-region" :default-value "us-west-2") |
|
21 |
+ (flag :short-name "u" :long-name "update")) |
|
22 |
+ (group (:header "misc") |
|
23 |
+ (flag :long-name "help")))) |
|
24 |
+ |
|
25 |
+(defun stack-parameters-main (name) |
|
26 |
+ (stack-parameters (stack-for-name name))) |
|
27 |
+ |
|
28 |
+(defun stack-outputs-main (name) |
|
29 |
+ (stack-outputs (stack-for-name name))) |
|
30 |
+ |
|
31 |
+(defun main () |
|
32 |
+ (let* ((context (net.didierverna.clon:make-context :synopsis *cloud-watcher-synopsis*)) |
|
33 |
+ (files (clon:remainder :context context)) |
|
34 |
+ (region (clon:getopt :long-name "aws-region")) |
|
35 |
+ (aws-sdk/api:*session* (aws-sdk/session:make-session :region region))) |
|
36 |
+ |
|
37 |
+ (format *error-output* "~&IN REGION: ~a~%" region) |
|
38 |
+ |
|
39 |
+ (cond ((clon:getopt :long-name "help") (clon:help)) |
|
40 |
+ ((clon:getopt :long-name "watch") (cloud-watcher.main:watch-stack (car files))) |
|
41 |
+ ((clon:getopt :long-name "outputs") (stack-outputs-main (car files))) |
|
42 |
+ ((clon:getopt :long-name "parameters") (stack-parameters-main (car files)))))) |
|
43 |
+ |
|
44 |
+(defun dump () |
|
45 |
+ "Create an executable with the command-line interface defined above." |
|
46 |
+ (clon:dump "cloud-watcher" main)) |
... | ... |
@@ -14,7 +14,12 @@ |
14 | 14 |
#:fwoar.lisputils |
15 | 15 |
#:net.didierverna.clon |
16 | 16 |
#:cl-base64 |
17 |
+ #:local-time |
|
18 |
+ #:local-time-duration |
|
17 | 19 |
#:aws-sdk/services/cloudformation |
20 |
+ #:aws-sdk/services/elasticmapreduce |
|
18 | 21 |
#:should-test) |
19 | 22 |
:serial t |
20 |
- :components ((:file "main"))) |
|
23 |
+ :components ((:file "aws-result") |
|
24 |
+ (:file "main") |
|
25 |
+ (:file "cli"))) |
... | ... |
@@ -1,126 +1,21 @@ |
1 | 1 |
;; MIT license Edward Langley (c) 2018 |
2 | 2 |
|
3 |
-(defpackage :cloud-watcher.cli |
|
4 |
- (:use :cl :net.didierverna.clon) |
|
5 |
- (:export options |
|
6 |
- #:*cloud-watcher-synopsis*)) |
|
3 |
+(eval-when (:execute :load-toplevel :compile-toplevel) |
|
4 |
+ (net.didierverna.clon:nickname-package)) |
|
7 | 5 |
|
8 | 6 |
(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")))) |
|
7 |
+ (:use :cl :fw.lu :alexandria :st :cloud-watcher.aws-result) |
|
8 |
+ (:export main dump |
|
9 |
+ #:stack-parameters |
|
10 |
+ #:stack-outputs |
|
11 |
+ #:stack-for-name |
|
12 |
+ #:watch-stack)) |
|
25 | 13 |
|
26 | 14 |
(in-package :cloud-watcher.main) |
27 | 15 |
|
28 |
-(eval-when (:execute :load-toplevel :compile-toplevel) |
|
29 |
- (net.didierverna.clon:nickname-package)) |
|
30 |
- |
|
31 | 16 |
(define-condition invalid-result (error) |
32 | 17 |
()) |
33 | 18 |
|
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 | 19 |
(defun stack-for-name (name) |
125 | 20 |
(let* ((aws-result (car (extract-list (cdar (aws/cloudformation:describe-stacks :stack-name name))))) |
126 | 21 |
(the-stack (extract-stack aws-result))) |
... | ... |
@@ -132,22 +27,21 @@ |
132 | 27 |
(funcall formatter stream k v)) |
133 | 28 |
data)) |
134 | 29 |
|
135 |
-(defmacro tagged-kv-formatter (tag) |
|
136 |
- `(formatter ,(format nil "~a ~~a ~~a~~%" tag))) |
|
137 |
- |
|
138 | 30 |
(defun stack-outputs (the-stack) |
139 |
- (print-kvs (tagged-kv-formatter "OUTPUT") |
|
31 |
+ (print-kvs (tagged-kv-formatter "OUTPUT") t |
|
140 | 32 |
(outputs the-stack))) |
141 | 33 |
|
142 | 34 |
(defun stack-parameters (the-stack) |
143 |
- (print-kvs (tagged-kv-formatter "PARAMETERS") |
|
35 |
+ (print-kvs (tagged-kv-formatter "PARAMETERS") t |
|
144 | 36 |
(parameters the-stack))) |
145 | 37 |
|
146 |
-(defun stack-parameters-main (name) |
|
147 |
- (stack-parameters (stack-for-name name))) |
|
38 |
+(defun lt-format (a b &key &allow-other-keys) |
|
39 |
+ (local-time:format-timestring a b)) |
|
148 | 40 |
|
149 |
-(defun stack-outputs-main (name) |
|
150 |
- (stack-outputs (stack-for-name name))) |
|
41 |
+(defgeneric duration-of (timeline) |
|
42 |
+ (:method ((timeline timeline)) |
|
43 |
+ (local-time-duration:timestamp-difference (end-date-time timeline) |
|
44 |
+ (start-date-time timeline)))) |
|
151 | 45 |
|
152 | 46 |
(defun watch-stack (name) |
153 | 47 |
(format t "~&Watching ~s~2%" name) |
... | ... |
@@ -173,15 +67,3 @@ |
173 | 67 |
(return))) |
174 | 68 |
(sleep 5))) |
175 | 69 |
(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)) |