constancy 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|