git.fiddlerwoaroof.com
Browse code

feature: new stack description window

Edward Langley authored on 08/10/2019 05:51:33
Showing 9 changed files
... ...
@@ -2,25 +2,31 @@
2 2
 (in-package :asdf-user)
3 3
 
4 4
 (defsystem :aws-access 
5
-    :description "A simple tool for access to CJ's AWS accounts"
6
-    :author "Ed L <edward@elangley.org>"
7
-    :license "MIT"
8
-    :depends-on (:alexandria
9
-                 :aws-sdk
10
-                 :aws-sdk/services/sts
11
-                 :cells
12
-                 :cl-yaml
13
-                 :fwoar-lisputils
14
-                 :serapeum
15
-                 :ubiquitous
16
-                 :uiop
17
-                 :yason
18
-                 :cxml
19
-                 :xpath)
20
-    :serial t
21
-    :components ((:module "src"
22
-                  :serial t
23
-                  :components ((:file "package")
24
-                               (:file "domain")
25
-                               (:file "mfa-tool")
26
-                               (:file "capi-interface")))))
5
+  :description "A simple tool for access to CJ's AWS accounts"
6
+  :author "Ed L <edward@elangley.org>"
7
+  :license "MIT"
8
+  :depends-on (:alexandria
9
+               :aws-sdk
10
+               :aws-sdk/services/sts
11
+               :cells
12
+               :cl-yaml
13
+               :cxml
14
+               :daydreamer
15
+               :fwoar-lisputils
16
+               :serapeum
17
+               :ubiquitous
18
+               :uiop
19
+               :xpath
20
+               :yason)
21
+  :serial t
22
+  :components ((:module "src"
23
+                :serial t
24
+                :components ((:file "package")
25
+                             (:file "store")
26
+                             (:file "aws-dispatcher")
27
+                             (:file "domain")
28
+                             (:file "objc-utils")
29
+                             (:file "mfa-tool")
30
+                             (:file "stack-store")
31
+                             (:file "stack")
32
+                             (:file "capi-interface")))))
27 33
new file mode 100644
... ...
@@ -0,0 +1,52 @@
1
+(defpackage :mfa-tool.aws-dispatcher
2
+  (:use :cl)
3
+  (:export #:aws-dispatcher #:update-stacks #:select-stack #:stacks #:stack))
4
+(in-package :mfa-tool.aws-dispatcher)
5
+
6
+(defclass aws-dispatcher ()
7
+  ((%region :reader region :accessor %region
8
+            :initarg :region)
9
+   (%credentials :reader credentials
10
+                 :initarg :credentials))
11
+  (:default-initargs
12
+   :region "us-east-1"
13
+   :credentials (error "AWS-DISPATCHER requires a :CREDENTIALS initarg")))
14
+
15
+(defclass update-stacks ()
16
+  ((%stacks :initarg :stacks :reader stacks)))
17
+(defun update-stacks (stacks)
18
+  (fw.lu:new 'update-stacks stacks))
19
+
20
+(defclass select-stack ()
21
+  ((%stacks :initarg :stack :reader stack)))
22
+
23
+(defun select-stack (stack)
24
+  (fw.lu:new 'select-stack stack))
25
+
26
+(defclass update-region ()
27
+  ((%new-region :initarg :region :reader region)))
28
+(defun update-region (region)
29
+  (fw.lu:new 'update-region region))
30
+
31
+(defmethod mfa-tool.store:dispatch :around ((store aws-dispatcher) action)
32
+  (let ((aws-sdk:*session* (aws-sdk:make-session :credentials (credentials store)
33
+                                                 :region (region store))))
34
+    (call-next-method)))
35
+
36
+(defmethod mfa-tool.store:execute ((store aws-dispatcher) (action update-region))
37
+  (setf (%region store) (region action)))
38
+
39
+(defmethod mfa-tool.store:dispatch :after ((store aws-dispatcher) (action update-region))
40
+  (mfa-tool.store:dispatch store :|Get Stacks|))
41
+
42
+(defmethod mfa-tool.store:dispatch :after ((store aws-dispatcher) (action (eql :|Get Stacks|)))
43
+  (bt:make-thread
44
+   (lambda ()
45
+     (let ((aws-sdk:*session* (aws-sdk:make-session :credentials (credentials store)
46
+                                                    :region (region store))))
47
+       (mfa-tool.store:dispatch store
48
+                                (update-stacks (mapcar 'daydreamer.aws-result:extract-stack
49
+                                                       (daydreamer.aws-result:extract-list
50
+                                                        (cdar
51
+                                                         (aws/cloudformation:describe-stacks))))))))
52
+   :name "Stack Fetcher"))
0 53
\ No newline at end of file
... ...
@@ -1,7 +1,8 @@
1 1
 (in-package :mfa-tool)
2 2
 
3 3
 (capi:define-interface mfa-tool ()
4
-  ((%default-account :initarg :default-account :reader default-account)
4
+  ((assumed-credentials :accessor assumed-credentials :initform (make-hash-table :test 'equal))
5
+   (%default-account :initarg :default-account :reader default-account)
5 6
    (%signin-url :accessor signin-url))
6 7
   (:panes
7 8
    (output-pane capi:collector-pane :reader output
... ...
@@ -31,7 +32,8 @@
31 32
                      :reader account-selector)
32 33
    (action-buttons capi:push-button-panel
33 34
                    :items '(:|Open Web Console|
34
-                            :|Authorize iTerm|)
35
+                            :|Authorize iTerm|
36
+                            :|Cloudformation Stacks|)
35 37
                    :selection-callback 'execute-action
36 38
                    :callback-type :data-interface)
37 39
    (listener-button capi:push-button
... ...
@@ -71,6 +73,11 @@
71 73
                               (probe-file 
72 74
                                (merge-pathnames (make-pathname :name "AuthorizeShell" :type "scpt") 
73 75
                                                 (bundle-resource-root))))))
76
+  (:method ((action (eql :|Cloudformation Stacks|)) (interface mfa-tool))
77
+   (let ((stack-interface (make-instance 'mfa-tool.stack:stack-interface
78
+                                         :credentials (current-credentials interface))))
79
+     (mfa-tool.store:dispatch stack-interface :|Get Stacks|)
80
+     (capi:display stack-interface)))
74 81
   (:method ((action (eql :|Lisp REPL|)) (interface mfa-tool))
75 82
     (capi:contain (make-instance 'capi:listener-pane)
76 83
                   :best-width 1280
... ...
@@ -133,6 +140,14 @@
133 140
 
134 141
   (:menu-bar edit-menu window-menu))
135 142
 
143
+(defun start-in-repl (&optional (accounts (asdf:system-relative-pathname :aws-access "accounts.json")))
144
+  (ubiquitous:restore :cj.mfa-tool)
145
+  (setf aws:*session* (aws:make-session)
146
+        *print-readably* nil
147
+        *accounts* (reprocess-accounts (load-accounts accounts)))
148
+  (interface :default-account 
149
+             (ubiquitous:value :default-account)))
150
+
136 151
 (defun main ()
137 152
   (setf *debugger-hook* 'debugging
138 153
         *print-readably* nil
... ...
@@ -1,7 +1,7 @@
1 1
 (in-package :mfa-tool)
2 2
 
3 3
 (defvar *accounts* ())
4
-
4
+(defvar *main-credentials* (aws-sdk:make-credentials))
5 5
 (defun session-name ()
6 6
   (format nil "bootstrap~d" (+ 5000 (random 5000))))
7 7
 
... ...
@@ -27,6 +27,7 @@
27 27
           (mfa-serial-number *user_management_account_id*
28 28
                              user))
29 29
         (role-arn (role-arn account role)))
30
+    (let ((aws-sdk:*session* (aws-sdk:make-session :credentials *main-credentials*))))
30 31
     (loop
31 32
       (restart-case
32 33
           (return
... ...
@@ -72,6 +73,13 @@
72 73
    (url :reader url
73 74
         :initform (cells:c? (get-url (^url-params))))))
74 75
 
76
+(defgeneric session-credentials (source)
77
+  (:method ((source sts-result-handler))
78
+   (aws-sdk:make-credentials
79
+    :access-key-id (session-id source)
80
+    :secret-access-key (session-key source)
81
+    :session-token (session-token source))))
82
+
75 83
 (defun url-from-signin-token (signin-token)
76 84
   (format nil "https://signin.aws.amazon.com/federation?Action=login&Destination=https%3A%2F%2Fconsole.aws.amazon.com&SigninToken=~a"
77 85
           signin-token))
... ...
@@ -7,25 +7,29 @@
7 7
 
8 8
 (defparameter *developer-p* (equal "elangley" (uiop/os:getenv "USER")))
9 9
 
10
-(defun bundle-resource-root ()
11
-  (make-pathname :directory
12
-                 (pathname-directory
13
-                  (objc:invoke-into 'string
14
-                                    (objc:invoke "NSBundle" "mainBundle") 
15
-                                    "pathForResource:ofType:" "app" "icns"))))
10
+(defgeneric assumed-credentials (store))
11
+(defgeneric (setf assumed-credentials) (value store))
16 12
 
17
-(defun clear-cookies ()
18
-  (let ((cookie-storage (objc:invoke "NSHTTPCookieStorage" "sharedHTTPCookieStorage")))
19
-    (map nil
20
-         (lambda (cookie) 
21
-           (objc:invoke cookie-storage "deleteCookie:" cookie))
22
-         (objc:invoke-into 'array cookie-storage "cookies"))))
13
+(defun current-account (interface)
14
+  (cdr (capi:choice-selected-item (account-selector interface))))
15
+
16
+(defun credentials-for-account (interface account)
17
+   (gethash account 
18
+            (assumed-credentials interface)))
19
+(defun (setf credentials-for-account) (new-credentials interface account)
20
+  (setf (gethash account
21
+                 (assumed-credentials interface))
22
+        new-credentials))
23
+
24
+(defun current-credentials (interface)
25
+  (credentials-for-account interface
26
+                           (current-account interface)))
23 27
 
24 28
 (defun go-on (_ interface)
25 29
   (declare (ignore _))
26 30
   (let ((token (capi:text-input-pane-text (mfa-input interface)))
27 31
         (user-name (capi:text-input-pane-text (user-input interface)))
28
-        (account (cdr (capi:choice-selected-item (account-selector interface)))))
32
+        (account (current-account interface)))
29 33
     (clear-cookies)
30 34
     (multiple-value-bind (signin-token creds)
31 35
         (handler-bind (((or dexador:http-request-forbidden
... ...
@@ -58,8 +62,8 @@
58 62
                 (session-token creds)))
59 63
       (capi:set-button-panel-enabled-items (slot-value interface 'action-buttons)
60 64
                                            :set t)
61
-      (setf (signin-url interface) 
62
-            (url-from-signin-token signin-token)))))
65
+      (setf (credentials-for-account interface account) (session-credentials creds)
66
+            (signin-url interface) (url-from-signin-token signin-token)))))
63 67
 
64 68
 (defun close-active-screen ()
65 69
   (let ((active-interface
66 70
new file mode 100644
... ...
@@ -0,0 +1,15 @@
1
+(in-package :mfa-tool)
2
+
3
+(defun bundle-resource-root ()
4
+  (make-pathname :directory
5
+                 (pathname-directory
6
+                  (objc:invoke-into 'string
7
+                                    (objc:invoke "NSBundle" "mainBundle") 
8
+                                    "pathForResource:ofType:" "app" "icns"))))
9
+
10
+(defun clear-cookies ()
11
+  (let ((cookie-storage (objc:invoke "NSHTTPCookieStorage" "sharedHTTPCookieStorage")))
12
+    (map nil
13
+         (lambda (cookie) 
14
+           (objc:invoke cookie-storage "deleteCookie:" cookie))
15
+         (objc:invoke-into 'array cookie-storage "cookies"))))
0 16
new file mode 100644
... ...
@@ -0,0 +1,43 @@
1
+(defpackage :mfa-tool.stack-store
2
+  (:use :cl)
3
+  (:export #:stack-store #:available-stacks selected-stack parameters outputs))
4
+(in-package :mfa-tool.stack-store)
5
+
6
+(defun column (key)
7
+  (serapeum:op (serapeum:assocadr key _
8
+                                  :test 'equal)))
9
+
10
+(defclass stack-store (mfa-tool.store:store mfa-tool.aws-dispatcher:aws-dispatcher)
11
+  ((%available-stacks :accessor available-stacks :initform nil)
12
+   (%selected-stack :accessor selected-stack :initform nil)
13
+   (%parameters :accessor parameters :initform nil)
14
+   (%outputs :accessor outputs :initform nil)))
15
+
16
+
17
+(defun output-columns (output)
18
+  (funcall (data-lens:juxt (column "OutputKey")
19
+                           (column "OutputValue"))
20
+           output))
21
+
22
+(defun parameter-columns (output)
23
+  (funcall (data-lens:juxt (column "ParameterKey")
24
+                           (column "ParameterValue"))
25
+           output))
26
+
27
+(defmethod mfa-tool.store:dispatch :after ((store stack-store)
28
+                                           (action mfa-tool.aws-dispatcher:update-stacks))
29
+  (alexandria:when-let ((stack (car (mfa-tool.aws-dispatcher:stacks action))))
30
+    (mfa-tool.store:dispatch store (mfa-tool.aws-dispatcher:select-stack stack))))
31
+
32
+(defmethod mfa-tool.store:execute ((store stack-store) (action mfa-tool.aws-dispatcher:update-stacks))
33
+  (setf (available-stacks store) (sort (mfa-tool.aws-dispatcher:stacks action)
34
+                                       'string-lessp
35
+                                       :key 'daydreamer.aws-result:stack-name)))
36
+
37
+(defmethod mfa-tool.store:execute ((store stack-store) (action mfa-tool.aws-dispatcher:select-stack))
38
+  (let ((stack (mfa-tool.aws-dispatcher:stack action)))
39
+    (setf (selected-stack store) stack
40
+
41
+          (parameters store) (mapcar 'parameter-columns (daydreamer.aws-result:parameters stack))
42
+
43
+          (outputs store) (mapcar 'output-columns (daydreamer.aws-result:outputs stack)))))
0 44
new file mode 100644
... ...
@@ -0,0 +1,144 @@
1
+(in-package :mfa-tool)
2
+(defpackage :mfa-tool.stack
3
+  (:use :cl)
4
+  (:export #:stack-interface
5
+           #:format-stack-status))
6
+(in-package :mfa-tool.stack)
7
+
8
+(defun dispatch-with-action-creator (action-creator)
9
+  (lambda (store data)
10
+    (mfa-tool.store:dispatch store
11
+                             (funcall action-creator data))))
12
+
13
+(defmacro with-pp ((pane) &body body)
14
+  `(capi:apply-in-pane-process ,pane
15
+                               (lambda ()
16
+                                 ,@body)))
17
+
18
+(defun human-readable-stack-status (stack)
19
+  (nstring-capitalize
20
+   (substitute #\space #\_
21
+               (string (daydreamer.aws-result:stack-status stack)))))
22
+(defun format-stack-status (stream stack &optional colon-p at-sign-p)
23
+  (declare (ignore colon-p at-sign-p))
24
+  (princ (human-readable-stack-status stack)
25
+         stream))
26
+
27
+(defun get-output-columns (type col1 col2)
28
+  `((:title ,(format nil "~a Name" type) 
29
+     :adjust :right
30
+     :width (character ,(max (+ (length type) 5) col1)))
31
+    (:title "Value" 
32
+     :adjust :left
33
+     :width (character ,(max (+ (length type) 5) col2)))))
34
+
35
+(capi:define-interface stack-interface (capi:interface mfa-tool.stack-store:stack-store)
36
+  ()
37
+  (:panes
38
+   (region-chooser capi:option-pane
39
+                   :reader region-chooser
40
+                   ;; :external-max-width '(character 35)
41
+                   :items (list "us-east-1" "us-east-2"
42
+                                "us-west-1" "us-west-2"
43
+                                "ca-central-1"
44
+                                "eu-central-1"
45
+                                "eu-west-1" "eu-west-2")
46
+                   :selection-callback (dispatch-with-action-creator 'mfa-tool.aws-dispatcher::update-region)
47
+                   :callback-type :interface-data)
48
+
49
+   (stack-chooser capi:list-panel
50
+                  :reader stack-chooser
51
+                  ;; :external-max-width '(character 35)
52
+                  :items ()
53
+                  :print-function 'daydreamer.aws-result:stack-name
54
+                  :selection-callback (dispatch-with-action-creator 'mfa-tool.aws-dispatcher:select-stack)
55
+                  :callback-type :interface-data)
56
+
57
+   (status-display capi:display-pane
58
+                   :background :transparent
59
+                   :reader status-display
60
+                   :text "")
61
+
62
+   (outputs-display capi:multi-column-list-panel
63
+                    :columns (get-output-columns "Output" 10 10)
64
+                    :header-args (list :selection-callback :sort)
65
+                    :sort-descriptions (list (capi:make-sorting-description
66
+                                              :type "Output Name"
67
+                                              :key 'car
68
+                                              :sort 'string-lessp
69
+                                              :reverse-sort 'string-greaterp)
70
+                                             (capi:make-sorting-description
71
+                                              :type "Value"
72
+                                              :key 'cadr
73
+                                              :sort 'string-lessp
74
+                                              :reverse-sort 'string-greaterp))
75
+                    :items nil
76
+                    :vertical-scroll t
77
+                    :reader outputs-display
78
+                    :visible-min-height '(character 10)
79
+                    :visible-min-width '(character 50))
80
+   (parameters-display capi:multi-column-list-panel
81
+                       :columns (get-output-columns "Parameter" 10 10)
82
+                       :header-args (list :selection-callback :sort)
83
+                       :sort-descriptions (list (capi:make-sorting-description
84
+                                                 :type "Parameter Name"
85
+                                                 :key 'car
86
+                                                 :sort 'string-lessp
87
+                                                 :reverse-sort 'string-greaterp)
88
+                                                (capi:make-sorting-description
89
+                                                 :type "Value"
90
+                                                 :key 'cadr
91
+                                                 :sort 'string-lessp
92
+                                                 :reverse-sort 'string-greaterp))
93
+                       :items nil
94
+                       :vertical-scroll t
95
+                       :reader parameters-display
96
+                       :visible-min-height '(character 10)
97
+                       :visible-min-width '(character 50)))
98
+  (:layouts
99
+   (key-layout capi:column-layout
100
+               '(region-chooser
101
+                 stack-chooser)
102
+               :visible-max-width '(character 35))
103
+   (attribute-layout capi:column-layout
104
+                     '(status-display
105
+                       parameters-display
106
+                       outputs-display))
107
+   (main-layout capi:row-layout
108
+                '(key-layout
109
+                  attribute-layout)))
110
+  (:default-initargs
111
+   :layout 'main-layout
112
+   :title "Stack Explorer"
113
+   :visible-min-width 800))
114
+
115
+(defmethod mfa-tool.store:execute :after ((interface stack-interface) (_ mfa-tool.aws-dispatcher:update-stacks))
116
+  (with-pp (interface)
117
+    (with-accessors ((stack-chooser stack-chooser)) interface
118
+      (setf (capi:collection-items stack-chooser)
119
+            (mfa-tool.stack-store:available-stacks interface)))))
120
+
121
+(defun max-widths (cols)
122
+  (loop for (col1 col2) in cols
123
+        maximizing (length col1) into len1
124
+        maximizing (length col2) into len2
125
+        finally (return (list len1 len2))))
126
+
127
+(defmethod mfa-tool.store:execute :after ((interface stack-interface) (_ mfa-tool.aws-dispatcher:select-stack))
128
+  (with-pp (interface)
129
+    (with-accessors ((status-display status-display) (selected-stack mfa-tool.stack-store:selected-stack)
130
+                     (outputs-display outputs-display) (outputs mfa-tool.stack-store:outputs)
131
+                     (parameters-display parameters-display) (parameters mfa-tool.stack-store:parameters))
132
+        interface
133
+      (capi:modify-multi-column-list-panel-columns
134
+       outputs-display :columns (apply 'get-output-columns "Output" (max-widths outputs)))
135
+      (capi:modify-multi-column-list-panel-columns
136
+       parameters-display :columns (apply 'get-output-columns "Parameter" (max-widths parameters)))
137
+      
138
+      (setf (capi:display-pane-text status-display)
139
+            (format nil "~a: ~/mfa-tool.stack:format-stack-status/"
140
+                    (daydreamer.aws-result:stack-name selected-stack)
141
+                    selected-stack)
142
+            (capi:collection-items parameters-display) parameters
143
+            (capi:collection-items outputs-display) outputs))))
144
+
0 145
new file mode 100644
... ...
@@ -0,0 +1,20 @@
1
+(defpackage :mfa-tool.store
2
+  (:use :cl)
3
+  (:export #:store #:execute #:dispatch))
4
+(in-package :mfa-tool.store)
5
+
6
+(defclass store ()
7
+  ())
8
+
9
+(defgeneric execute (store action)
10
+  (:argument-precedence-order action store)
11
+  (:method :around (store action)
12
+   (call-next-method)
13
+   store)
14
+  (:method (store action)
15
+   store))
16
+
17
+(defgeneric dispatch (store action)
18
+  (:argument-precedence-order action store)
19
+  (:method ((store store) action)
20
+   (execute store action)))
0 21
\ No newline at end of file