secret_config 0.6.4 → 0.9.1

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,10 @@ 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"
23
+ autoload :SettingInterpolator, "secret_config/setting_interpolator"
24
+ autoload :StringInterpolator, "secret_config/string_interpolator"
21
25
  autoload :Utils, "secret_config/utils"
22
26
 
23
27
  class << self
@@ -28,7 +32,6 @@ module SecretConfig
28
32
  def_delegator :registry, :[]
29
33
  def_delegator :registry, :[]=
30
34
  def_delegator :registry, :key?
31
- def_delegator :registry, :fetch
32
35
  def_delegator :registry, :set
33
36
  def_delegator :registry, :delete
34
37
  def_delegator :registry, :refresh!
@@ -40,6 +43,34 @@ module SecretConfig
40
43
  @registry = SecretConfig::Registry.new(path: path, provider: provider, provider_args: args)
41
44
  end
42
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
+
43
74
  # Returns the global registry.
44
75
  # Unless `.use` was called above, it will default to a file provider.
45
76
  def self.registry
@@ -65,8 +96,6 @@ module SecretConfig
65
96
  @check_env_var = check_env_var
66
97
  end
67
98
 
68
- private
69
-
70
99
  @check_env_var = true
71
100
  @filters = [/password/i, /key\Z/i, /passphrase/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,
13
- :import, :key_id, :key_alias, :random_size, :prune, :overwrite,
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,
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,9 +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
66
+ @force = false
67
+ @interpolate = false
49
68
 
50
69
  if argv.empty?
51
70
  puts parser
@@ -57,27 +76,32 @@ module SecretConfig
57
76
  def run!
58
77
  if show_version
59
78
  puts "Secret Config v#{VERSION}"
60
- puts "Region: #{region}"
61
79
  elsif console
62
80
  run_console
63
81
  elsif export
64
- 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)
65
85
  elsif import
66
- run_import(import, path, prune)
67
- elsif import_path
68
- run_import_path(import_path, path, prune)
86
+ if path
87
+ run_import_path(import, path, prune, force)
88
+ else
89
+ run_import(import, file_name || STDIN, prune, force)
90
+ end
69
91
  elsif diff
70
- run_diff(diff, path)
71
- elsif diff_path
72
- run_diff_path(diff, path)
92
+ if path
93
+ run_diff_path(diff, path)
94
+ else
95
+ run_diff(diff, file_name || STDIN)
96
+ end
73
97
  elsif set_key
74
98
  run_set(set_key, set_value)
75
99
  elsif fetch_key
76
100
  run_fetch(fetch_key)
77
101
  elsif delete_key
78
102
  run_delete(delete_key)
79
- elsif delete_path
80
- run_delete_path(delete_path)
103
+ elsif delete_tree
104
+ run_delete_tree(delete_tree)
81
105
  else
82
106
  puts parser
83
107
  end
@@ -90,27 +114,27 @@ module SecretConfig
90
114
 
91
115
  For more information, see: https://rocketjob.github.io/secret_config/
92
116
 
93
- secret_config [options]
117
+ secret-config [options]
94
118
  BANNER
95
119
 
96
- 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|
97
- @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
98
122
  end
99
123
 
100
- 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|
101
- @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
102
126
  end
103
127
 
104
- opts.on "--import-path SOURCE_PATH", "Import configuration from the configuration on another path. --path TARGET_PATH is required." do |path|
105
- @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
106
130
  end
107
131
 
108
- 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|
109
- @diff = file_name
132
+ opts.on "-p", "--path PATH", "Import/Export/Diff to/from this path." do |path|
133
+ @path = path
110
134
  end
111
135
 
112
- opts.on "--diff-path SOURCE_PATH", "Diff configuration with the configuration on another path. --path TARGET_PATH is required." do |path|
113
- @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
114
138
  end
115
139
 
116
140
  opts.on "-s", "--set KEY=VALUE", "Set one key to value. Example: --set mysql/database=localhost" do |param|
@@ -120,55 +144,55 @@ module SecretConfig
120
144
  end
121
145
  end
122
146
 
123
- 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|
124
148
  @fetch_key = key
125
149
  end
126
150
 
127
- 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|
128
152
  @delete_key = key
129
153
  end
130
154
 
131
- opts.on "-r", "--delete-path PATH", "Recursively delete all keys under the specified path.. " do |path|
132
- @delete_path = path
155
+ opts.on "-r", "--delete-tree PATH", "Recursively delete all keys under the specified path." do |path|
156
+ @delete_tree = path
133
157
  end
134
158
 
135
159
  opts.on "-c", "--console", "Start interactive console." do
136
160
  @console = true
137
161
  end
138
162
 
139
- opts.on "-p", "--path PATH", "Path in central configuration to use." do |path|
140
- @path = path
141
- end
142
-
143
163
  opts.on "--provider PROVIDER", "Provider to use. [ssm | file]. Default: ssm" do |provider|
144
164
  @provider = provider.to_sym
145
165
  end
146
166
 
147
- 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
148
168
  @no_filter = true
149
169
  end
150
170
 
151
- 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
152
176
  @prune = true
153
177
  end
154
178
 
155
- opts.on "--key_id KEY_ID", "Encrypt config settings with this AWS KMS key id. Default: AWS Default key." do |key_id|
156
- @key_id = key_id
179
+ opts.on "--force", "For --import only. Overwrite all values, not just the changed ones. Useful for changing the KMS key." do
180
+ @force = true
157
181
  end
158
182
 
159
- opts.on "--key_alias KEY_ALIAS", "Encrypt config settings with this AWS KMS alias." do |key_alias|
160
- @key_alias = key_alias
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|
184
+ @key_id = key_id
161
185
  end
162
186
 
163
- opts.on "--region REGION", "AWS Region to use. Default: AWS_REGION env var." do |region|
164
- @region = region
187
+ opts.on "--key_alias KEY_ALIAS", "For --import only. Encrypt config settings with this AWS KMS alias." do |key_alias|
188
+ @key_alias = key_alias
165
189
  end
166
190
 
167
- 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|
168
192
  @random_size = random_size
169
193
  end
170
194
 
171
- opts.on "-v", "--version", "Display Symmetric Encryption version." do
195
+ opts.on "-v", "--version", "Display Secret Config version." do
172
196
  @show_version = true
173
197
  end
174
198
 
@@ -182,67 +206,66 @@ module SecretConfig
182
206
  private
183
207
 
184
208
  def provider_instance
185
- @provider_instance ||= begin
186
- case provider
187
- when :ssm
188
- Providers::Ssm.new(key_id: key_id, key_alias: key_alias)
189
- else
190
- raise ArgumentError, "Invalid provider: #{provider}"
209
+ @provider_instance ||=
210
+ begin
211
+ case provider
212
+ when :ssm
213
+ Providers::Ssm.new(key_id: key_id, key_alias: key_alias)
214
+ else
215
+ raise ArgumentError, "Invalid provider: #{provider}"
216
+ end
191
217
  end
192
- end
193
218
  end
194
219
 
195
- def run_export(file_name, path, filtered: true)
196
- raise(ArgumentError, "Missing required option --path") unless path
220
+ def run_export(source_path, file_name, filtered: true)
221
+ puts("Exporting #{provider}:#{source_path} to #{file_name}") if file_name.is_a?(String)
197
222
 
198
- config = fetch_config(path, filtered: filtered)
223
+ config = fetch_config(source_path, filtered: filtered)
199
224
  write_config_file(file_name, config)
200
-
201
- puts("Exported #{path} from #{provider} to #{file_name}") if file_name.is_a?(String)
202
225
  end
203
226
 
204
- def run_import(file_name, path, prune = false)
205
- raise(ArgumentError, "Missing required option --path") unless path
206
-
227
+ def run_import(target_path, file_name, prune, force)
228
+ puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
229
+ puts "+++ #{file_name}#{Colors::CLEAR}"
207
230
  config = read_config_file(file_name)
208
- import_config(config, path, prune)
209
-
210
- puts("Imported #{file_name} to #{path} on provider: #{provider}") if file_name.is_a?(String)
231
+ import_config(config, target_path, prune, force)
211
232
  end
212
233
 
213
- def run_import_path(source_path, path, prune = false)
214
- raise(ArgumentError, "Missing required option --path") unless path
234
+ def run_import_path(target_path, source_path, prune, force)
235
+ puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
236
+ puts "+++ #{provider}:#{source_path}#{Colors::CLEAR}"
215
237
 
216
238
  config = fetch_config(source_path, filtered: false)
217
- import_config(config, path, prune)
239
+ import_config(config, target_path, prune, force)
218
240
 
219
- puts("Imported #{source_path} to #{path} on provider: #{provider}")
241
+ puts("Imported #{target_path} from #{source_path} on provider: #{provider}")
220
242
  end
221
243
 
222
- def run_diff(file_name, path)
223
- raise(ArgumentError, "Missing required option --path") unless path
224
-
225
- file_config = read_config_file(file_name)
226
- file = Utils.flatten(file_config, path)
244
+ def run_diff(target_path, file_name)
245
+ source_config = read_config_file(file_name)
246
+ source = Utils.flatten(source_config, target_path)
227
247
 
228
- registry_config = fetch_config(path, filtered: false)
229
- registry = Utils.flatten(registry_config, path)
248
+ target_config = fetch_config(target_path, filtered: false)
249
+ target = Utils.flatten(target_config, target_path)
230
250
 
231
- puts("Comparing #{file_name} to #{path} on provider: #{provider}") if file_name.is_a?(String)
232
- diff_config(file, registry)
251
+ if file_name.is_a?(String)
252
+ puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
253
+ puts "+++ #{file_name}#{Colors::CLEAR}"
254
+ end
255
+ diff_config(target, source)
233
256
  end
234
257
 
235
- def run_diff_path(source_path, path)
236
- raise(ArgumentError, "Missing required option --path") unless path
237
-
258
+ def run_diff_path(target_path, source_path)
238
259
  source_config = fetch_config(source_path, filtered: false)
239
- source = Utils.flatten(source_config, path)
260
+ source = Utils.flatten(source_config)
261
+
262
+ target_config = fetch_config(target_path, filtered: false)
263
+ target = Utils.flatten(target_config)
240
264
 
241
- target_config = fetch_config(path, filtered: false)
242
- target = Utils.flatten(target_config, path)
265
+ puts "#{Colors::TITLE}--- #{provider}:#{target_path}"
266
+ puts "+++ #{provider}:#{source_path}#{Colors::CLEAR}"
243
267
 
244
- puts("Comparing #{source_path} to #{path} on provider: #{provider}")
245
- diff_config(source, target)
268
+ diff_config(target, source)
246
269
  end
247
270
 
248
271
  def run_console
@@ -250,9 +273,22 @@ module SecretConfig
250
273
  end
251
274
 
252
275
  def run_delete(key)
276
+ puts "#{Colors::TITLE}--- #{provider}:#{path}"
277
+ puts "#{Colors::REMOVE}- #{key}#{Colors::CLEAR}"
253
278
  provider_instance.delete(key)
254
279
  end
255
280
 
281
+ def run_delete_tree(path)
282
+ source_config = fetch_config(path)
283
+ puts "#{Colors::TITLE}--- #{provider}:#{path}#{Colors::CLEAR}"
284
+
285
+ source = Utils.flatten(source_config, path)
286
+ source.each_key do |key|
287
+ puts "#{Colors::REMOVE}- #{key}#{Colors::CLEAR}"
288
+ provider_instance.delete(key)
289
+ end
290
+ end
291
+
256
292
  def run_fetch(key)
257
293
  value = provider_instance.fetch(key)
258
294
  puts value if value
@@ -262,8 +298,8 @@ module SecretConfig
262
298
  provider_instance.set(key, value)
263
299
  end
264
300
 
265
- def current_values
266
- @current_values ||= Utils.flatten(fetch_config(path, filtered: false), path)
301
+ def current_values(path)
302
+ Utils.flatten(fetch_config(path, filtered: false), path)
267
303
  end
268
304
 
269
305
  def read_config_file(file_name)
@@ -291,38 +327,53 @@ module SecretConfig
291
327
  # Ignore filtered values
292
328
  next
293
329
  end
294
- puts "Setting: #{key}"
330
+
331
+ if current_values.key?(key)
332
+ puts "#{Colors::KEY}* #{key}#{Colors::CLEAR}"
333
+ else
334
+ puts "#{Colors::ADD}+ #{key}#{Colors::CLEAR}"
335
+ end
336
+
295
337
  provider_instance.set(key, value)
296
338
  end
297
339
  end
298
340
 
299
341
  def fetch_config(path, filtered: true)
300
- registry = Registry.new(path: path, provider: provider_instance)
342
+ registry = Registry.new(path: path, provider: provider_instance, interpolate: interpolate)
301
343
  config = filtered ? registry.configuration : registry.configuration(filters: nil)
302
344
  sort_hash_by_key!(config)
303
345
  end
304
346
 
305
347
  # Diffs two configs and displays the results
306
- def diff_config(source, target)
348
+ def diff_config(target, source)
307
349
  (source.keys + target.keys).sort.uniq.each do |key|
308
350
  if target.key?(key)
309
351
  if source.key?(key)
310
352
  value = source[key].to_s
311
353
  # Ignore filtered values
312
- puts "* #{key}: #{target[key]} => #{source[key]}" if (value != target[key].to_s) && (value != FILTERED)
354
+ if (value != target[key].to_s) && (value != FILTERED)
355
+ puts "#{Colors::KEY}#{key}:"
356
+ puts "#{Colors::REMOVE}#{prefix_lines("- ", target[key])}"
357
+ puts "#{Colors::ADD}#{prefix_lines("+ ", source[key])}#{Colors::CLEAR}\n\n"
358
+ end
313
359
  else
314
- puts "- #{key}"
360
+ puts "#{Colors::KEY}#{key}:"
361
+ puts "#{Colors::REMOVE}#{prefix_lines("- ", target[key])}\n\n"
315
362
  end
316
363
  elsif source.key?(key)
317
- puts "+ #{key}: #{source[key]}"
364
+ puts "#{Colors::KEY}#{key}:"
365
+ puts "#{Colors::ADD}#{prefix_lines("+ ", source[key])}#{Colors::CLEAR}\n\n"
318
366
  end
319
367
  end
320
368
  end
321
369
 
322
- def import_config(config, path, prune = false)
323
- raise(ArgumentError, "Missing required option --path") unless path
370
+ def prefix_lines(prefix, value)
371
+ value.to_s.lines.collect { |line| "#{prefix}#{line}" }.join("")
372
+ end
324
373
 
325
- delete_keys = prune ? current_values.keys - Utils.flatten(config, path).keys : []
374
+ def import_config(config, path, prune, force)
375
+ current = current_values(path)
376
+ delete_keys = prune ? current.keys - Utils.flatten(config, path).keys : []
326
377
 
327
378
  unless delete_keys.empty?
328
379
  puts "Going to delete the following keys:"
@@ -330,10 +381,10 @@ module SecretConfig
330
381
  sleep(5)
331
382
  end
332
383
 
333
- set_config(config, path, current_values)
384
+ set_config(config, path, force ? {} : current)
334
385
 
335
386
  delete_keys.each do |key|
336
- puts "Deleting: #{key}"
387
+ puts "#{Colors::REMOVE}- #{key}#{Colors::CLEAR}"
337
388
  provider_instance.delete(key)
338
389
  end
339
390
  end