-
Notifications
You must be signed in to change notification settings - Fork 0
/
env.clj
291 lines (254 loc) · 13.6 KB
/
env.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
(ns andrewslai.clj.init.env
"Parses environment variables into Clojure maps that are used to boot system
components."
(:require [andrewslai.clj.http-api.andrewslai :as andrewslai]
[andrewslai.clj.http-api.auth.buddy-backends :as bb]
[andrewslai.clj.http-api.middleware :as mw]
[andrewslai.clj.http-api.virtual-hosting :as vh]
[andrewslai.clj.http-api.wedding :as wedding]
[andrewslai.clj.persistence.filesystem.in-memory-impl :as memory]
[andrewslai.clj.persistence.filesystem.local :as local-fs]
[andrewslai.clj.persistence.filesystem.s3-impl :as s3-storage]
[andrewslai.clj.persistence.rdbms.embedded-h2-impl :as embedded-h2]
[andrewslai.clj.persistence.rdbms.embedded-postgres-impl
:as embedded-pg]
[andrewslai.clj.test-utils :as tu]
[malli.core :as m]
[malli.dev.pretty :as pretty]
[malli.dev.virhe :as v]
[malli.instrument :as mi]
[next.jdbc :as next]
[taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Boot instructions for starting system components from the environment
;;
;; After parsing into a `launch-options` map, the config
;; namespace will continue booting pieces/components of the system
;; depending on which launch options were selected
;;
;; e.g. if `[:andrewslai :authentication-type]` of `:keycloak` was selected,
;; the `init-andrewslai-keycloak` helper will start a `bb/keycloak-backend`
;; by parsing relevant keycloak environment variables into a configuration
;; map (`env->keycloak`) that can be used to boot the keycloak component.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn env->keycloak
{:malli/schema [:=> [:cat :map]
[:map
[:realm [:string {:error/message "Missing Keycloak realm. Set via ANDREWSLAI_AUTH_REALM environment variable."}]]
[:auth-server-url [:string {:error/message "Missing Keycloak auth server url. Set via ANDREWSLAI_AUTH_URL environment variable."}]]
[:client-id [:string {:error/message "Missing Keycloak client id. Set via ANDREWSLAI_AUTH_CLIENT environment variable."}]]
[:client-secret [:string {:error/message "Missing Keycloak secret. Set via ANDREWSLAI_AUTH_SECRET environment variable."}]]
[:ssl-required [:string {:error/message "Missing Keycloak ssl requirement. Set in code. Should never happen."}]]
[:confidential-port [:int {:error/message "Missing Keycloak confidential port. Set in code. Should never happen."}]]]]
:malli/scope #{:output}}
[env]
{:realm (get env "ANDREWSLAI_AUTH_REALM")
:auth-server-url (get env "ANDREWSLAI_AUTH_URL")
:client-id (get env "ANDREWSLAI_AUTH_CLIENT")
:client-secret (get env "ANDREWSLAI_AUTH_SECRET")
:ssl-required "external"
:confidential-port 0})
(defn env->pg-conn
{:malli/schema [:=> [:cat :map]
[:map
[:dbname [:string {:error/message "Missing DB name. Set via ANDREWSLAI_DB_NAME environment variable."}]]
[:db-port [:string {:error/message "Missing DB port. Set via ANDREWSLAI_DB_PORT environment variable."}]]
[:host [:string {:error/message "Missing DB host. Set via ANDREWSLAI_DB_HOST environment variable."}]]
[:user [:string {:error/message "Missing DB user. Set via ANDREWSLAI_DB_USER environment variable."}]]
[:password [:string {:error/message "Missing DB pass. Set via ANDREWSLAI_DB_PASSWORD environment variable."}]]
[:dbtype [:string {:error/message "Missing DB type. Set in code. Should never happen."}]]]]
:malli/scope #{:output}}
[env]
{:dbname (get env "ANDREWSLAI_DB_NAME")
:db-port (get env "ANDREWSLAI_DB_PORT" "5432")
:host (get env "ANDREWSLAI_DB_HOST")
:user (get env "ANDREWSLAI_DB_USER")
:password (get env "ANDREWSLAI_DB_PASSWORD")
:dbtype "postgresql"})
(defn env->andrewslai-s3
{:malli/schema [:=> [:cat :map]
[:map
[:bucket [:string {:error/message "Missing S3 bucket. Set via ANDREWSLAI_BUCKET environment variable."}]]
[:creds [:any {:error/message "Missing S3 credential provider chain. Set in code. Should never happen."}]]]]
:malli/scope #{:output}}
[env]
{:bucket (get env "ANDREWSLAI_BUCKET")
:creds s3-storage/CustomAWSCredentialsProviderChain})
(defn env->wedding-s3
{:malli/schema [:=> [:cat :map]
[:map
[:bucket [:string {:error/message "Missing Wedding S3 bucket. Set via ANDREWSLAI_WEDDING_BUCKET environment variable."}]]
[:creds [:any {:error/message "Missing Wedding S3 credential provider chain. Set in code. Should never happen."}]]]]
:malli/scope #{:output}}
[env]
{:bucket (get env "ANDREWSLAI_WEDDING_BUCKET")
:creds s3-storage/CustomAWSCredentialsProviderChain})
(defn env->andrewslai-local-fs
{:malli/schema [:=> [:cat :map]
[:map
[:root [:string {:error/message "Missing Local FS root path. Set via ANDREWSLAI_STATIC_CONTENT_FOLDER environment variable."}]]]]
:malli/scope #{:output}}
[env]
{:root (get env "ANDREWSLAI_STATIC_CONTENT_FOLDER")})
(defn env->wedding-local-fs
{:malli/schema [:=> [:cat :map]
[:map
[:root [:string {:error/message "Missing Local FS root path. Set via ANDREWSLAI_WEDDING_STATIC_CONTENT_FOLDER environment variable."}]]]]
:malli/scope #{:output}}
[env]
{:root (get env "ANDREWSLAI_WEDDING_STATIC_CONTENT_FOLDER")})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Configuration map
;; Parse environment variables into a map of config values:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def database-boot-instructions
{:name :database-connection
:path "ANDREWSLAI_DB_TYPE"
:launchers {"postgres" (fn [env] (next/get-datasource (env->pg-conn env)))
"embedded-h2" (fn [_env] (embedded-h2/fresh-db!))
"embedded-postgres" (fn [_env] (embedded-pg/fresh-db!))}
:default "postgres"})
(def andrewslai-authentication-boot-instructions
{:name :andrewslai-authentication
:path "ANDREWSLAI_AUTH_TYPE"
:launchers {"keycloak" (fn [env] (bb/keycloak-backend (env->keycloak env)))
"always-unauthenticated" (fn [_env] bb/unauthenticated-backend)
"custom-authenticated-user" (fn [_env] (bb/authenticated-backend {:name "Test User"
:realm_access {:roles ["andrewslai" "wedding"]}}))}
:default "keycloak"})
(def andrewslai-authorization-boot-instructions
{:name :andrewslai-authorization
:path "ANDREWSLAI_AUTHORIZATION_TYPE"
:launchers {"public-access" (fn [_env] tu/public-access)
"use-access-control-list" (fn [_env] andrewslai/ANDREWSLAI-ACCESS-CONTROL-LIST)}
:default "use-access-control-list"})
(def andrewslai-static-content-adapter-boot-instructions
{:name :andrewslai-static-content-adapter
:path "ANDREWSLAI_STATIC_CONTENT_TYPE"
:launchers {"none" (fn [_env] identity)
"s3" (fn [env] (s3-storage/map->S3 (env->andrewslai-s3 env)))
"in-memory" (fn [_env] (memory/map->MemFS {:store (atom memory/example-fs)}))
"local-filesystem" (fn [env] (local-fs/map->LocalFS (env->andrewslai-local-fs env)))}
:default "s3"})
(def wedding-authentication-boot-instructions
{:name :wedding-authentication
:path "ANDREWSLAI_WEDDING_AUTH_TYPE"
:launchers {"keycloak" (fn [env] (bb/keycloak-backend (env->keycloak env)))
"always-unauthenticated" (fn [_env] bb/unauthenticated-backend)
"custom-authenticated-user" (fn [_env] (bb/authenticated-backend {:name "Test User"
:realm_access {:roles ["andrewslai" "wedding"]}}))}
:default "keycloak"})
(def wedding-authorization-boot-instructions
{:name :wedding-authorization
:path "ANDREWSLAI_WEDDING_AUTHORIZATION_TYPE"
:launchers {"public-access" (fn [_env] tu/public-access)
"use-access-control-list" (fn [_env] wedding/WEDDING-ACCESS-CONTROL-LIST)}
:default "use-access-control-list"})
(def wedding-static-content-adapter-boot-instructions
{:name :wedding-static-content-adapter
:path "ANDREWSLAI_WEDDING_STATIC_CONTENT_TYPE"
:launchers {"none" (fn [_env] identity)
"s3" (fn [env] (s3-storage/map->S3 (env->wedding-s3 env)))
"in-memory" (fn [_env] (memory/map->MemFS {:store (atom memory/example-fs)}))
"local-filesystem" (fn [env] (local-fs/map->LocalFS (env->wedding-local-fs env)))}
:default "s3"})
(def DEFAULT-BOOT-INSTRUCTIONS
"Instructions for how to boot the entire system"
[database-boot-instructions
andrewslai-authentication-boot-instructions
andrewslai-authorization-boot-instructions
andrewslai-static-content-adapter-boot-instructions
wedding-authentication-boot-instructions
wedding-authorization-boot-instructions
wedding-static-content-adapter-boot-instructions])
(def ANDREWSLAI-BOOT-INSTRUCTIONS
"Instructions for how to boot the Andrewslai app"
[database-boot-instructions
andrewslai-authentication-boot-instructions
andrewslai-authorization-boot-instructions
andrewslai-static-content-adapter-boot-instructions])
(def WEDDING-BOOT-INSTRUCTIONS
"Instructions for how to boot the Andrewslai app"
[database-boot-instructions
wedding-authentication-boot-instructions
wedding-authorization-boot-instructions
wedding-static-content-adapter-boot-instructions])
(def BootInstruction
[:map
[:name [:keyword {:error/message "Invalid Boot Instruction. Missing name."}]]
[:path [:string {:error/message "Invalid Boot Instruction. Missing path."}]]
[:default [:string {:error/message "Invalid Boot Instruction. Missing default."}]]
[:launchers [:map {:error/message "Invalid Boot Instruction. Missing launchers."}]]
])
(def BootInstructions
[:sequential BootInstruction])
;; TODO: TEST ME!
;; TODO: 2023-01-22: Just refactored boot instructions. Need to update tests!
(defn start-system!
{:malli/schema [:function
[:=> [:cat :map] :map]
[:=> [:cat BootInstructions :map] :map]]}
([env]
(start-system! DEFAULT-BOOT-INSTRUCTIONS
env))
([boot-instructions env]
(reduce (fn [acc {:keys [name path default launchers] :as system-component}]
(let [launcher (get env path default)
init-fn (get launchers launcher)]
(if init-fn
(do (log/debugf "Starting %s using `%s`launcher: %s" name launcher init-fn)
(assoc acc name (init-fn env)))
(throw (ex-info (format "%s had invalid value [%s] for component [%s]. Valid options are: %s"
path
launcher
name
(keys launchers))
{})))))
{}
boot-instructions)))
(defn make-middleware
[authentication authorization]
(comp mw/standard-stack
(mw/auth-stack authentication authorization)))
(defn prepare-andrewslai
[{:keys [database-connection
andrewslai-authentication
andrewslai-authorization
andrewslai-static-content-adapter]
:as system}]
{:database database-connection
:http-mw (make-middleware andrewslai-authentication
andrewslai-authorization)
:static-content-adapter andrewslai-static-content-adapter})
(defn prepare-wedding
[{:keys [database-connection
wedding-authentication
wedding-authorization
wedding-static-content-adapter]
:as system}]
{:database database-connection
:http-mw (make-middleware wedding-authentication
wedding-authorization)
:static-content-adapter wedding-static-content-adapter})
(defn prepare-for-virtual-hosting
[system]
{:andrewslai (prepare-andrewslai system)
:wedding (prepare-wedding system)})
(defn make-http-handler
[{:keys [andrewslai wedding] :as components}]
(vh/host-based-routing
{#"caheriaguilar.and.andrewslai.com" {:priority 0
:app (wedding/wedding-app wedding)}
#".*" {:priority 100
:app (andrewslai/andrewslai-app andrewslai)}}))
;; Updates the Malli schema validation output to only show the errors
;; This will:
;; (1) Make sure we don't log secrets
;; (2) Focus on errors so the user gets direct feedback about what to fix
(defmethod v/-format ::m/invalid-output [_ _ {:keys [value args output fn-name]} printer]
{:body
[:group
(pretty/-block "Invalid function return value. Function Var:" (v/-visit fn-name printer) printer) :break :break
(pretty/-block "Errors:" (pretty/-explain output value printer) printer) :break :break]})
(mi/collect! {:ns 'andrewslai.clj.init.env})
(mi/instrument! {:report (pretty/thrower)})