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 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