secret_config 0.7.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,21 +1,21 @@
1
- require 'rake/testtask'
2
- require_relative 'lib/secret_config/version'
1
+ require "rake/testtask"
2
+ require_relative "lib/secret_config/version"
3
3
 
4
4
  task :gem do
5
- system 'gem build secret_config.gemspec'
5
+ system "gem build secret_config.gemspec"
6
6
  end
7
7
 
8
- task :publish => :gem do
8
+ task publish: :gem do
9
9
  system "git tag -a v#{SecretConfig::VERSION} -m 'Tagging #{SecretConfig::VERSION}'"
10
- system 'git push --tags'
10
+ system "git push --tags"
11
11
  system "gem push secret_config-#{SecretConfig::VERSION}.gem"
12
12
  system "rm secret_config-#{SecretConfig::VERSION}.gem"
13
13
  end
14
14
 
15
15
  Rake::TestTask.new(:test) do |t|
16
- t.pattern = 'test/**/*_test.rb'
16
+ t.pattern = "test/**/*_test.rb"
17
17
  t.verbose = true
18
18
  t.warning = true
19
19
  end
20
20
 
21
- task :default => :test
21
+ task default: :test
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'secret_config'
3
+ require "secret_config"
4
4
 
5
5
  SecretConfig::CLI.run!(ARGV)
@@ -18,6 +18,8 @@ module SecretConfig
18
18
  end
19
19
 
20
20
  autoload :CLI, "secret_config/cli"
21
+ autoload :Config, "secret_config/config"
22
+ autoload :Parser, "secret_config/parser"
21
23
  autoload :SettingInterpolator, "secret_config/setting_interpolator"
22
24
  autoload :StringInterpolator, "secret_config/string_interpolator"
23
25
  autoload :Utils, "secret_config/utils"
@@ -30,7 +32,6 @@ module SecretConfig
30
32
  def_delegator :registry, :[]
31
33
  def_delegator :registry, :[]=
32
34
  def_delegator :registry, :key?
33
- def_delegator :registry, :fetch
34
35
  def_delegator :registry, :set
35
36
  def_delegator :registry, :delete
36
37
  def_delegator :registry, :refresh!
@@ -42,6 +43,34 @@ module SecretConfig
42
43
  @registry = SecretConfig::Registry.new(path: path, provider: provider, provider_args: args)
43
44
  end
44
45
 
46
+ # Fetch configuration in a block by supplying the root path once.
47
+ #
48
+ # Example:
49
+ # SecretConfig.configure("suppliers/kafka_service") do |config|
50
+ # Kafka::Client.new(
51
+ # seed_brokers: config.fetch("brokers", separator: ","),
52
+ # delivery_interval: config.fetch("delivery_interval", type: :integer, default: 0),
53
+ # delivery_threshold: config.fetch("delivery_threshold", type: :integer, default: 0),
54
+ # max_queue_size: config.fetch("max_queue_size", type: :integer, default: 10_000),
55
+ # max_retries: config.fetch("max_retries", type: :integer, default: -1),
56
+ # retry_backoffs: config.fetch("retry_backoff", type: :integer, default: 0),
57
+ # )
58
+ # end
59
+ #
60
+ # If `SecretConfig.configure` was not used it would have looked like:
61
+ # Kafka::Client.new(
62
+ # seed_brokers: SecretConfig.fetch("suppliers/kafka_service/brokers", separator: ","),
63
+ # delivery_interval: SecretConfig.fetch("suppliers/kafka_service/delivery_interval", type: :integer, default: 0),
64
+ # delivery_threshold: SecretConfig.fetch("suppliers/kafka_service/delivery_threshold", type: :integer, default: 0),
65
+ # max_queue_size: SecretConfig.fetch("suppliers/kafka_service/max_queue_size", type: :integer, default: 10_000),
66
+ # max_retries: SecretConfig.fetch("suppliers/kafka_service/max_retries", type: :integer, default: -1),
67
+ # retry_backoffs: SecretConfig.fetch("suppliers/kafka_service/retry_backoff", type: :integer, default: 0),
68
+ # )
69
+ def self.configure(path)
70
+ config = Config.new(path, registry)
71
+ yield(config)
72
+ end
73
+
45
74
  # Returns the global registry.
46
75
  # Unless `.use` was called above, it will default to a file provider.
47
76
  def self.registry
@@ -68,5 +97,5 @@ module SecretConfig
68
97
  end
69
98
 
70
99
  @check_env_var = true
71
- @filters = [/password/i, /key\Z/i, /passphrase/i]
100
+ @filters = [/password/i, /key\Z/i, /passphrase/i, /secret/i, /pwd\Z/i]
72
101
  end
@@ -8,11 +8,29 @@ require "irb"
8
8
 
9
9
  module SecretConfig
10
10
  class CLI
11
- attr_reader :path, :region, :provider,
12
- :export, :no_filter,
11
+ module Colors
12
+ CLEAR = "\e[0m".freeze
13
+ BOLD = "\e[1m".freeze
14
+ BLACK = "\e[30m".freeze
15
+ RED = "\e[31m".freeze
16
+ GREEN = "\e[32m".freeze
17
+ YELLOW = "\e[33m".freeze
18
+ BLUE = "\e[34m".freeze
19
+ MAGENTA = "\e[35m".freeze
20
+ CYAN = "\e[36m".freeze
21
+ WHITE = "\e[37m".freeze
22
+
23
+ TITLE = "\e[1m".freeze
24
+ KEY = "\e[36m".freeze
25
+ REMOVE = "\e[31m".freeze
26
+ ADD = "\e[32m".freeze
27
+ end
28
+
29
+ attr_reader :path, :provider, :file_name,
30
+ :export, :no_filter, :interpolate,
13
31
  :import, :key_id, :key_alias, :random_size, :prune, :force,
14
32
  :diff_path, :import_path,
15
- :fetch_key, :delete_key, :set_key, :set_value, :delete_path,
33
+ :fetch_key, :delete_key, :set_key, :set_value, :delete_tree,
16
34
  :copy_path, :diff,
17
35
  :console,
18
36
  :show_version
@@ -29,7 +47,6 @@ module SecretConfig
29
47
  @path = nil
30
48
  @key_id = nil
31
49
  @key_alias = nil
32
- @region = ENV["AWS_REGION"]
33
50
  @provider = :ssm
34
51
  @random_size = 32
35
52
  @no_filter = false
@@ -43,10 +60,11 @@ module SecretConfig
43
60
  @set_value = nil
44
61
  @fetch_key = nil
45
62
  @delete_key = nil
46
- @delete_path = nil
63
+ @delete_tree = nil
47
64
  @diff_path = nil
48
65
  @import_path = nil
49
66
  @force = false
67
+ @interpolate = false
50
68
 
51
69
  if argv.empty?
52
70
  puts parser
@@ -58,27 +76,32 @@ module SecretConfig
58
76
  def run!
59
77
  if show_version
60
78
  puts "Secret Config v#{VERSION}"
61
- puts "Region: #{region}"
62
79
  elsif console
63
80
  run_console
64
81
  elsif export
65
- run_export(export, path, filtered: !no_filter)
82
+ raise(ArgumentError, "--path option is not valid for --export") if path
83
+
84
+ run_export(export, file_name || STDOUT, filtered: !no_filter)
66
85
  elsif import
67
- run_import(import, path, prune, force)
68
- elsif import_path
69
- run_import_path(import_path, path, prune, force)
86
+ if path
87
+ run_import_path(import, path, prune, force)
88
+ else
89
+ run_import(import, file_name || STDIN, prune, force)
90
+ end
70
91
  elsif diff
71
- run_diff(diff, path)
72
- elsif diff_path
73
- run_diff_path(diff_path, path)
92
+ if path
93
+ run_diff_path(diff, path)
94
+ else
95
+ run_diff(diff, file_name || STDIN)
96
+ end
74
97
  elsif set_key
75
98
  run_set(set_key, set_value)
76
99
  elsif fetch_key
77
100
  run_fetch(fetch_key)
78
101
  elsif delete_key
79
102
  run_delete(delete_key)
80
- elsif delete_path
81
- run_delete_path(delete_path)
103
+ elsif delete_tree
104
+ run_delete_tree(delete_tree)
82
105
  else
83
106
  puts parser
84
107
  end
@@ -91,27 +114,27 @@ module SecretConfig
91
114
 
92
115
  For more information, see: https://rocketjob.github.io/secret_config/
93
116
 
94
- secret_config [options]
117
+ secret-config [options]
95
118
  BANNER
96
119
 
97
- opts.on "-e", "--export [FILE_NAME]", "Export configuration to a file or stdout if no file_name supplied. --path SOURCE_PATH is required." do |file_name|
98
- @export = file_name || STDOUT
120
+ opts.on "-e", "--export SOURCE_PATH", "Export configuration. Use --file to specify the file name, otherwise stdout is used." do |path|
121
+ @export = path
99
122
  end
100
123
 
101
- opts.on "-i", "--import [FILE_NAME]", "Import configuration from a file or stdin if no file_name supplied. --path TARGET_PATH is required." do |file_name|
102
- @import = file_name || STDIN
124
+ opts.on "-i", "--import TARGET_PATH", "Import configuration. Use --file to specify the file name, --path for the SOURCE_PATH, otherwise stdin is used." do |path|
125
+ @import = path
103
126
  end
104
127
 
105
- opts.on "--import-path SOURCE_PATH", "Import configuration from the configuration on another path. --path TARGET_PATH is required." do |path|
106
- @import_path = path
128
+ opts.on "-f", "--file FILE_NAME", "Import/Export/Diff to/from this file." do |file_name|
129
+ @file_name = file_name
107
130
  end
108
131
 
109
- opts.on "--diff [FILE_NAME]", "Compare configuration from a file or stdin if no file_name supplied. --path TARGET_PATH is required." do |file_name|
110
- @diff = file_name
132
+ opts.on "-p", "--path PATH", "Import/Export/Diff to/from this path." do |path|
133
+ @path = path
111
134
  end
112
135
 
113
- opts.on "--diff-path SOURCE_PATH", "Diff configuration with the configuration on another path. --path TARGET_PATH is required." do |path|
114
- @diff_path = path
136
+ opts.on "--diff TARGET_PATH", "Compare configuration to this path. Use --file to specify the source file name, --path for the SOURCE_PATH, otherwise stdin is used." do |file_name|
137
+ @diff = file_name
115
138
  end
116
139
 
117
140
  opts.on "-s", "--set KEY=VALUE", "Set one key to value. Example: --set mysql/database=localhost" do |param|
@@ -121,59 +144,55 @@ module SecretConfig
121
144
  end
122
145
  end
123
146
 
124
- opts.on "-f", "--fetch KEY", "Fetch the value for one setting. Example: --get mysql/database. " do |key|
147
+ opts.on "-f", "--fetch KEY", "Fetch the value for one setting. Example: --fetch mysql/database." do |key|
125
148
  @fetch_key = key
126
149
  end
127
150
 
128
- opts.on "-d", "--delete KEY", "Delete one specific key. See --delete-path to delete all keys under a specific path " do |key|
151
+ opts.on "-d", "--delete KEY", "Delete one specific key." do |key|
129
152
  @delete_key = key
130
153
  end
131
154
 
132
- opts.on "-r", "--delete-path PATH", "Recursively delete all keys under the specified path.. " do |path|
133
- @delete_path = path
155
+ opts.on "-r", "--delete-tree PATH", "Recursively delete all keys under the specified path." do |path|
156
+ @delete_tree = path
134
157
  end
135
158
 
136
159
  opts.on "-c", "--console", "Start interactive console." do
137
160
  @console = true
138
161
  end
139
162
 
140
- opts.on "-p", "--path PATH", "Path in central configuration to use." do |path|
141
- @path = path
142
- end
143
-
144
163
  opts.on "--provider PROVIDER", "Provider to use. [ssm | file]. Default: ssm" do |provider|
145
164
  @provider = provider.to_sym
146
165
  end
147
166
 
148
- opts.on "--no-filter", "Do not filter passwords and keys." do
167
+ opts.on "--no-filter", "For --export only. Do not filter passwords and keys." do
149
168
  @no_filter = true
150
169
  end
151
170
 
152
- opts.on "--prune", "During import delete all existing keys for which there is no key in the import file. Only applies to --import and --import-path." do
171
+ opts.on "--interpolate", "For --export only. Evaluate string interpolation and __import__." do
172
+ @interpolate = true
173
+ end
174
+
175
+ opts.on "--prune", "For --import only. During import delete all existing keys for which there is no key in the import file. Only works with --import." do
153
176
  @prune = true
154
177
  end
155
178
 
156
- opts.on "--force", "During import overwrite all values, not just the changed ones. Useful for changing the KMS key. Only applies to --import and --import-path." do
179
+ opts.on "--force", "For --import only. Overwrite all values, not just the changed ones. Useful for changing the KMS key." do
157
180
  @force = true
158
181
  end
159
182
 
160
- opts.on "--key_id KEY_ID", "Encrypt config settings with this AWS KMS key id. Default: AWS Default key." do |key_id|
183
+ opts.on "--key_id KEY_ID", "For --import only. Encrypt config settings with this AWS KMS key id. Default: AWS Default key." do |key_id|
161
184
  @key_id = key_id
162
185
  end
163
186
 
164
- opts.on "--key_alias KEY_ALIAS", "Encrypt config settings with this AWS KMS alias." do |key_alias|
187
+ opts.on "--key_alias KEY_ALIAS", "For --import only. Encrypt config settings with this AWS KMS alias." do |key_alias|
165
188
  @key_alias = key_alias
166
189
  end
167
190
 
168
- opts.on "--region REGION", "AWS Region to use. Default: AWS_REGION env var." do |region|
169
- @region = region
170
- end
171
-
172
- opts.on "--random_size INTEGER", Integer, "Size to use when generating random values. Whenever #{RANDOM} is encountered during an import. Default: 32" do |random_size|
191
+ opts.on "--random_size INTEGER", Integer, "For --import only. Size to use when generating random values when $(random) is encountered in the source. Default: 32" do |random_size|
173
192
  @random_size = random_size
174
193
  end
175
194
 
176
- opts.on "-v", "--version", "Display Symmetric Encryption version." do
195
+ opts.on "-v", "--version", "Display Secret Config version." do
177
196
  @show_version = true
178
197
  end
179
198
 
@@ -191,64 +210,68 @@ module SecretConfig
191
210
  begin
192
211
  case provider
193
212
  when :ssm
194
- Providers::Ssm.new(key_id: key_id, key_alias: key_alias)
213
+ if key_alias
214
+ Providers::Ssm.new(key_alias: key_alias)
215
+ elsif key_id
216
+ Providers::Ssm.new(key_id: key_id)
217
+ else
218
+ Providers::Ssm.new
219
+ end
195
220
  else
196
221
  raise ArgumentError, "Invalid provider: #{provider}"
197
222
  end
198
223
  end
199
224
  end
200
225
 
201
- def run_export(file_name, path, filtered: true)
202
- raise(ArgumentError, "Missing required option --path") unless path
226
+ def run_export(source_path, file_name, filtered: true)
227
+ puts("Exporting #{provider}:#{source_path} to #{file_name}") if file_name.is_a?(String)
203
228
 
204
- config = fetch_config(path, filtered: filtered)
229
+ config = fetch_config(source_path, filtered: filtered)
205
230
  write_config_file(file_name, config)
206
-
207
- puts("Exported #{path} from #{provider} to #{file_name}") if file_name.is_a?(String)
208
231
  end
209
232
 
210
- def run_import(file_name, path, prune, force)
211
- raise(ArgumentError, "Missing required option --path") unless path
212
-
233
+ def run_import(target_path, file_name, prune, force)
234
+ puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
235
+ puts "+++ #{file_name}#{Colors::CLEAR}"
213
236
  config = read_config_file(file_name)
214
- import_config(config, path, prune, force)
215
-
216
- puts("Imported #{file_name} to #{path} on provider: #{provider}") if file_name.is_a?(String)
237
+ import_config(config, target_path, prune, force)
217
238
  end
218
239
 
219
- def run_import_path(source_path, path, prune, force)
220
- raise(ArgumentError, "Missing required option --path") unless path
240
+ def run_import_path(target_path, source_path, prune, force)
241
+ puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
242
+ puts "+++ #{provider}:#{source_path}#{Colors::CLEAR}"
221
243
 
222
244
  config = fetch_config(source_path, filtered: false)
223
- import_config(config, path, prune, force)
245
+ import_config(config, target_path, prune, force)
224
246
 
225
- puts("Imported #{source_path} to #{path} on provider: #{provider}")
247
+ puts("Imported #{target_path} from #{source_path} on provider: #{provider}")
226
248
  end
227
249
 
228
- def run_diff(file_name, path)
229
- raise(ArgumentError, "Missing required option --path") unless path
250
+ def run_diff(target_path, file_name)
251
+ source_config = read_config_file(file_name)
252
+ source = Utils.flatten(source_config, target_path)
230
253
 
231
- file_config = read_config_file(file_name)
232
- file = Utils.flatten(file_config, path)
254
+ target_config = fetch_config(target_path, filtered: false)
255
+ target = Utils.flatten(target_config, target_path)
233
256
 
234
- registry_config = fetch_config(path, filtered: false)
235
- registry = Utils.flatten(registry_config, path)
236
-
237
- puts("Comparing #{file_name} to #{path} on provider: #{provider}") if file_name.is_a?(String)
238
- diff_config(file, registry)
257
+ if file_name.is_a?(String)
258
+ puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
259
+ puts "+++ #{file_name}#{Colors::CLEAR}"
260
+ end
261
+ diff_config(target, source)
239
262
  end
240
263
 
241
- def run_diff_path(source_path, path)
242
- raise(ArgumentError, "Missing required option --path") unless path
243
-
264
+ def run_diff_path(target_path, source_path)
244
265
  source_config = fetch_config(source_path, filtered: false)
245
266
  source = Utils.flatten(source_config)
246
267
 
247
- target_config = fetch_config(path, filtered: false)
268
+ target_config = fetch_config(target_path, filtered: false)
248
269
  target = Utils.flatten(target_config)
249
270
 
250
- puts("Comparing #{source_path} to #{path} on provider: #{provider}")
251
- diff_config(source, target)
271
+ puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
272
+ puts "+++ #{provider}:#{source_path}#{Colors::CLEAR}"
273
+
274
+ diff_config(target, source)
252
275
  end
253
276
 
254
277
  def run_console
@@ -256,14 +279,18 @@ module SecretConfig
256
279
  end
257
280
 
258
281
  def run_delete(key)
282
+ puts "#{Colors::TITLE}--- #{provider}:#{path}"
283
+ puts "#{Colors::REMOVE}- #{key}#{Colors::CLEAR}"
259
284
  provider_instance.delete(key)
260
285
  end
261
286
 
262
- def run_delete_path(path)
287
+ def run_delete_tree(path)
263
288
  source_config = fetch_config(path)
264
- source = Utils.flatten(source_config, path)
289
+ puts "#{Colors::TITLE}--- #{provider}:#{path}#{Colors::CLEAR}"
290
+
291
+ source = Utils.flatten(source_config, path)
265
292
  source.each_key do |key|
266
- puts("Deleting #{key}")
293
+ puts "#{Colors::REMOVE}- #{key}#{Colors::CLEAR}"
267
294
  provider_instance.delete(key)
268
295
  end
269
296
  end
@@ -277,8 +304,8 @@ module SecretConfig
277
304
  provider_instance.set(key, value)
278
305
  end
279
306
 
280
- def current_values
281
- @current_values ||= Utils.flatten(fetch_config(path, filtered: false), path)
307
+ def current_values(path)
308
+ Utils.flatten(fetch_config(path, filtered: false), path)
282
309
  end
283
310
 
284
311
  def read_config_file(file_name)
@@ -306,38 +333,53 @@ module SecretConfig
306
333
  # Ignore filtered values
307
334
  next
308
335
  end
309
- puts "Setting: #{key}"
336
+
337
+ if current_values.key?(key)
338
+ puts "#{Colors::KEY}* #{key}#{Colors::CLEAR}"
339
+ else
340
+ puts "#{Colors::ADD}+ #{key}#{Colors::CLEAR}"
341
+ end
342
+
310
343
  provider_instance.set(key, value)
311
344
  end
312
345
  end
313
346
 
314
347
  def fetch_config(path, filtered: true)
315
- registry = Registry.new(path: path, provider: provider_instance)
348
+ registry = Registry.new(path: path, provider: provider_instance, interpolate: interpolate)
316
349
  config = filtered ? registry.configuration : registry.configuration(filters: nil)
317
350
  sort_hash_by_key!(config)
318
351
  end
319
352
 
320
353
  # Diffs two configs and displays the results
321
- def diff_config(source, target)
354
+ def diff_config(target, source)
322
355
  (source.keys + target.keys).sort.uniq.each do |key|
323
356
  if target.key?(key)
324
357
  if source.key?(key)
325
358
  value = source[key].to_s
326
359
  # Ignore filtered values
327
- puts "* #{key}: #{target[key]} => #{source[key]}" if (value != target[key].to_s) && (value != FILTERED)
360
+ if (value != target[key].to_s) && (value != FILTERED)
361
+ puts "#{Colors::KEY}#{key}:"
362
+ puts "#{Colors::REMOVE}#{prefix_lines("- ", target[key])}"
363
+ puts "#{Colors::ADD}#{prefix_lines("+ ", source[key])}#{Colors::CLEAR}\n\n"
364
+ end
328
365
  else
329
- puts "- #{key}"
366
+ puts "#{Colors::KEY}#{key}:"
367
+ puts "#{Colors::REMOVE}#{prefix_lines("- ", target[key])}\n\n"
330
368
  end
331
369
  elsif source.key?(key)
332
- puts "+ #{key}: #{source[key]}"
370
+ puts "#{Colors::KEY}#{key}:"
371
+ puts "#{Colors::ADD}#{prefix_lines("+ ", source[key])}#{Colors::CLEAR}\n\n"
333
372
  end
334
373
  end
335
374
  end
336
375
 
337
- def import_config(config, path, prune, force)
338
- raise(ArgumentError, "Missing required option --path") unless path
376
+ def prefix_lines(prefix, value)
377
+ value.to_s.lines.collect { |line| "#{prefix}#{line}" }.join("")
378
+ end
339
379
 
340
- delete_keys = prune ? current_values.keys - Utils.flatten(config, path).keys : []
380
+ def import_config(config, path, prune, force)
381
+ current = current_values(path)
382
+ delete_keys = prune ? current.keys - Utils.flatten(config, path).keys : []
341
383
 
342
384
  unless delete_keys.empty?
343
385
  puts "Going to delete the following keys:"
@@ -345,10 +387,10 @@ module SecretConfig
345
387
  sleep(5)
346
388
  end
347
389
 
348
- set_config(config, path, force ? {} : current_values)
390
+ set_config(config, path, force ? {} : current)
349
391
 
350
392
  delete_keys.each do |key|
351
- puts "Deleting: #{key}"
393
+ puts "#{Colors::REMOVE}- #{key}#{Colors::CLEAR}"
352
394
  provider_instance.delete(key)
353
395
  end
354
396
  end