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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3119c8dc521f42f974a1f708273026eb49f59c19e25867408aae150dc83261ac
4
- data.tar.gz: f6c729bfd5bbf17e29ecd21570076dcf212ae5af999b5f5d89f629216d1c33b0
3
+ metadata.gz: 4f4ece9e3ea596821b57e8e4e2f4e11d70833df49f3480082a4438a1a018a69b
4
+ data.tar.gz: 5cead608d291729245435375774093004032877b5e4e447aa7ebf0ff8a7b0e2a
5
5
  SHA512:
6
- metadata.gz: 1928aead12f4a255cc9e8595d3c024b115a30b950fdcefe5df3ab50d35b122ba8340ed7a95f4db15a36b04f8b8f124d10e00467000088d78a1e6e88e72d0c3e0
7
- data.tar.gz: f1d991706a3e0883bf2b22f9e53588a1c6472a55376fb424c5ead455a5c704d99bc6966d378deda7d07c648c606941fb5499fa23affd837104e4e1e513362265
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-whitelist.json
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-whitelist.json
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-whitelist.json
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-whitelist.json
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-whitelist.json OK
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
- I hope you like it.
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
- config/yourapp/prod/dbname
239
- config/yourapp/prod/message
240
- config/yourapp/prod/app/config.json
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
- Likewise, a `constancy pull` operation will work in reverse, and pull values
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
- Note that JSON is also supported for this file for `push` operations, given that
247
- YAML parsers will correctly parse JSON. However, `constancy pull` will only
248
- write out YAML in the current version.
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
- Also important to note that any comments in the YAML file will be lost on a
251
- `pull` operation that updates a file sync target.
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 `VAULT_TOKEN` is not
283
- set, Constancy will attempt to read a token from `~/.vault-token`. If the
284
- `url` field is set, it will take priority over the `VAULT_ADDR` environment
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
@@ -3,6 +3,6 @@
3
3
  # This software is public domain. No rights are reserved. See LICENSE for more information.
4
4
  #
5
5
 
6
- require 'constancy/cli'
6
+ require_relative '../lib/constancy/cli'
7
7
 
8
8
  Constancy::CLI.run
@@ -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
- require 'constancy/version'
10
- require 'constancy/config'
11
- require 'constancy/diff'
12
- require 'constancy/sync_target'
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
@@ -1,11 +1,12 @@
1
1
  # This software is public domain. No rights are reserved. See LICENSE for more information.
2
2
 
3
- require 'constancy'
3
+ require_relative '../constancy'
4
4
  require 'diffy'
5
- require 'constancy/cli/check_command'
6
- require 'constancy/cli/push_command'
7
- require 'constancy/cli/pull_command'
8
- require 'constancy/cli/config_command'
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.consul.url}"
11
+ puts " Consul URL: #{Constancy.config.consul_url}"
12
12
  puts " Verbose: #{Constancy.config.verbose?.to_s.bold}"
13
- if Constancy.config.consul_token_source == "env"
14
- puts
15
- puts "Token Source: CONSUL_TOKEN or CONSUL_HTTP_TOKEN environment variable"
16
-
17
- elsif Constancy.config.consul_token_source == "vault"
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 "Token Source: Vault"
20
- puts " Vault URL: #{Constancy.config.vault_config.url}"
21
- puts " Token Path: #{Constancy.config.vault_config.consul_token_path}"
22
- puts " Token Field: #{Constancy.config.vault_config.consul_token_field}"
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 " Datacenter: #{target.datacenter}"
39
- puts " Local type: #{target.type == :dir ? 'Directory' : 'Single file'}"
40
- puts " #{target.type == :dir ? " Dir" : "File"} path: #{target.path}"
41
- puts " Prefix: #{target.prefix}"
42
- puts " Autochomp? #{target.chomp?}"
43
- puts " Delete? #{target.delete?}"
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 " Exclusions:"
53
+ puts " Exclusions:"
46
54
  target.exclude.each do |exclusion|
47
- puts " - #{exclusion}"
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
@@ -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, :consul, :consul_token_source, :sync_targets, :target_whitelist, :call_external_apis, :vault_config
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.target_whitelist = targets
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
- private
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
- if (raw.keys - Constancy::Config::VALID_CONFIG_KEYS) != []
95
- raise Constancy::ConfigFileInvalid.new("Only the following keys are valid at the top level of the config: #{Constancy::Config::VALID_CONFIG_KEYS.join(", ")}")
96
- end
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 - Constancy::Config::VALID_CONSUL_CONFIG_KEYS) != []
104
- raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in the consul config: #{Constancy::Config::VALID_CONSUL_CONFIG_KEYS.join(", ")}")
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'] || Constancy::Config::DEFAULT_CONSUL_URL
108
-
109
- # start with a token from the environment, regardless of the token_source setting
110
- consul_token = ENV['CONSUL_HTTP_TOKEN'] || ENV['CONSUL_TOKEN']
111
-
112
- self.consul_token_source = raw['consul']['token_source'] || Constancy::Config::DEFAULT_CONSUL_TOKEN_SOURCE
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 - Constancy::Config::VALID_CONSTANCY_CONFIG_KEYS) != []
203
- raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in the 'constancy' config block: #{Constancy::Config::VALID_CONSTANCY_CONFIG_KEYS.join(", ")}")
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.target_whitelist.nil?
247
- # unnamed targets cannot be whitelisted
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 whitelist
251
- next if not self.target_whitelist.include?(target['name'])
198
+ # named targets must be on the allowlist
199
+ next if not self.target_allowlist.include?(target['name'])
252
200
  end
253
201
 
254
- self.sync_targets << Constancy::SyncTarget.new(config: target, imperium_config: self.consul, base_dir: self.base_dir)
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
@@ -36,7 +36,7 @@ class Constancy
36
36
  end
37
37
  end
38
38
 
39
- consul_key = [@target.prefix, key].compact.join("/")
39
+ consul_key = [@target.prefix, key].compact.join("/").squeeze("/")
40
40
 
41
41
  if @target.exclude.include?(key) or @target.exclude.include?(consul_key)
42
42
  op = :ignore
@@ -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:, imperium_config:, base_dir:)
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.consul = Imperium::KV.new(imperium_config)
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
- if self.chomp?
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 = YAML.load_file(self.base_path)
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
@@ -1,5 +1,5 @@
1
1
  # This software is public domain. No rights are reserved. See LICENSE for more information.
2
2
 
3
3
  class Constancy
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.3.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: 2019-01-07 00:00:00.000000000 Z
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
- rubyforge_project:
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