constancy 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +117 -24
- data/bin/constancy +1 -1
- data/lib/constancy.rb +6 -4
- data/lib/constancy/cli.rb +16 -7
- data/lib/constancy/cli/config_command.rb +26 -18
- data/lib/constancy/cli/pull_command.rb +2 -2
- data/lib/constancy/cli/push_command.rb +2 -2
- data/lib/constancy/cli/targets_command.rb +21 -0
- data/lib/constancy/config.rb +56 -103
- data/lib/constancy/diff.rb +1 -1
- data/lib/constancy/sync_target.rb +63 -10
- data/lib/constancy/token_source.rb +94 -0
- data/lib/constancy/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f4ece9e3ea596821b57e8e4e2f4e11d70833df49f3480082a4438a1a018a69b
|
4
|
+
data.tar.gz: 5cead608d291729245435375774093004032877b5e4e447aa7ebf0ff8a7b0e2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b40eb6bc0491fce5719d10c44cd11b154f71ea6415a5f5564b116ed7814a8d796deb046cf18e6d74cf17b8da15a5726a094141ae491506d1e5a3739c192f6b6
|
7
|
+
data.tar.gz: c45737ecc9a1c1aa92b0bfb46594dbe5ce8be6dac55f4e9efb3e30655b65bec9ec1cccec1895a0805da86c9b8bd6193f5116b11798c3e2a1a24d2f6d3b159d24
|
data/README.md
CHANGED
@@ -20,14 +20,14 @@ synchronize the changes from the filesystem to Consul.
|
|
20
20
|
local:consul/config => consul:dc1:config/myapp
|
21
21
|
Keys scanned: 80
|
22
22
|
|
23
|
-
UPDATE config/myapp/prod/ip-
|
23
|
+
UPDATE config/myapp/prod/ip-allowlist.json
|
24
24
|
-------------------------------------------------------------------------------------
|
25
25
|
-["10.8.0.0/16"]
|
26
26
|
+["10.8.0.0/16","10.9.10.0/24"]
|
27
27
|
-------------------------------------------------------------------------------------
|
28
28
|
|
29
29
|
Keys to update: 1
|
30
|
-
~ config/myapp/prod/ip-
|
30
|
+
~ config/myapp/prod/ip-allowlist.json
|
31
31
|
|
32
32
|
You can also limit your command to specific synchronization targets by using
|
33
33
|
the `--target` flag:
|
@@ -38,19 +38,19 @@ the `--target` flag:
|
|
38
38
|
local:consul/config => consul:dc1:config/myapp
|
39
39
|
Keys scanned: 80
|
40
40
|
|
41
|
-
UPDATE config/myapp/prod/ip-
|
41
|
+
UPDATE config/myapp/prod/ip-allowlist.json
|
42
42
|
-------------------------------------------------------------------------------------
|
43
43
|
-["10.8.0.0/16"]
|
44
44
|
+["10.8.0.0/16","10.9.10.0/24"]
|
45
45
|
-------------------------------------------------------------------------------------
|
46
46
|
|
47
47
|
Keys to update: 1
|
48
|
-
~ config/myapp/prod/ip-
|
48
|
+
~ config/myapp/prod/ip-allowlist.json
|
49
49
|
|
50
50
|
Do you want to push these changes?
|
51
51
|
Enter 'yes' to continue: yes
|
52
52
|
|
53
|
-
UPDATE config/myapp/prod/ip-
|
53
|
+
UPDATE config/myapp/prod/ip-allowlist.json OK
|
54
54
|
|
55
55
|
Run `constancy --help` for additional options and commands.
|
56
56
|
|
@@ -127,6 +127,9 @@ required. An example `constancy.yml` is below including explanatory comments:
|
|
127
127
|
# 'none': expect no Consul token (although env vars will be used if they are set)
|
128
128
|
# 'env': expect Consul token to be set in CONSUL_TOKEN or CONSUL_HTTP_TOKEN
|
129
129
|
# 'vault': read Consul token from Vault based on settings in the 'vault' section
|
130
|
+
# 'vault.<label>': a named Vault token source, eg `vault.us-east-1` or `vault.dev`
|
131
|
+
# NOTE: labels must begin with a letter and may contain only (ASCII) letters,
|
132
|
+
# numbers, hyphens, and underscores
|
130
133
|
|
131
134
|
# the vault section is only necessary if consul.token_source is set to 'vault'
|
132
135
|
vault:
|
@@ -146,6 +149,15 @@ required. An example `constancy.yml` is below including explanatory comments:
|
|
146
149
|
# but can be set to something else for static values.
|
147
150
|
consul_token_field: token
|
148
151
|
|
152
|
+
# You can define one or more 'vault.<label>' sections to define alternative Vault
|
153
|
+
# token sources for use in individual sync targets.
|
154
|
+
vault.other:
|
155
|
+
url: https://your.vault.example
|
156
|
+
consul_token_path: consul/creds/my-other-role
|
157
|
+
vault.dev:
|
158
|
+
url: https://dev.vault.example
|
159
|
+
consul_token_path: consul/creds/my-dev-role
|
160
|
+
|
149
161
|
sync:
|
150
162
|
# sync is an array of hashes of sync target configurations
|
151
163
|
# Fields:
|
@@ -167,6 +179,10 @@ required. An example `constancy.yml` is below including explanatory comments:
|
|
167
179
|
# containing a hash of remote keys if this sync target has
|
168
180
|
# type=file. This path is calculated relative to the directory
|
169
181
|
# containing the configuration file.
|
182
|
+
# token_source - An alternative token source other than the
|
183
|
+
# default. Potential values are the same as for the
|
184
|
+
# consul.token_source config value: 'none', 'env', 'vault',
|
185
|
+
# or 'vault.<label>'.
|
170
186
|
# delete - Whether or not to delete remote keys that do not exist
|
171
187
|
# in the local filesystem. This inherits the setting from the
|
172
188
|
# `constancy` section, or if not specified, defaults to `false`.
|
@@ -180,6 +196,9 @@ required. An example `constancy.yml` is below including explanatory comments:
|
|
180
196
|
# ignored. At this time there is no provision for specifying
|
181
197
|
# prefixes or patterns. Each key must be fully and explicitly
|
182
198
|
# specified.
|
199
|
+
# erb_enabled - Whether or not to run the local content through
|
200
|
+
# ERB parsing before attempting to sync to the remote. Defaults
|
201
|
+
# to `false`.
|
183
202
|
- name: myapp-config
|
184
203
|
prefix: config/myapp
|
185
204
|
datacenter: dc1
|
@@ -192,6 +211,7 @@ required. An example `constancy.yml` is below including explanatory comments:
|
|
192
211
|
type: dir
|
193
212
|
datacenter: dc1
|
194
213
|
path: consul/private
|
214
|
+
token_source: vault.dev
|
195
215
|
delete: true
|
196
216
|
- name: yourapp-config
|
197
217
|
prefix: config/yourapp
|
@@ -199,6 +219,7 @@ required. An example `constancy.yml` is below including explanatory comments:
|
|
199
219
|
datacenter: dc1
|
200
220
|
path: consul/yourapp.yml
|
201
221
|
delete: true
|
222
|
+
erb_enabled: true
|
202
223
|
|
203
224
|
You can run `constancy config` to get a summary of the defined configuration
|
204
225
|
and to double-check config syntax.
|
@@ -222,33 +243,98 @@ If the file `yourapp.yml` has the following content:
|
|
222
243
|
prod/dbname: yourapp
|
223
244
|
prod/message: |
|
224
245
|
Hello, world. This is a multiline message.
|
225
|
-
|
226
|
-
Thanks,
|
227
|
-
YourApp
|
246
|
+
Thanks.
|
228
247
|
prod/app/config.json: |
|
229
248
|
{
|
230
249
|
"port": 8080,
|
231
|
-
"listen": "0.0.0.0",
|
232
250
|
"enabled": true
|
233
251
|
}
|
234
252
|
|
235
253
|
Then `constancy push` will attempt to create and/or update the following keys
|
236
254
|
with the corresponding content from `yourapp.yml`:
|
237
255
|
|
238
|
-
|
239
|
-
|
240
|
-
|
256
|
+
| Key | Value |
|
257
|
+
|:-----|:-------|
|
258
|
+
| `config/yourapp/prod/dbname` | `yourapp` |
|
259
|
+
| `config/yourapp/prod/message` | `Hello, world. This is a multiline message.\nThanks.` |
|
260
|
+
| `config/yourapp/prod/app/config.json` | `{\n "port": 8080,\n "enabled": true\n}` |
|
261
|
+
|
262
|
+
In addition to specifying the entire relative path in each key, you may also
|
263
|
+
reference paths via your file's YAML structure directly. For example:
|
264
|
+
|
265
|
+
---
|
266
|
+
prod:
|
267
|
+
redis:
|
268
|
+
port: 6380
|
269
|
+
host: redis.example.com
|
270
|
+
|
271
|
+
When pushed, this document will create and/or update the following keys:
|
272
|
+
|
273
|
+
| Key | Value |
|
274
|
+
|:-----|:-------|
|
275
|
+
| `config/yourapp/prod/redis/port` | `6380` |
|
276
|
+
| `config/yourapp/prod/redis/host` | `redis.example.com` |
|
277
|
+
|
278
|
+
You may mix and match relative paths and document hierarchy to build paths as
|
279
|
+
you would like. And you may also use the special key `_` to embed a value for
|
280
|
+
a particular prefix while also nesting values underneath it. For example, given
|
281
|
+
this local file target content:
|
282
|
+
|
283
|
+
---
|
284
|
+
prod/postgres:
|
285
|
+
host: db.myproject.example.com
|
286
|
+
port: 10001
|
287
|
+
|
288
|
+
prod:
|
289
|
+
redis:
|
290
|
+
_: Embedded Value
|
291
|
+
port: 6380
|
292
|
+
|
293
|
+
prod/redis/host: cache.myproject.example.com
|
241
294
|
|
242
|
-
|
243
|
-
from any keys under `config/yourapp/` into the file `yourapp.yml`, overwriting
|
244
|
-
whatever values are there.
|
295
|
+
This file target content would correspond to the following values, when pushed:
|
245
296
|
|
246
|
-
|
247
|
-
|
248
|
-
|
297
|
+
| Key | Value |
|
298
|
+
|:-----|:-------|
|
299
|
+
| `config/yourapp/prod/postgres/host` | `db.myproject.example.com` |
|
300
|
+
| `config/yourapp/prod/postgres/port` | `10001` |
|
301
|
+
| `config/yourapp/prod/redis` | `Embedded Value` |
|
302
|
+
| `config/yourapp/prod/redis/port` | `6380` |
|
303
|
+
| `config/yourapp/prod/redis/host` | `cache.myproject.example.com` |
|
249
304
|
|
250
|
-
|
251
|
-
|
305
|
+
A `constancy pull` operation against a file type target will work in reverse,
|
306
|
+
and pull values from any keys under `config/yourapp/` into the file
|
307
|
+
`yourapp.yml`, overwriting whatever values are there.
|
308
|
+
|
309
|
+
**NOTE**: Values in local file targets are converted to strings before comparing
|
310
|
+
with or uploading to the remote Consul server. However, because YAML parsing
|
311
|
+
converts some values (such as `yes` or `no`) to boolean types, the effective
|
312
|
+
value of a key with a value of a bare `yes` will be `true` when converted to a
|
313
|
+
string. If you need the actual values `yes` or `no`, use quotes around the value
|
314
|
+
to force the YAML parser to interpret it as a string.
|
315
|
+
|
316
|
+
|
317
|
+
#### IMPORTANT NOTES ABOUT PULL MODE WITH FILE TARGETS
|
318
|
+
|
319
|
+
Against a file target, the structure of the local file can vary in a number
|
320
|
+
of ways while still producing the same remote structure. Thus, in pull mode,
|
321
|
+
Constancy must necessarily choose one particular rendering format, and will not
|
322
|
+
be able to retain the exact structure of the local file if you alternate push
|
323
|
+
and pull operations.
|
324
|
+
|
325
|
+
Specifically, the following caveats are important to note, when pulling a target
|
326
|
+
to a local file:
|
327
|
+
|
328
|
+
* The local file will be written out as YAML, even if it was originally
|
329
|
+
provided locally as a JSON file, and even if the extension is `.json`.
|
330
|
+
|
331
|
+
* Any existing comments in the local file will be lost.
|
332
|
+
|
333
|
+
* The document structure will be that of a flat hash will fully-specified
|
334
|
+
relative paths as the keys.
|
335
|
+
|
336
|
+
Future versions of Constancy may provide options to modify the behavior for pull
|
337
|
+
operations on a per-target basis. Pull requests are always welcome.
|
252
338
|
|
253
339
|
|
254
340
|
### Dynamic configuration
|
@@ -270,6 +356,12 @@ It's a good idea to sanity-check your ERB by running `constancy config` after
|
|
270
356
|
making any changes.
|
271
357
|
|
272
358
|
|
359
|
+
### Dynamic content
|
360
|
+
|
361
|
+
You can also choose to enable ERB parsing for local content as well, by setting
|
362
|
+
`erb_enabled: true` on any sync targets you wish to populate in this way.
|
363
|
+
|
364
|
+
|
273
365
|
### Environment configuration
|
274
366
|
|
275
367
|
Constancy may be partially configured using environment variables:
|
@@ -279,10 +371,10 @@ Constancy may be partially configured using environment variables:
|
|
279
371
|
interacting with the API. Otherwise, by default the agent's `acl_token`
|
280
372
|
setting is used implicitly.
|
281
373
|
* `VAULT_ADDR` and `VAULT_TOKEN` - if `consul.token_source` is set to `vault`
|
282
|
-
these variables are used to authenticate to Vault. If
|
283
|
-
set, Constancy will attempt to read a token from
|
284
|
-
`url` field is set, it will take priority over the
|
285
|
-
variable, but one or the other must be set.
|
374
|
+
or `vault.<label>`, these variables are used to authenticate to Vault. If
|
375
|
+
`VAULT_TOKEN` is not set, Constancy will attempt to read a token from
|
376
|
+
`~/.vault-token`. If the `url` field is set, it will take priority over the
|
377
|
+
`VAULT_ADDR` environment variable, but one or the other must be set.
|
286
378
|
|
287
379
|
|
288
380
|
## Roadmap
|
@@ -291,6 +383,7 @@ Constancy is relatively new software. There's more to be done. Some ideas, which
|
|
291
383
|
may or may not ever be implemented:
|
292
384
|
|
293
385
|
* Using CAS to verify the key has not changed in the interim before updating/deleting
|
386
|
+
* Options for file target pull-mode rendering
|
294
387
|
* Automation support for running non-interactively
|
295
388
|
* Pattern- and prefix-based exclusions
|
296
389
|
* Logging of changes to files, syslog, other services
|
data/bin/constancy
CHANGED
data/lib/constancy.rb
CHANGED
@@ -4,12 +4,14 @@ require 'erb'
|
|
4
4
|
require 'imperium'
|
5
5
|
require 'fileutils'
|
6
6
|
require 'ostruct'
|
7
|
+
require 'vault'
|
7
8
|
require 'yaml'
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
require_relative 'constancy/version'
|
11
|
+
require_relative 'constancy/config'
|
12
|
+
require_relative 'constancy/token_source'
|
13
|
+
require_relative 'constancy/diff'
|
14
|
+
require_relative 'constancy/sync_target'
|
13
15
|
|
14
16
|
class Constancy
|
15
17
|
class InternalError < RuntimeError; end
|
data/lib/constancy/cli.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
2
|
|
3
|
-
|
3
|
+
require_relative '../constancy'
|
4
4
|
require 'diffy'
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
require_relative 'cli/check_command'
|
6
|
+
require_relative 'cli/push_command'
|
7
|
+
require_relative 'cli/pull_command'
|
8
|
+
require_relative 'cli/config_command'
|
9
|
+
require_relative 'cli/targets_command'
|
9
10
|
|
10
11
|
class Constancy
|
11
12
|
class CLI
|
@@ -56,6 +57,7 @@ Commands:
|
|
56
57
|
push Push changes from filesystem to Consul
|
57
58
|
pull Pull changes from Consul to filesystem
|
58
59
|
config Print a summary of the active configuration
|
60
|
+
targets List target names
|
59
61
|
|
60
62
|
General options:
|
61
63
|
--help Print help for the given command
|
@@ -65,6 +67,12 @@ General options:
|
|
65
67
|
Options for 'check' command:
|
66
68
|
--pull Perform dry run in pull mode
|
67
69
|
|
70
|
+
Options for 'pull' command:
|
71
|
+
--yes Skip confirmation prompt
|
72
|
+
|
73
|
+
Options for 'push' command:
|
74
|
+
--yes Skip confirmation prompt
|
75
|
+
|
68
76
|
USAGE
|
69
77
|
exit 1
|
70
78
|
end
|
@@ -122,9 +130,10 @@ USAGE
|
|
122
130
|
when :command
|
123
131
|
case self.command
|
124
132
|
when 'check' then Constancy::CLI::CheckCommand.run(self.extra_args)
|
125
|
-
when 'push' then Constancy::CLI::PushCommand.run
|
126
|
-
when 'pull' then Constancy::CLI::PullCommand.run
|
133
|
+
when 'push' then Constancy::CLI::PushCommand.run(self.extra_args)
|
134
|
+
when 'pull' then Constancy::CLI::PullCommand.run(self.extra_args)
|
127
135
|
when 'config' then Constancy::CLI::ConfigCommand.run
|
136
|
+
when 'targets' then Constancy::CLI::TargetsCommand.run
|
128
137
|
when nil then self.print_usage
|
129
138
|
|
130
139
|
else
|
@@ -8,18 +8,25 @@ class Constancy
|
|
8
8
|
Constancy::CLI.configure(call_external_apis: false)
|
9
9
|
|
10
10
|
puts " Config file: #{Constancy.config.config_file}"
|
11
|
-
puts " Consul URL: #{Constancy.config.
|
11
|
+
puts " Consul URL: #{Constancy.config.consul_url}"
|
12
12
|
puts " Verbose: #{Constancy.config.verbose?.to_s.bold}"
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
puts
|
14
|
+
puts " Defined Consul Token Sources:"
|
15
|
+
default_src_name = Constancy.config.default_consul_token_source.name
|
16
|
+
srcs = Constancy.config.consul_token_sources
|
17
|
+
( %w( none env ) + ( srcs.keys.sort - %w( none env ) ) ).each do |name|
|
18
18
|
puts
|
19
|
-
puts "
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
puts " #{name}:#{ default_src_name == name ? " (DEFAULT)".bold : ""}"
|
20
|
+
case name
|
21
|
+
when "none"
|
22
|
+
puts " uses CONSUL_HTTP_TOKEN or CONSUL_TOKEN env var if available"
|
23
|
+
when "env"
|
24
|
+
puts " requires CONSUL_HTTP_TOKEN or CONSUL_TOKEN env var"
|
25
|
+
when /^vault/
|
26
|
+
puts " address: #{srcs[name].vault_addr}"
|
27
|
+
puts " path: #{srcs[name].consul_token_path}"
|
28
|
+
puts " field: #{srcs[name].consul_token_field}"
|
29
|
+
end
|
23
30
|
end
|
24
31
|
puts
|
25
32
|
puts "Sync target defaults:"
|
@@ -35,16 +42,17 @@ class Constancy
|
|
35
42
|
else
|
36
43
|
print '*'
|
37
44
|
end
|
38
|
-
puts "
|
39
|
-
puts "
|
40
|
-
puts "
|
41
|
-
puts "
|
42
|
-
puts "
|
43
|
-
puts "
|
45
|
+
puts " Datacenter: #{target.datacenter}"
|
46
|
+
puts " Local type: #{target.type == :dir ? 'Directory' : 'Single file'}"
|
47
|
+
puts " #{target.type == :dir ? " Dir" : "File"} path: #{target.path}"
|
48
|
+
puts " Prefix: #{target.prefix}"
|
49
|
+
puts " Token Source: #{target.token_source.name}"
|
50
|
+
puts " Autochomp? #{target.chomp?}"
|
51
|
+
puts " Delete? #{target.delete?}"
|
44
52
|
if not target.exclude.empty?
|
45
|
-
puts "
|
53
|
+
puts " Exclusions:"
|
46
54
|
target.exclude.each do |exclusion|
|
47
|
-
puts "
|
55
|
+
puts " - #{exclusion}"
|
48
56
|
end
|
49
57
|
end
|
50
58
|
puts
|
@@ -4,7 +4,7 @@ class Constancy
|
|
4
4
|
class CLI
|
5
5
|
class PullCommand
|
6
6
|
class << self
|
7
|
-
def run
|
7
|
+
def run(args)
|
8
8
|
Constancy::CLI.configure
|
9
9
|
STDOUT.sync = true
|
10
10
|
|
@@ -22,7 +22,7 @@ class Constancy
|
|
22
22
|
puts
|
23
23
|
puts "Do you want to pull these changes?"
|
24
24
|
print " Enter '" + "yes".bold + "' to continue: "
|
25
|
-
answer = gets.chomp
|
25
|
+
answer = args.include?('--yes') ? 'yes' : gets.chomp
|
26
26
|
|
27
27
|
if answer.downcase != "yes"
|
28
28
|
puts
|
@@ -4,7 +4,7 @@ class Constancy
|
|
4
4
|
class CLI
|
5
5
|
class PushCommand
|
6
6
|
class << self
|
7
|
-
def run
|
7
|
+
def run(args)
|
8
8
|
Constancy::CLI.configure
|
9
9
|
STDOUT.sync = true
|
10
10
|
|
@@ -22,7 +22,7 @@ class Constancy
|
|
22
22
|
puts
|
23
23
|
puts "Do you want to push these changes?"
|
24
24
|
print " Enter '" + "yes".bold + "' to continue: "
|
25
|
-
answer = gets.chomp
|
25
|
+
answer = args.include?('--yes') ? 'yes' : gets.chomp
|
26
26
|
|
27
27
|
if answer.downcase != "yes"
|
28
28
|
puts
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Constancy
|
4
|
+
class CLI
|
5
|
+
class TargetsCommand
|
6
|
+
class << self
|
7
|
+
def run
|
8
|
+
Constancy::CLI.configure(call_external_apis: false)
|
9
|
+
|
10
|
+
Constancy.config.sync_targets.each do |target|
|
11
|
+
if target.name
|
12
|
+
puts target.name
|
13
|
+
else
|
14
|
+
puts "[unnamed target] #{target.datacenter}:#{target.prefix}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/constancy/config.rb
CHANGED
@@ -11,6 +11,8 @@ class Constancy
|
|
11
11
|
class Config
|
12
12
|
CONFIG_FILENAMES = %w( constancy.yml )
|
13
13
|
VALID_CONFIG_KEYS = %w( sync consul vault constancy )
|
14
|
+
VALID_VAULT_KEY_PATTERNS = [ %r{^vault\.[A-Za-z][A-Za-z0-9_-]*$}, %r{^vault$} ]
|
15
|
+
VALID_CONFIG_KEY_PATTERNS = VALID_VAULT_KEY_PATTERNS
|
14
16
|
VALID_CONSUL_CONFIG_KEYS = %w( url datacenter token_source )
|
15
17
|
VALID_VAULT_CONFIG_KEYS = %w( url consul_token_path consul_token_field )
|
16
18
|
VALID_CONSTANCY_CONFIG_KEYS = %w( verbose chomp delete color )
|
@@ -18,7 +20,8 @@ class Constancy
|
|
18
20
|
DEFAULT_CONSUL_TOKEN_SOURCE = "none"
|
19
21
|
DEFAULT_VAULT_CONSUL_TOKEN_FIELD = "token"
|
20
22
|
|
21
|
-
attr_accessor :config_file, :base_dir, :
|
23
|
+
attr_accessor :config_file, :base_dir, :consul_url, :default_consul_token_source,
|
24
|
+
:sync_targets, :target_allowlist, :call_external_apis, :consul_token_sources
|
22
25
|
|
23
26
|
class << self
|
24
27
|
# discover the nearest config file
|
@@ -34,6 +37,15 @@ class Constancy
|
|
34
37
|
|
35
38
|
dir == "/" ? nil : self.discover(dir: File.dirname(dir))
|
36
39
|
end
|
40
|
+
|
41
|
+
def only_valid_config_keys!(keylist)
|
42
|
+
(keylist - VALID_CONFIG_KEYS).each do |key|
|
43
|
+
if not VALID_CONFIG_KEY_PATTERNS.find { |pattern| key =~ pattern }
|
44
|
+
raise Constancy::ConfigFileInvalid.new("'#{key}' is not a valid configuration key")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
true
|
48
|
+
end
|
37
49
|
end
|
38
50
|
|
39
51
|
def initialize(path: nil, targets: nil, call_external_apis: true)
|
@@ -51,7 +63,7 @@ class Constancy
|
|
51
63
|
|
52
64
|
self.config_file = File.expand_path(self.config_file)
|
53
65
|
self.base_dir = File.dirname(self.config_file)
|
54
|
-
self.
|
66
|
+
self.target_allowlist = targets
|
55
67
|
self.call_external_apis = call_external_apis
|
56
68
|
parse!
|
57
69
|
end
|
@@ -72,7 +84,11 @@ class Constancy
|
|
72
84
|
@use_color
|
73
85
|
end
|
74
86
|
|
75
|
-
|
87
|
+
def parse_vault_token_sources!(raw)
|
88
|
+
raw.keys.select { |key| VALID_VAULT_KEY_PATTERNS.find { |pattern| key =~ pattern } }.collect do |key|
|
89
|
+
[key, Constancy::VaultTokenSource.new(name: key, config: raw[key])]
|
90
|
+
end.to_h
|
91
|
+
end
|
76
92
|
|
77
93
|
def parse!
|
78
94
|
raw = {}
|
@@ -91,116 +107,40 @@ class Constancy
|
|
91
107
|
raise Constancy::ConfigFileInvalid.new("Config file must form a hash")
|
92
108
|
end
|
93
109
|
|
94
|
-
|
95
|
-
|
96
|
-
|
110
|
+
Constancy::Config.only_valid_config_keys!(raw.keys)
|
111
|
+
|
112
|
+
self.consul_token_sources = {
|
113
|
+
"none" => Constancy::PassiveTokenSource.new,
|
114
|
+
"env" => Constancy::EnvTokenSource.new,
|
115
|
+
}.merge(
|
116
|
+
self.parse_vault_token_sources!(raw),
|
117
|
+
)
|
97
118
|
|
98
119
|
raw['consul'] ||= {}
|
99
120
|
if not raw['consul'].is_a? Hash
|
100
121
|
raise Constancy::ConfigFileInvalid.new("'consul' must be a hash")
|
101
122
|
end
|
102
123
|
|
103
|
-
if (raw['consul'].keys -
|
104
|
-
raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in the consul config: #{
|
124
|
+
if (raw['consul'].keys - VALID_CONSUL_CONFIG_KEYS) != []
|
125
|
+
raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in the consul config: #{VALID_CONSUL_CONFIG_KEYS.join(", ")}")
|
105
126
|
end
|
106
127
|
|
107
|
-
consul_url = raw['consul']['url'] ||
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
case self.consul_token_source
|
114
|
-
when "none"
|
115
|
-
# nothing to do
|
116
|
-
|
117
|
-
when "env"
|
118
|
-
if consul_token.nil? or consul_token == ""
|
119
|
-
raise Constancy::ConsulTokenRequired.new("Consul token_source is set to 'env' but neither CONSUL_TOKEN nor CONSUL_HTTP_TOKEN is set")
|
120
|
-
end
|
121
|
-
|
122
|
-
when "vault"
|
123
|
-
require 'vault'
|
124
|
-
|
125
|
-
raw['vault'] ||= {}
|
126
|
-
if not raw['vault'].is_a? Hash
|
127
|
-
raise Constancy::ConfigFileInvalid.new("'vault' must be a hash")
|
128
|
-
end
|
129
|
-
|
130
|
-
if (raw['vault'].keys - Constancy::Config::VALID_VAULT_CONFIG_KEYS) != []
|
131
|
-
raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in the vault config: #{Constancy::Config::VALID_VAULT_CONFIG_KEYS.join(", ")}")
|
132
|
-
end
|
133
|
-
|
134
|
-
vault_path = raw['vault']['consul_token_path']
|
135
|
-
if vault_path.nil? or vault_path == ""
|
136
|
-
raise Constancy::ConfigFileInvalid.new("vault.consul_token_path must be specified to use vault as a token source")
|
137
|
-
end
|
138
|
-
|
139
|
-
# prioritize the config file over environment variables for vault address
|
140
|
-
vault_addr = raw['vault']['url'] || ENV['VAULT_ADDR']
|
141
|
-
if vault_addr.nil? or vault_addr == ""
|
142
|
-
raise Constancy::VaultConfigInvalid.new("Vault address must be set in vault.url or VAULT_ADDR")
|
143
|
-
end
|
144
|
-
|
145
|
-
vault_token = ENV['VAULT_TOKEN']
|
146
|
-
if vault_token.nil? or vault_token == ""
|
147
|
-
vault_token_file = File.expand_path("~/.vault-token")
|
148
|
-
if File.exist?(vault_token_file)
|
149
|
-
vault_token = File.read(vault_token_file)
|
150
|
-
else
|
151
|
-
raise Constancy::VaultConfigInvalid.new("Vault token must be set in ~/.vault-token or VAULT_TOKEN")
|
128
|
+
self.consul_url = raw['consul']['url'] || DEFAULT_CONSUL_URL
|
129
|
+
srcname = raw['consul']['token_source'] || DEFAULT_CONSUL_TOKEN_SOURCE
|
130
|
+
self.default_consul_token_source =
|
131
|
+
self.consul_token_sources[srcname].tap do |src|
|
132
|
+
if src.nil?
|
133
|
+
raise Constancy::ConfigFileInvalid.new("Consul token source '#{consul_token_source}' is not defined")
|
152
134
|
end
|
153
135
|
end
|
154
136
|
|
155
|
-
vault_field = raw['vault']['consul_token_field'] || Constancy::Config::DEFAULT_VAULT_CONSUL_TOKEN_FIELD
|
156
|
-
|
157
|
-
self.vault_config = OpenStruct.new(
|
158
|
-
url: vault_addr,
|
159
|
-
consul_token_path: vault_path,
|
160
|
-
consul_token_field: vault_field,
|
161
|
-
)
|
162
|
-
|
163
|
-
# don't waste time talking to Vault if this is just a config parsing run
|
164
|
-
if self.call_external_apis
|
165
|
-
ENV['VAULT_ADDR'] = vault_addr
|
166
|
-
ENV['VAULT_TOKEN'] = vault_token
|
167
|
-
|
168
|
-
begin
|
169
|
-
response = Vault.logical.read(vault_path)
|
170
|
-
consul_token = response.data[vault_field.to_sym]
|
171
|
-
|
172
|
-
if response.lease_id
|
173
|
-
at_exit {
|
174
|
-
begin
|
175
|
-
Vault.sys.revoke(response.lease_id)
|
176
|
-
rescue => e
|
177
|
-
# this is fine
|
178
|
-
end
|
179
|
-
}
|
180
|
-
end
|
181
|
-
|
182
|
-
rescue => e
|
183
|
-
raise Constancy::VaultConfigInvalid.new("Are you logged in to Vault?\n\n#{e}")
|
184
|
-
end
|
185
|
-
|
186
|
-
if consul_token.nil? or consul_token == ""
|
187
|
-
raise Constancy::VaultConfigInvalid.new("Could not acquire a Consul token from Vault")
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
else
|
192
|
-
raise Constancy::ConfigFileInvalid.new("Only the following values are valid for token_source: none, env, vault")
|
193
|
-
end
|
194
|
-
|
195
|
-
self.consul = Imperium::Configuration.new(url: consul_url, token: consul_token)
|
196
|
-
|
197
137
|
raw['constancy'] ||= {}
|
198
138
|
if not raw['constancy'].is_a? Hash
|
199
139
|
raise Constancy::ConfigFileInvalid.new("'constancy' must be a hash")
|
200
140
|
end
|
201
141
|
|
202
|
-
if (raw['constancy'].keys -
|
203
|
-
raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in the 'constancy' config block: #{
|
142
|
+
if (raw['constancy'].keys - VALID_CONSTANCY_CONFIG_KEYS) != []
|
143
|
+
raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in the 'constancy' config block: #{VALID_CONSTANCY_CONFIG_KEYS.join(", ")}")
|
204
144
|
end
|
205
145
|
|
206
146
|
# verbose: default false
|
@@ -233,6 +173,7 @@ class Constancy
|
|
233
173
|
|
234
174
|
self.sync_targets = []
|
235
175
|
raw['sync'].each do |target|
|
176
|
+
token_source = self.default_consul_token_source
|
236
177
|
if target.is_a? Hash
|
237
178
|
target['datacenter'] ||= raw['consul']['datacenter']
|
238
179
|
if target['chomp'].nil?
|
@@ -241,19 +182,31 @@ class Constancy
|
|
241
182
|
if target['delete'].nil?
|
242
183
|
target['delete'] = self.delete?
|
243
184
|
end
|
185
|
+
if not target['token_source'].nil?
|
186
|
+
token_source = self.consul_token_sources[target['token_source']]
|
187
|
+
if token_source.nil?
|
188
|
+
raise Constancy::ConfigFileInvalid.new("Consul token source '#{target['token_source']}' is not defined")
|
189
|
+
end
|
190
|
+
target.delete('token_source')
|
191
|
+
end
|
244
192
|
end
|
245
193
|
|
246
|
-
if not self.
|
247
|
-
# unnamed targets cannot be
|
194
|
+
if not self.target_allowlist.nil?
|
195
|
+
# unnamed targets cannot be allowlisted
|
248
196
|
next if target['name'].nil?
|
249
197
|
|
250
|
-
# named targets must be on the
|
251
|
-
next if not self.
|
198
|
+
# named targets must be on the allowlist
|
199
|
+
next if not self.target_allowlist.include?(target['name'])
|
252
200
|
end
|
253
201
|
|
254
|
-
|
202
|
+
# only try to fetch consul tokens if we are actually going to do work
|
203
|
+
consul_token = if self.call_external_apis
|
204
|
+
token_source.consul_token
|
205
|
+
else
|
206
|
+
""
|
207
|
+
end
|
208
|
+
self.sync_targets << Constancy::SyncTarget.new(config: target, consul_url: consul_url, token_source: token_source, base_dir: self.base_dir, call_external_apis: self.call_external_apis)
|
255
209
|
end
|
256
|
-
|
257
210
|
end
|
258
211
|
end
|
259
212
|
end
|
data/lib/constancy/diff.rb
CHANGED
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
class Constancy
|
4
4
|
class SyncTarget
|
5
|
-
VALID_CONFIG_KEYS = %w( name type datacenter prefix path exclude chomp delete )
|
6
|
-
attr_accessor :name, :type, :datacenter, :prefix, :path, :exclude, :consul
|
5
|
+
VALID_CONFIG_KEYS = %w( name type datacenter prefix path exclude chomp delete erb_enabled )
|
6
|
+
attr_accessor :name, :type, :datacenter, :prefix, :path, :exclude, :consul, :erb_enabled, :consul_url, :token_source, :call_external_apis
|
7
7
|
|
8
8
|
REQUIRED_CONFIG_KEYS = %w( prefix )
|
9
9
|
VALID_TYPES = [ :dir, :file ]
|
10
10
|
DEFAULT_TYPE = :dir
|
11
11
|
|
12
|
-
def initialize(config:,
|
12
|
+
def initialize(config:, consul_url:, token_source:, base_dir:, call_external_apis: true)
|
13
13
|
if not config.is_a? Hash
|
14
14
|
raise Constancy::ConfigFileInvalid.new("Sync target entries must be specified as hashes")
|
15
15
|
end
|
@@ -46,7 +46,20 @@ class Constancy
|
|
46
46
|
@do_delete = false
|
47
47
|
end
|
48
48
|
|
49
|
-
self.
|
49
|
+
self.call_external_apis = call_external_apis
|
50
|
+
self.consul_url = consul_url
|
51
|
+
self.token_source = token_source
|
52
|
+
token = if self.call_external_apis
|
53
|
+
self.token_source.consul_token
|
54
|
+
else
|
55
|
+
""
|
56
|
+
end
|
57
|
+
self.consul = Imperium::KV.new(Imperium::Configuration.new(url: self.consul_url, token: token))
|
58
|
+
self.erb_enabled = config['erb_enabled']
|
59
|
+
end
|
60
|
+
|
61
|
+
def erb_enabled?
|
62
|
+
@erb_enabled
|
50
63
|
end
|
51
64
|
|
52
65
|
def chomp?
|
@@ -89,22 +102,41 @@ class Constancy
|
|
89
102
|
when :dir
|
90
103
|
self.local_files.each do |local_file|
|
91
104
|
@local_items[local_file.sub(%r{^#{self.base_path}/?}, '')] =
|
92
|
-
|
93
|
-
File.read(local_file).chomp.force_encoding(Encoding::ASCII_8BIT)
|
94
|
-
else
|
95
|
-
File.read(local_file).force_encoding(Encoding::ASCII_8BIT)
|
96
|
-
end
|
105
|
+
load_local_file(local_file)
|
97
106
|
end
|
98
107
|
|
99
108
|
when :file
|
100
109
|
if File.exist?(self.base_path)
|
101
|
-
@local_items =
|
110
|
+
@local_items = local_items_from_file
|
102
111
|
end
|
103
112
|
end
|
104
113
|
|
105
114
|
@local_items
|
106
115
|
end
|
107
116
|
|
117
|
+
def local_items_from_file
|
118
|
+
if erb_enabled?
|
119
|
+
loaded_file = YAML.load(ERB.new(File.read(self.base_path)).result)
|
120
|
+
else
|
121
|
+
loaded_file = YAML.load_file(self.base_path)
|
122
|
+
end
|
123
|
+
|
124
|
+
flatten_hash(nil, loaded_file)
|
125
|
+
end
|
126
|
+
|
127
|
+
def load_local_file(local_file)
|
128
|
+
file = File.read(local_file)
|
129
|
+
|
130
|
+
if self.chomp?
|
131
|
+
encoded_file = file.chomp.force_encoding(Encoding::ASCII_8BIT)
|
132
|
+
else
|
133
|
+
encoded_file = file.force_encoding(Encoding::ASCII_8BIT)
|
134
|
+
end
|
135
|
+
|
136
|
+
return ERB.new(encoded_file).result if erb_enabled?
|
137
|
+
encoded_file
|
138
|
+
end
|
139
|
+
|
108
140
|
def remote_items
|
109
141
|
return @remote_items if not @remote_items.nil?
|
110
142
|
@remote_items = {}
|
@@ -122,5 +154,26 @@ class Constancy
|
|
122
154
|
def diff(mode)
|
123
155
|
Constancy::Diff.new(target: self, local: self.local_items, remote: self.remote_items, mode: mode)
|
124
156
|
end
|
157
|
+
|
158
|
+
private def flatten_hash(prefix, hash)
|
159
|
+
new_hash = {}
|
160
|
+
|
161
|
+
hash.each do |k, v|
|
162
|
+
if k == '_' && !prefix.nil?
|
163
|
+
new_key = prefix
|
164
|
+
else
|
165
|
+
new_key = [prefix, k].compact.join('/')
|
166
|
+
end
|
167
|
+
|
168
|
+
case v
|
169
|
+
when Hash
|
170
|
+
new_hash.merge!(flatten_hash(new_key, v))
|
171
|
+
else
|
172
|
+
new_hash[new_key] = v.to_s
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
new_hash
|
177
|
+
end
|
125
178
|
end
|
126
179
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Constancy
|
4
|
+
# use env vars if defined, but otherwise just return an empty string
|
5
|
+
class PassiveTokenSource
|
6
|
+
def name
|
7
|
+
"none"
|
8
|
+
end
|
9
|
+
|
10
|
+
def consul_token
|
11
|
+
ENV['CONSUL_HTTP_TOKEN'] || ENV['CONSUL_TOKEN'] || ""
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# use env vars and raise an error if none is found
|
16
|
+
class EnvTokenSource
|
17
|
+
def name
|
18
|
+
"env"
|
19
|
+
end
|
20
|
+
|
21
|
+
def consul_token
|
22
|
+
consul_token = ENV['CONSUL_HTTP_TOKEN'] || ENV['CONSUL_TOKEN']
|
23
|
+
if consul_token.nil? or consul_token == ""
|
24
|
+
raise Constancy::ConsulTokenRequired.new("Consul token_source was set to 'env' but neither CONSUL_TOKEN nor CONSUL_HTTP_TOKEN is set")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class VaultTokenSource
|
30
|
+
attr_accessor :name, :vault_addr, :vault_token, :consul_token_path, :consul_token_field
|
31
|
+
|
32
|
+
def initialize(name:, config:)
|
33
|
+
self.name = name
|
34
|
+
|
35
|
+
config ||= {}
|
36
|
+
if not config.is_a? Hash
|
37
|
+
raise Constancy::ConfigFileInvalid.new("'#{name}' must be a hash")
|
38
|
+
end
|
39
|
+
|
40
|
+
if (config.keys - Constancy::Config::VALID_VAULT_CONFIG_KEYS) != []
|
41
|
+
raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in a vault config: #{Constancy::Config::VALID_VAULT_CONFIG_KEYS.join(", ")}")
|
42
|
+
end
|
43
|
+
|
44
|
+
self.consul_token_path = config['consul_token_path']
|
45
|
+
if self.consul_token_path.nil? or self.consul_token_path == ""
|
46
|
+
raise Constancy::ConfigFileInvalid.new("consul_token_path must be specified to use '#{name}' as a token source")
|
47
|
+
end
|
48
|
+
|
49
|
+
# prioritize the config file over environment variables for vault address
|
50
|
+
self.vault_addr = config['url'] || ENV['VAULT_ADDR']
|
51
|
+
if self.vault_addr.nil? or self.vault_addr == ""
|
52
|
+
raise Constancy::VaultConfigInvalid.new("Vault address must be set in #{name}.vault_addr or VAULT_ADDR")
|
53
|
+
end
|
54
|
+
|
55
|
+
self.vault_token = ENV['VAULT_TOKEN']
|
56
|
+
if self.vault_token.nil? or self.vault_token == ""
|
57
|
+
vault_token_file = File.expand_path("~/.vault-token")
|
58
|
+
if File.exist?(vault_token_file)
|
59
|
+
self.vault_token = File.read(vault_token_file)
|
60
|
+
else
|
61
|
+
raise Constancy::VaultConfigInvalid.new("Vault token must be set in ~/.vault-token or VAULT_TOKEN")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
self.consul_token_field = config['consul_token_field'] || Constancy::Config::DEFAULT_VAULT_CONSUL_TOKEN_FIELD
|
66
|
+
end
|
67
|
+
|
68
|
+
def consul_token
|
69
|
+
if @consul_token.nil?
|
70
|
+
begin
|
71
|
+
response = Vault::Client.new(address: self.vault_addr, token: self.vault_token).logical.read(self.consul_token_path)
|
72
|
+
@consul_token = response.data[self.consul_token_field.to_sym]
|
73
|
+
if response.lease_id
|
74
|
+
at_exit {
|
75
|
+
begin
|
76
|
+
Vault::Client.new(address: self.vault_addr, token: self.vault_token).sys.revoke(response.lease_id)
|
77
|
+
rescue => e
|
78
|
+
# this is fine
|
79
|
+
end
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
rescue => e
|
84
|
+
raise Constancy::VaultConfigInvalid.new("Are you logged in to Vault?\n\n#{e}")
|
85
|
+
end
|
86
|
+
|
87
|
+
if @consul_token.nil? or @consul_token == ""
|
88
|
+
raise Constancy::VaultConfigInvalid.new("Could not acquire a Consul token from Vault")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
@consul_token
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/constancy/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: constancy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Adams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-09-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: imperium
|
@@ -83,9 +83,11 @@ files:
|
|
83
83
|
- lib/constancy/cli/config_command.rb
|
84
84
|
- lib/constancy/cli/pull_command.rb
|
85
85
|
- lib/constancy/cli/push_command.rb
|
86
|
+
- lib/constancy/cli/targets_command.rb
|
86
87
|
- lib/constancy/config.rb
|
87
88
|
- lib/constancy/diff.rb
|
88
89
|
- lib/constancy/sync_target.rb
|
90
|
+
- lib/constancy/token_source.rb
|
89
91
|
- lib/constancy/version.rb
|
90
92
|
homepage: https://github.com/daveadams/constancy
|
91
93
|
licenses:
|
@@ -106,8 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
108
|
- !ruby/object:Gem::Version
|
107
109
|
version: '0'
|
108
110
|
requirements: []
|
109
|
-
|
110
|
-
rubygems_version: 2.7.6
|
111
|
+
rubygems_version: 3.0.3
|
111
112
|
signing_key:
|
112
113
|
specification_version: 4
|
113
114
|
summary: Simple filesystem-to-Consul KV synchronization
|