├── .gitignore ├── CHANGES.org ├── README.org ├── deps.edn ├── pom.xml ├── src ├── nomad.clj └── nomad │ └── config.clj └── test └── nomad └── config_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.cpcache 3 | /.nrepl-port 4 | -------------------------------------------------------------------------------- /CHANGES.org: -------------------------------------------------------------------------------- 1 | * Changelog 2 | ** 0.9.x 3 | 0.9.x is a significant change to the way Nomad works. We're no longer storing 4 | config in one imported EDN file, preferring instead to keep configuration inline 5 | next to the code it's configuring. 6 | 7 | In terms of backwards compatibility - users of 0.7.x can still use the =nomad= 8 | namespace exactly as before; users of 0.8.x pre-releases (of which I doubt there 9 | are many) can find the functions under the =nomad.temp-080-beta= namespace. 10 | 11 | Both of these will be removed in a future release. 12 | 13 | Migration guide: 14 | - Firstly, move your configuration from EDN into the relevant CLJ namespaces in 15 | a =defconfig= declaration, as described in the README. 16 | - =nomad/environments=, =nomad/hosts= and =nomad/instances= have all been 17 | replaced by a single notion of 'switches' (which can be whatever you want them 18 | to be). 19 | #+BEGIN_SRC clojure 20 | ;; --- previously 21 | {:db {:host "dev-host" 22 | :username "db-user" 23 | :port 5432} 24 | 25 | :nomad/environments {"beta" {:db {:host "beta-host" 26 | :username "beta-user"}} 27 | "live" {:db {:host "live-host" 28 | :username "live-user"}}}} 29 | 30 | (let [{:keys [host username port]} (:db (nomad/read-config "my-config.edn"))] 31 | ...) 32 | 33 | 34 | ;; --- now 35 | 36 | (:require [nomad.config :as n]) 37 | 38 | (n/set-defaults {:switches #{...}}) ; once, at application startup 39 | 40 | (defconfig *db-config* 41 | (merge {:port 5432} 42 | (n/switch 43 | :beta {:host "beta-host" 44 | :username "beta-user"} 45 | :live {:host "live-host" 46 | :username "live-user"} 47 | {:host "dev-host" 48 | :username "db-user"}))) 49 | 50 | (let [{:keys [host username port]} *db-config*] 51 | ...) 52 | #+END_SRC 53 | - =#nomad/file=, =#nomad/resource=, =#nomad/env-var=, =#nomad/edn-env-var=, 54 | =#nomad/jvm-prop=, =#nomad/edn-jvm-prop= and =#nomad/snippet= data readers are 55 | no longer required - use standard clojure.core functions/java interop 56 | - =#nomad/secret= (0.8.0-beta only) - use =(n/secret =. Secret keys and 57 | cipher text are now expected in base64 format for brevity - use =n/hex->b64= 58 | to convert existing keys/cipher-text. =n/encrypt= now returns b64-encoded 59 | cipher-text too. 60 | 61 | 62 | *** 0.9.1 63 | Reinstated =env-switches=, which disappeared during one of the alphas. 64 | 65 | ** 0.8.x 66 | The 0.8.x development branch was a lot of iterations around the core idea. This 67 | eventually made it to a beta-quality release, but I was never really convinced 68 | about the changes, so it never had a stable release. 69 | 70 | ** 0.7.x 71 | *** 0.7.3 72 | Update get-hostname to use Java interop rather than shelling out - thanks [[https://github.com/nha][Nicolas Ha]]! 73 | 74 | Thanks also to [[https://github.com/ganmacs][Yuta Iwama]], for refactoring the deep-merge implementation, and 75 | [[https://github.com/sumbach][Sam Umbach]] for a doc fix 76 | 77 | *** 0.7.2 78 | 79 | No breaking changes. 80 | 81 | Addition of ~envf~ - thanks [[https://github.com/martintrojer][Martin]]! 82 | 83 | *** 0.7.1 84 | 85 | No breaking changes. 86 | 87 | Introduced a JVM property reader macro, thanks to [[https://github.com/rosejn][Jeff Rose]] for the 88 | suggestion! 89 | 90 | Introduced default values for config pulled from environment 91 | variables, thanks to [[https://github.com/glittershark][Griffin Smith]] for the PR! 92 | 93 | You can now supply your own data-readers to =read-config= - thanks to 94 | [[https://github.com/lloydshark][@lloydshark]] for the suggestion! 95 | 96 | *** 0.7.0 97 | 98 | Thanks to [[https://github.com/dparis][Dylan Paris]] for his work on this release :) 99 | 100 | Big refactoring, adding =read-config= function. No need to have a 101 | =defconfig= if you only want to read the config once and don't want to 102 | create a var. 103 | 104 | *Breaking change*: Nomad versions later than 0.7.0 are not compatible 105 | with Frodo versions earlier than 0.4.1 - please update your Frodo 106 | dependency accordingly. 107 | 108 | ** 0.6.x 109 | 110 | *** 0.6.5 (stable not released) 111 | 112 | 0.6.5-rc{1,2} were released in preparation for 0.7.0 - see above. 113 | 114 | *** 0.6.4 115 | 116 | No breaking changes - can now set the Nomad environment by setting the 117 | nomad.env Java system property 118 | 119 | *** 0.6.3 120 | 121 | No breaking changes, changing =#nomad/edn-env-var= to return =nil= 122 | rather than throwing an ugly exception on missing environment 123 | variables. 124 | 125 | *** 0.6.2 126 | 127 | No breaking changes, adding =#nomad/edn-env-var= reader macro. 128 | 129 | *** 0.6.1 130 | 131 | No breaking changes, adding =with-location-override= 132 | 133 | *** 0.6.0 134 | 135 | Breaking change - environment config now takes preference over host 136 | config. Seems that, if an environment is explicitly specified, the 137 | expected behaviour is that the environment config is honoured. 138 | 139 | Also, added =#nomad/env-var= reader macro to read a config value from 140 | an environment variable. 141 | 142 | Thanks to [[https://github.com/oholworthy][Oliver Holworthy]] for 143 | these suggestions! 144 | 145 | ** 0.5.x 146 | 147 | *** 0.5.1 148 | 149 | More helpful error message when a snippet can't be found. No breaking 150 | changes. 151 | 152 | *** 0.5.0 153 | 154 | Minor breaking change - removing the whole =:nomad/environments= map 155 | from the full resulting configuration, in line with =:nomad/hosts= 156 | 157 | ** 0.4.x 158 | *** 0.4.1 159 | 160 | Adding in concept of 'environments' 161 | 162 | Minor breaking change - in the config meta-information, =:environment= 163 | now points to the current environment's config, and the old 164 | =:environment= key can now be found under =:location= 165 | 166 | ** 0.3.x 167 | *** 0.3.3 168 | 169 | Handling gracefully when any of the configuration files don't exist. 170 | 171 | No breaking changes. 172 | 173 | *** 0.3.2 174 | 175 | Allowed private config in the general section, for private files in a 176 | known, common location. 177 | 178 | No breaking changes. 179 | 180 | Thanks Michael Jakl! 181 | 182 | *** 0.3.1 183 | 184 | Introduced 'snippets' using the =:nomad/snippets= key and the 185 | =#nomad/snippet= reader macro. 186 | 187 | No breaking changes. 188 | 189 | *** 0.3.0 190 | 191 | 0.3.0 introduces a rather large breaking change: in the outputted 192 | configuration map, rather than lots of :nomad/* keys, all of the 193 | current host/current instance maps are merged into the main output map. 194 | 195 | In general, you should just be able to replace: 196 | 197 | - =(get-in (my-config) [:nomad/current-host :x :y])= with =(get-in 198 | (my-config) [:x :y])= 199 | 200 | and 201 | 202 | - =(get-in (my-config) [:nomad/current-instance :x :y])= with =(get-in 203 | (my-config) [:x :y])= 204 | 205 | unless you have conflicting key names in your general configuration. 206 | 207 | ** 0.2.x 208 | *** 0.2.1 209 | 210 | Mainly the addition of the private configuration - no breaking changes. 211 | 212 | - Allowed users to add =:nomad/private-file= key to host/instance maps 213 | to specify a private configuration file, which is merged into the 214 | =:nomad/current-host= and =:nomad/current-instance= maps. 215 | - Added =#nomad/file= reader macro 216 | - Added =:nomad/hostname= and =:nomad/instance= keys to 217 | =:nomad/current-host= and =:nomad/current-instance= maps 218 | respectively. 219 | 220 | *** 0.2.0 221 | 222 | 0.2.0 has introduced a couple of breaking changes: 223 | 224 | - =get-config=, =get-host-config= and =get-instance-config= have been 225 | removed. Use =defconfig= as described above in place of 226 | =get-config=; the current host and instance config now live under 227 | the =:nomad/current-host= and =:nomad/current-instance= keys 228 | respectively. 229 | - Previously, Nomad expected your configuration file to be in a 230 | =nomad-config.edn= file at the root of the classpath. You can now 231 | specify the file or resource (or many, in fact, if you use several 232 | =defconfig= invocations) for Nomad to use. 233 | 234 | ** 0.1.0 235 | 236 | Initial release 237 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * nomad 2 | 3 | A configuration library designed to allow Clojure applications to travel 4 | painlessly between different hosts and environments. 5 | 6 | ** Usage 7 | 8 | *** Set-up 9 | 10 | Add the *nomad* dependency to your =project.clj= 11 | 12 | #+BEGIN_SRC clojure 13 | [jarohen/nomad "0.9.0"] 14 | 15 | {jarohen/nomad {:mvn/version "0.9.0"}} 16 | #+END_SRC 17 | 18 | Please see the Changelog for more details. 19 | 20 | *** Rationale 21 | 22 | In an ideal world, we'd choose to declare configuration as a vanilla Clojure 23 | map, and access it as such, from anywhere, and everything would Just Work™. 24 | 25 | Nomad aims to get as close to that as possible. In doing so, it makes a few 26 | opinionated decisions: 27 | 28 | - Configuration is declared *near* the code it is configuring - i.e. not in an 29 | EDN file, not in environment variables, not in a separate infrastructure 30 | repository, not in deploy scripts, etc etc etc. This locality allows us to 31 | *reason* about how the code will behave in certain environments without having 32 | to audit an entire system, and aids us when we come to adding new 33 | configuration variables. 34 | 35 | Sure, we'll always need to allow whatever's bootstrapping our 36 | application to alter the behaviour in some way (Nomad relies on passing in a 37 | set of 'switches', for example), but let's *minimise* it. 38 | 39 | - Configuration is declared in *Clojure* - this gives us the full flexibility of 40 | normal Clojure functions to build/compose our configuration as necessary. In 41 | our experience, configuration libraries often tend to try to replicate a full 42 | language trying to emulate certain behaviours - retrieving configuration from 43 | elsewhere, fallbacks/defaults, environment variable parsing and composing 44 | multiple configuration files, to name a few. 45 | 46 | Let's just use Clojure core/simple Java interop over a config-specific DSL - 47 | they're good at this. 48 | 49 | Is there a possibility of this freedom getting abused, and the boundary 50 | between 'configuration' and code getting blurred? Sure. I'm trusting you to 51 | know when your configuration goes significantly beyond 'just a map', though. 52 | 53 | *** Migrating from 0.7.x/0.8.x betas 54 | 55 | I had a significant re-think of what I wanted from a configuration library 56 | between 0.7.x/0.8.x and 0.9.x, based on learnings from using it in a number of 57 | non-trivial applications. 58 | 59 | - The original 0.7.x behaviour will be maintained for the time being, in the 60 | original =nomad= namespace, although will be removed in a later release. 61 | - The 0.8.x behaviour never made it to a stable release, and so has been 62 | removed in 0.9.0-rc1. 63 | 64 | There is a migration guide for both of these versions in the changelog. 65 | 66 | ** Getting started 67 | 68 | First, we require =[nomad.config :as n]=. 69 | 70 | In the entry point to our application (or, for now, at the REPL) we need to 71 | initialise Nomad - over time, we'll need to add to this: 72 | 73 | #+BEGIN_SRC clojure 74 | (n/set-defaults! {}) 75 | #+END_SRC 76 | 77 | We then use =defconfig= to declare some configuration: 78 | 79 | #+BEGIN_SRC clojure 80 | (n/defconfig email-config 81 | {:email-behaviour :just-console-for-now}) 82 | 83 | (defn email-user! [{:keys [...] :as email}] 84 | (case (:email-behaviour email-config) 85 | :just-console-for-now (prn "Would send:" email) 86 | :actually-send-the-email (send-email! email))) 87 | #+END_SRC 88 | 89 | We can see that, once we've declared our configuration, we use it in the same 90 | way we'd use any other vanilla Clojure data structure. We can destructure it, 91 | compose it, pass it around, play with it/redefine it in our REPL - no problem. 92 | 93 | If I wanted to, I could use =System/getenv=, =System/getProperty=, or any 94 | vanilla Clojure functions etc in here: 95 | 96 | #+BEGIN_SRC clojure 97 | (n/defconfig email-config 98 | {:behaviour :actually-send-the-email 99 | :host (System/getenv "EMAIL_HOST") 100 | :port (or (some-> (System/getenv "EMAIL_PORT") Long/parseLong) 101 | 25)}) 102 | #+END_SRC 103 | 104 | This obviates the need for any kind of config DSL to retrieve/parse/default 105 | configuration values. 106 | 107 | ** Changing configuration based on location 108 | 109 | Your configuration will likely vary depending on whether you're running your 110 | application in development, test/beta/staging environments, or production. Nomad 111 | accomplishes this using 'switches', which are set in your call to 112 | =set-defaults!=: 113 | 114 | #+BEGIN_SRC clojure 115 | (n/set-defaults! {:switches #{:live}}) 116 | #+END_SRC 117 | 118 | You can then vary your configuration using the =n/switch= macro, which behaves 119 | a lot like Clojure's =case= macro: 120 | 121 | #+BEGIN_SRC clojure 122 | ;; in your app entry point 123 | (n/set-defaults! {:switches #{:live}}) 124 | 125 | ;; in your namespace 126 | (n/defconfig db-config 127 | (merge {:port 5432} 128 | (n/switch 129 | :beta {:host "beta-db-host" 130 | :username "beta-username"} 131 | :live {:host "live-db-host" 132 | :username "live-username"} 133 | 134 | ;; you can also provide a default, if none of the above switches are 135 | ;; active 136 | {:host "localhost" 137 | :username "local-user"}))) 138 | 139 | ;; at the REPL (say) 140 | (let [{:keys [host port username]} db-config] 141 | ;; in here, we get the live config, because of our earlier `set-defaults!` 142 | ...) 143 | #+END_SRC 144 | 145 | You're free to choose how to select your switches - or, you can use 146 | =n/env-switches=, which looks for the =NOMAD_SWITCHES= environment variable, or 147 | the =nomad.switches= JVM property, expecting a comma-separated list of switches: 148 | 149 | #+BEGIN_SRC clojure 150 | ;; starting the application 151 | NOMAD_SWITCHES=live,foo java -cp ... clojure.main -m ... 152 | 153 | ;; --- in the entry point 154 | (n/set-defaults! {:switches n/env-switches}) 155 | ;; sets switches to #{:live :foo} 156 | #+END_SRC 157 | 158 | ** Secrets (shh!) 159 | 160 | Nomad can manage your secrets for you, too. Under Nomad, these are encrypted and 161 | checked in to your application repository, with the encryption keys managed 162 | outside of your application (in whatever manner you choose). 163 | 164 | First, generate yourself an encryption key using =(n/generate-key)= 165 | 166 | #+BEGIN_SRC clojure 167 | (nomad.config/generate-key) 168 | ;; => "tvuGp8oGGbP+IQSzidYS+oXB3fhGZLpVLhMFljL0I/o=" 169 | #+END_SRC 170 | 171 | We then pass this to Nomad as part of the call to =set-defaults!=: 172 | 173 | #+BEGIN_SRC clojure 174 | (n/set-defaults! {:secret-keys {:my-dev-key "tvuGp8oGGbP+IQSzidYS+oXB3fhGZLpVLhMFljL0I/o="}}) 175 | #+END_SRC 176 | 177 | Obviously, normally, this would not be checked into your application repository! 178 | You can get it from an environment variable, an out-of-band file on the local 179 | disk, some external infrastructure management, some cloud key manager, or 180 | something else entirely - take your pick! 181 | 182 | We then encrypt credentials using =n/encrypt=, and store this cipher-text, along 183 | with the key-id used to encrypt the credentials, in our =defconfig= 184 | declarations: 185 | 186 | #+BEGIN_SRC clojure 187 | ;; --- at your REPL 188 | 189 | (n/encrypt :my-dev-key "super-secure-password123") 190 | ;; => "y/DwItK86ZgtUUTzz+sDCNd3rpsOuiyKmqcHIelHnRdrpr06k43NEnrraWrfUHE39ZXtLItqxZVM3hmCj1pqLw==" 191 | 192 | ;; --- in your namespace 193 | (defconfig db-config 194 | {:host "db-host" 195 | :username "db-username" 196 | :password (n/decrypt :my-dev-key "y/DwItK86ZgtUUTzz+sDCNd3rpsOuiyKmqcHIelHnRdrpr06k43NEnrraWrfUHE39ZXtLItqxZVM3hmCj1pqLw==")}) 197 | 198 | ;; access the password like any other map key 199 | (let [{:keys [host username password]} db-config] 200 | ...) 201 | #+END_SRC 202 | 203 | ** Testing your configuration 204 | 205 | Given configuration declarations are just normal Clojure variables, you can 206 | experiment with them at the REPL, as you would any other Clojure data structure. 207 | 208 | Nomad does offer a couple of other tools to facilitate testing, though. First, 209 | =defconfig= declarations can be dynamically re-bound, using Clojure's standard 210 | =binding= macro: 211 | 212 | #+BEGIN_SRC clojure 213 | (n/defconfig email-config 214 | {:email-behaviour :just-console-for-now}) 215 | 216 | (defn email-user! [{:keys [...] :as email}] 217 | (case (:email-behaviour email-config) 218 | :just-console-for-now (prn "Would send:" email) 219 | :actually-send-the-email (send-email! email))) 220 | 221 | (email-user! {...}) 222 | ;; prints the email to the console 223 | 224 | (binding [email-config {:email-behaviour :actually-send-the-email}] 225 | (email-user! {...})) 226 | ;; actually sends the email 227 | #+END_SRC 228 | 229 | Nomad also offers a =with-config-override= macro, which allows you to override 230 | what switches are active, throughout your system, for the duration of the 231 | expression body: 232 | 233 | #+BEGIN_SRC clojure 234 | (n/defconfig email-config 235 | {:email-behaviour (n/switch 236 | :live :actually-send-the-email 237 | :just-console-for-now)}) 238 | 239 | (defn email-user! [{:keys [...] :as email}] 240 | (case (:email-behaviour email-config) 241 | :just-console-for-now (prn "Would send:" email) 242 | :actually-send-the-email (send-email! email))) 243 | 244 | (email-user! {...}) 245 | ;; prints the email to the console 246 | 247 | (n/with-config-override {:switches #{:live}} 248 | (email-user! {...})) 249 | ;; actually sends the email 250 | #+END_SRC 251 | 252 | 253 | ** Bugs/features/suggestions/questions? 254 | 255 | Please feel free to submit bug reports/patches etc through the GitHub 256 | repository in the usual way! 257 | 258 | Thanks! 259 | 260 | ** Changes 261 | 262 | The Nomad changelog has moved to CHANGES.org. 263 | 264 | ** License 265 | 266 | Copyright © 2013-2018 James Henderson 267 | 268 | Distributed under the Eclipse Public License, the same as Clojure. 269 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.9.0"} 2 | org.clojure/tools.reader {:mvn/version "1.2.1"} 3 | buddy/buddy-core {:mvn/version "1.4.0"}}} 4 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jarohen 5 | nomad 6 | 0.9.1 7 | nomad 8 | 9 | 10 | A configuration library designed to allow Clojure applications to travel painlessly 11 | between different hosts and environments. 12 | 13 | 14 | 15 | 16 | Eclipse Public License - v1.0 17 | https://www.eclipse.org/legal/epl-v10.html 18 | repo 19 | 20 | 21 | 22 | 23 | 24 | org.clojure 25 | clojure 26 | 1.9.0 27 | 28 | 29 | org.clojure 30 | tools.reader 31 | 1.2.1 32 | 33 | 34 | buddy 35 | buddy-core 36 | 1.4.0 37 | 38 | 39 | 40 | 41 | src 42 | 43 | 44 | src 45 | 46 | **/*.clj 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | clojars 55 | https://repo.clojars.org/ 56 | 57 | 58 | 59 | 60 | 61 | clojars 62 | https://repo.clojars.org/ 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/nomad.clj: -------------------------------------------------------------------------------- 1 | (ns ^:deprecated nomad 2 | (:require [clojure.java.io :as io] 3 | [clojure.tools.reader.edn :as edn] 4 | [clojure.walk :refer [postwalk-replace]]) 5 | (:import [java.net InetAddress])) 6 | 7 | (defn- deep-merge 8 | "Like merge, but merges maps recursively. 9 | 10 | (deep-merge {:a {:b {:c 1 :d {:x 1 :y 2}} :e 3} :f 4} 11 | {:a {:b {:c 2 :d {:z 9} :z 3} :e 100}}) 12 | -> {:a {:b {:c 2, :d {:x 1, :y 2, :z 9}, :z 3}, :e 100}, :f 4}" 13 | [& maps] 14 | (if (every? map? maps) 15 | (apply merge-with deep-merge maps) 16 | (last maps))) 17 | 18 | (defprotocol ConfigFile 19 | (etag [_]) 20 | (slurp* [_])) 21 | 22 | (defn- safe-slurp [f default] 23 | (try 24 | (slurp f) 25 | (catch Exception e 26 | default))) 27 | 28 | (extend-protocol ConfigFile 29 | java.io.File 30 | (etag [f] {:file f 31 | :last-mod (.lastModified f)}) 32 | (slurp* [f] (safe-slurp f (pr-str {}))) 33 | 34 | java.net.URL 35 | (etag [url] 36 | (case (.getProtocol url) 37 | "file" (etag (io/as-file url)) 38 | 39 | ;; otherwise, we presume the config file is read-only 40 | ;; (i.e. in a JAR file) 41 | {:url url})) 42 | (slurp* [url] (safe-slurp url (pr-str {}))) 43 | 44 | nil 45 | (etag [_] nil) 46 | (slurp* [_] (pr-str {})) 47 | 48 | java.lang.String 49 | (etag [s] s) 50 | (slurp* [s] s)) 51 | 52 | (defn- get-hostname [] 53 | (.. java.net.InetAddress getLocalHost getHostName)) 54 | 55 | (defn- get-instance [] 56 | (get (System/getenv) "NOMAD_INSTANCE" :default)) 57 | 58 | (defn- get-environment [] 59 | (or (System/getProperty "nomad.env") 60 | (get (System/getenv) "NOMAD_ENV") 61 | :default)) 62 | 63 | (defn read-env-var [var-key] 64 | (or (System/getenv var-key) :nomad/nil)) 65 | 66 | (defn format-env-vars [[fmt & args]] 67 | (apply format fmt (map #(System/getenv (str %)) args))) 68 | 69 | (defn read-jvm-prop [prop-key] 70 | (or (System/getProperty prop-key) :nomad/nil)) 71 | 72 | (defn parse-edn [s] 73 | (or 74 | (try 75 | (edn/read-string s) 76 | (catch Throwable e 77 | (throw (ex-info "Can't parse EDN:" 78 | {:val-str s})))) 79 | 80 | ;; This does return :nomad/nil when the env-var/JVM prop is literal 81 | ;; nil (i.e. VAR=nil lein repl) but not sure I can fix this 82 | ;; until tools.reader accepts nil as a return value from a 83 | ;; reader macro fn 84 | :nomad/nil)) 85 | 86 | (defn- nomad-data-readers [snippet-reader] 87 | {'nomad/file io/file 88 | 'nomad/snippet snippet-reader 89 | 'nomad/env-var read-env-var 90 | 'nomad/edn-env-var (comp parse-edn read-env-var) 91 | 'nomad/envf format-env-vars 92 | 'nomad/jvm-prop read-jvm-prop 93 | 'nomad/edn-jvm-prop (comp parse-edn read-jvm-prop)}) 94 | 95 | (defn- replace-nomad-nils [m] 96 | (postwalk-replace {:nomad/nil nil} m)) 97 | 98 | (defn- readers-without-snippets [] 99 | (nomad-data-readers (constantly ::snippet))) 100 | 101 | (defn- readers-with-snippets [snippets] 102 | (nomad-data-readers 103 | (fn [ks] 104 | (or 105 | (get-in snippets ks) 106 | (throw (ex-info "No snippet found for keys" {:keys ks})))))) 107 | 108 | (defn- reload-config-file [config-file] 109 | (let [config-str (slurp* config-file) 110 | without-snippets (edn/read-string {:readers (merge (readers-without-snippets) *data-readers*)} 111 | config-str) 112 | snippets (get without-snippets :nomad/snippets) 113 | with-snippets (-> (edn/read-string {:readers (merge (readers-with-snippets snippets) *data-readers*)} 114 | config-str) 115 | (dissoc :nomad/snippets) 116 | replace-nomad-nils)] 117 | {:etag (etag config-file) 118 | :config-file config-file 119 | :config with-snippets})) 120 | 121 | (defn- update-config-file [current-config config-file] 122 | (let [{old-etag :etag} current-config 123 | new-etag (etag config-file)] 124 | (if (not= old-etag new-etag) 125 | (reload-config-file config-file) 126 | current-config))) 127 | 128 | (defn- update-specific-config [current-config downstream-key upstream-key selector value] 129 | (let [{new-etag :etag 130 | new-upstream-config :config} (get current-config upstream-key) 131 | 132 | {old-etag :upstream-etag 133 | old-selector-value :selector-value 134 | :as current-downstream-config} (get current-config downstream-key)] 135 | (assoc current-config 136 | downstream-key (if (and (= new-etag old-etag) 137 | (= old-selector-value value)) 138 | current-downstream-config 139 | {:upstream-etag new-etag 140 | :etag new-etag 141 | :selector-value value 142 | :config (get-in new-upstream-config [selector value])})))) 143 | 144 | (defn- add-location [configs] 145 | (assoc configs 146 | :location {:nomad/environment (get-environment) 147 | :nomad/hostname (get-hostname) 148 | :nomad/instance (get-instance)})) 149 | 150 | (defn- update-private-config [configs src-key dest-key] 151 | (let [{old-public-etag :public-etag 152 | :as current-config} (get configs dest-key) 153 | 154 | {new-public-etag :etag} (get configs src-key) 155 | private-file (get-in configs [src-key :config :nomad/private-file])] 156 | (assoc configs 157 | dest-key (if (not= old-public-etag new-public-etag) 158 | (reload-config-file private-file) 159 | (update-config-file current-config private-file))))) 160 | 161 | (defn- merge-configs [configs] 162 | (-> (deep-merge (or (get-in configs [:general :config]) {}) 163 | (or (get-in configs [:general-private :config]) {}) 164 | (or (get-in configs [:host :config]) {}) 165 | (or (get-in configs [:host-private :config]) {}) 166 | (or (get-in configs [:environment :config]) {}) 167 | (or (get-in configs [:environment-private :config]) {}) 168 | (or (get-in configs [:instance :config]) {}) 169 | (or (get-in configs [:instance-private :config]) {}) 170 | (or (get-in configs [:location]) {})) 171 | (dissoc :nomad/hosts :nomad/instances :nomad/environments :nomad/private-file) 172 | (with-meta configs))) 173 | 174 | (defn- update-config [current-config] 175 | (-> current-config 176 | (update-in [:general] update-config-file (get-in current-config [:general :config-file])) 177 | (update-specific-config :environment :general :nomad/environments (get-environment)) 178 | (update-specific-config :host :general :nomad/hosts (get-hostname)) 179 | (update-specific-config :instance :host :nomad/instances (get-instance)) 180 | add-location 181 | (update-private-config :general :general-private) 182 | (update-private-config :environment :environment-private) 183 | (update-private-config :host :host-private) 184 | (update-private-config :instance :instance-private))) 185 | 186 | ;; ---------- PUBLIC API ---------- 187 | 188 | ;; This incarnation of Nomad has been deprecated. Please see `nomad.config` and the README for more details. 189 | 190 | (defn ^:deprecated read-config [file-or-resource & [{:keys [cached-config]}]] 191 | (let [config-map (or (meta cached-config) 192 | {:general {:config-file file-or-resource}}) 193 | updated-config (update-config config-map)] 194 | (merge-configs updated-config))) 195 | 196 | (defmacro ^:deprecated with-location-override [override-map & body] 197 | `(with-redefs [~@(mapcat (fn [[override-sym override-value]] 198 | [(symbol (str "nomad/get-" (name override-sym))) `(constantly ~override-value)]) 199 | override-map)] 200 | ~@body)) 201 | 202 | (defmacro ^:deprecated defconfig [name file-or-resource & [{:keys [data-readers]}]] 203 | `(let [!cached-config# (atom nil)] 204 | (defn ~name [] 205 | (swap! !cached-config# 206 | (fn [cached-config#] 207 | (binding [*data-readers* (merge *data-readers* ~data-readers)] 208 | (read-config ~file-or-resource 209 | {:cached-config cached-config#}))))))) 210 | -------------------------------------------------------------------------------- /src/nomad/config.clj: -------------------------------------------------------------------------------- 1 | (ns nomad.config 2 | (:require [buddy.core.codecs :as bc] 3 | [buddy.core.codecs.base64 :as b64] 4 | [buddy.core.crypto :as b] 5 | [buddy.core.nonce :as bn] 6 | [clojure.set :as set] 7 | [clojure.tools.reader.edn :as edn] 8 | [clojure.string :as s] 9 | [clojure.walk :as w])) 10 | 11 | (def ^:dynamic *opts* nil) 12 | (def !clients (atom #{})) 13 | 14 | (defrecord Secret [key-id cipher-text]) 15 | 16 | (defn generate-key [] 17 | (bc/bytes->str (b64/encode (bn/random-bytes 32)))) 18 | 19 | (def ^:private block-size 20 | (b/block-size (b/block-cipher :aes :cbc))) 21 | 22 | (defn- resolve-secret-key [secret-key] 23 | (cond 24 | (string? secret-key) secret-key 25 | (keyword? secret-key) (or (get (:secret-keys *opts*) secret-key) 26 | (throw (ex-info "missing secret-key" {"secret-key" secret-key}))) 27 | :else (throw (ex-info "invalid secret-key" {:secret-key secret-key})))) 28 | 29 | (defn encrypt [secret-key plain-obj] 30 | (let [iv (bn/random-bytes block-size)] 31 | (->> [iv (b/encrypt (bc/str->bytes (pr-str plain-obj)) (b64/decode (bc/str->bytes (resolve-secret-key secret-key))) iv)] 32 | (mapcat seq) 33 | byte-array 34 | b64/encode 35 | bc/bytes->str))) 36 | 37 | (defn decrypt [secret-key cipher-text] 38 | (let [[iv cipher-bytes] (map byte-array (split-at block-size (b64/decode cipher-text)))] 39 | (edn/read-string (b/decrypt cipher-bytes (b64/decode (bc/str->bytes (resolve-secret-key secret-key))) iv)))) 40 | 41 | (defmacro switch 42 | "Takes a set of switch/expr clauses, and an optional default value. 43 | Returns the configuration from the first active switch, or the default if none are active, or nil. 44 | 45 | (n/switch 46 | 47 | 48 | ... 49 | )" 50 | {:style/indent 0} 51 | [& opts+clauses] 52 | (let [switches-sym (gensym 'switches) 53 | [opts clauses] (if (map? (first opts+clauses)) 54 | [(first opts+clauses) (rest opts+clauses)] 55 | [{} opts+clauses])] 56 | `(let [{override-key# ::override-key} ~opts 57 | ~switches-sym (set (:switches *opts*))] 58 | (cond 59 | ~@(for [[clause expr] (partition 2 clauses) 60 | form [`(some ~switches-sym ~(cond 61 | (set? clause) clause 62 | (keyword? clause) #{clause})) 63 | expr]] 64 | form) 65 | :else ~(when (pos? (mod (count clauses) 2)) 66 | (last clauses)))))) 67 | 68 | (defn add-client! [var] 69 | (let [{var-ns :ns, var-name :name} (meta var)] 70 | (swap! !clients conj (symbol (str var-ns) (name var-name))))) 71 | 72 | (defmacro defconfig [sym config] 73 | (let [config-sym (gensym 'config)] 74 | `(let [~config-sym (-> (fn [opts#] 75 | (binding [*opts* opts#] 76 | ~config)) 77 | memoize)] 78 | (doto (def ~(-> sym (with-meta {:dynamic true})) 79 | (when *opts* 80 | (~config-sym *opts*))) 81 | (alter-meta! assoc :nomad/config ~config-sym) 82 | add-client!)))) 83 | 84 | (defn parse-switches [switches] 85 | (some-> switches 86 | (s/split #",") 87 | (->> (into #{} (map keyword))))) 88 | 89 | (def env-switches 90 | (->> [(System/getenv "NOMAD_SWITCHES") 91 | (System/getProperty "nomad.switches")] 92 | (into #{} (mapcat parse-switches)))) 93 | 94 | (defn eval-config [config-var {:keys [switches secret-keys override-switches]}] 95 | ((:nomad/config (meta config-var)) {:switches (get override-switches config-var switches), :secret-keys secret-keys})) 96 | 97 | (defn set-defaults! [{:keys [switches secret-keys override-switches], :as defaults}] 98 | (alter-var-root #'*opts* merge defaults) 99 | (doseq [client @!clients] 100 | (when-let [config-var (resolve client)] 101 | (alter-var-root config-var (constantly (eval-config config-var *opts*)))))) 102 | 103 | (defn with-config-override* [{:keys [switches secret-keys override-switches] :as opts-override} f] 104 | (let [opts-override (merge *opts* opts-override)] 105 | (binding [*opts* opts-override] 106 | (with-bindings (into {} 107 | (keep (fn [client] 108 | (when-let [config-var (resolve client)] 109 | [config-var (eval-config config-var opts-override)]))) 110 | @!clients) 111 | (f))))) 112 | 113 | (doto (defmacro with-config-override [opts & body] 114 | `(with-config-override* ~opts (fn [] ~@body))) 115 | 116 | (alter-meta! assoc :arglists '([{:keys [switches secret-keys override-switches] :as opts-override} & body]))) 117 | -------------------------------------------------------------------------------- /test/nomad/config_test.clj: -------------------------------------------------------------------------------- 1 | (ns nomad.config-test 2 | (:require [nomad.config :as sut] 3 | [clojure.test :as t])) 4 | 5 | (t/deftest applies-switches 6 | (t/is (= (#'sut/apply-switches {:config-key :value 7 | 8 | :switched? (sut/switch 9 | :the-switch true 10 | false) 11 | 12 | :overruled? (sut/switch 13 | :other-switch true 14 | :the-switch false) 15 | 16 | :uh-oh? (sut/switch 17 | :not-this-one true)} 18 | 19 | #{:the-switch :other-switch}) 20 | {:config-key :value 21 | :switched? true 22 | :overruled? true 23 | :uh-oh? nil}))) 24 | 25 | (t/deftest applies-keyed-switches 26 | (t/is (= (#'sut/apply-switches {:keyed-switch (sut/switch 27 | :live :not-this-one)} 28 | #{:keyed/live}) 29 | {:keyed-switch nil})) 30 | 31 | (t/is (= (#'sut/apply-switches {:keyed-switch (sut/switch 32 | :live :yeehah) 33 | :nomad/key :keyed} 34 | #{:keyed/live}) 35 | 36 | {:keyed-switch :yeehah}))) 37 | 38 | (t/deftest resolves-secrets 39 | (let [secret-key (sut/generate-key)] 40 | (t/is (= (-> (sut/resolve-config {:db-config {:password (sut/->Secret :my-key (sut/encrypt "password123" secret-key))}} 41 | {:secret-keys {:my-key secret-key}}) 42 | (get-in [:db-config :password])) 43 | "password123")))) 44 | --------------------------------------------------------------------------------