secret_config 0.7.0 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ module SecretConfig
2
+ class Config
3
+ extend Forwardable
4
+ def_delegator :registry, :configuration
5
+ def_delegator :registry, :refresh!
6
+
7
+ def initialize(path, registry)
8
+ @path = path
9
+ @registry = registry
10
+ end
11
+
12
+ def fetch(sub_path, **options)
13
+ registry.fetch(join_path(sub_path), **options)
14
+ end
15
+
16
+ def [](sub_path)
17
+ registry[join_path(sub_path)]
18
+ end
19
+
20
+ def []=(sub_path, value)
21
+ registry[join_path(sub_path)] = value
22
+ end
23
+
24
+ def key?(sub_path)
25
+ registry.key?(join_path(sub_path))
26
+ end
27
+
28
+ def set(sub_path, value)
29
+ registry.set(join_path(sub_path), value)
30
+ end
31
+
32
+ def delete(sub_path)
33
+ registry.delete(join_path(sub_path))
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :path, :registry
39
+
40
+ def join_path(sub_path)
41
+ File.join(path, sub_path)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,76 @@
1
+ module SecretConfig
2
+ class Parser
3
+ attr_reader :tree, :path, :registry, :interpolator
4
+
5
+ def initialize(path, registry, interpolate: true)
6
+ @path = path
7
+ @registry = registry
8
+ @fetch_list = {}
9
+ @import_list = {}
10
+ @tree = {}
11
+ @interpolator = interpolate ? SettingInterpolator.new : nil
12
+ end
13
+
14
+ # Returns a flat path of keys and values from the provider without looking in the local path.
15
+ # Keys are returned with path names relative to the supplied path.
16
+ def parse(key, value)
17
+ relative_key = relative_key?(key) ? key : key.sub("#{path}/", "")
18
+ value = interpolator.parse(value) if interpolator && value.is_a?(String) && value.include?("${")
19
+ tree[relative_key] = value
20
+ end
21
+
22
+ # Returns a flat Hash of the rendered paths.
23
+ def render
24
+ apply_imports if interpolator
25
+ tree
26
+ end
27
+
28
+ private
29
+
30
+ # def apply_fetches
31
+ # tree[key] = relative_key?(fetch_key) ? registry[fetch_key] : registry.provider.fetch(fetch_key)
32
+ # end
33
+
34
+ # Import from the current registry as well as new fetches.
35
+ #
36
+ # Notes:
37
+ # - A lot of absolute key lookups can be expensive since each one is a separate call.
38
+ # - Imports cannot reference other imports at this time.
39
+ def apply_imports
40
+ tree.keys.each do |key|
41
+ next unless (key =~ /\/__import__\Z/) || (key == "__import__")
42
+
43
+ import_key = tree.delete(key)
44
+ key, _ = ::File.split(key)
45
+ key = nil if key == "."
46
+
47
+ # binding.irb
48
+
49
+ # With a relative key, look for the values in the current registry.
50
+ # With an absolute key call the provider and fetch the value directly.
51
+
52
+ if relative_key?(import_key)
53
+ tree.keys.each do |current_key|
54
+ match = current_key.match(/\A#{import_key}\/(.*)/)
55
+ next unless match
56
+
57
+ imported_key = key.nil? ? match[1] : ::File.join(key, match[1])
58
+ tree[imported_key] = tree[current_key] unless tree.key?(imported_key)
59
+ end
60
+ else
61
+ relative_paths = registry.send(:fetch_path, import_key)
62
+ relative_paths.each_pair do |relative_key, value|
63
+ imported_key = key.nil? ? relative_key : ::File.join(key, relative_key)
64
+ tree[imported_key] = value unless tree.key?(imported_key)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # Returns [true|false] whether the supplied key is considered a relative key.
71
+ def relative_key?(key)
72
+ !key.start_with?("/")
73
+ end
74
+
75
+ end
76
+ end
@@ -12,17 +12,30 @@ module SecretConfig
12
12
  raise(ConfigurationError, "Cannot find config file: #{file_name}") unless ::File.exist?(file_name)
13
13
  end
14
14
 
15
+ # Yields the key with its absolute path and corresponding string value
15
16
  def each(path, &block)
16
- config = YAML.load(ERB.new(::File.new(file_name).read).result)
17
-
18
- paths = path.sub(%r{\A/*}, "").sub(%r{/*\Z}, "").split("/")
19
- settings = config.dig(*paths)
17
+ settings = fetch_path(path)
20
18
 
21
19
  raise(ConfigurationError, "Path #{paths.join('/')} not found in file: #{file_name}") unless settings
22
20
 
23
21
  Utils.flatten_each(settings, path, &block)
24
22
  nil
25
23
  end
24
+
25
+ # Returns the value or `nil` if not found
26
+ def fetch(key)
27
+ values = fetch_path(path)
28
+ value.is_a?(Hash) ? nil : value
29
+ end
30
+
31
+ private
32
+
33
+ def fetch_path(path)
34
+ config = YAML.load(ERB.new(::File.new(file_name).read).result)
35
+
36
+ paths = path.sub(%r{\A/*}, "").sub(%r{/*\Z}, "").split("/")
37
+ config.dig(*paths)
38
+ end
26
39
  end
27
40
  end
28
41
  end
@@ -10,7 +10,13 @@ module SecretConfig
10
10
  class Ssm < Provider
11
11
  attr_reader :client, :key_id, :retry_count, :retry_max_ms, :logger
12
12
 
13
- def initialize(key_id: ENV["AWS_ACCESS_KEY_ID"], key_alias: ENV["AWS_ACCESS_KEY_ALIAS"], retry_count: 10, retry_max_ms: 3_000)
13
+ def initialize(
14
+ key_id: ENV["SECRET_CONFIG_KEY_ID"],
15
+ key_alias: ENV["SECRET_CONFIG_KEY_ALIAS"],
16
+ retry_count: 25,
17
+ retry_max_ms: 10_000,
18
+ **args
19
+ )
14
20
  @key_id =
15
21
  if key_alias
16
22
  key_alias =~ %r{^alias/} ? key_alias : "alias/#{key_alias}"
@@ -20,9 +26,10 @@ module SecretConfig
20
26
  @retry_count = retry_count
21
27
  @retry_max_ms = retry_max_ms
22
28
  @logger = SemanticLogger["Aws::SSM"] if defined?(SemanticLogger)
23
- @client = Aws::SSM::Client.new(logger: logger)
29
+ @client = Aws::SSM::Client.new({logger: logger}.merge!(args))
24
30
  end
25
31
 
32
+ # Yields the key with its absolute path and corresponding string value
26
33
  def each(path)
27
34
  retries = 0
28
35
  token = nil
@@ -4,26 +4,31 @@ require "concurrent-ruby"
4
4
  module SecretConfig
5
5
  # Centralized configuration with values stored in AWS System Manager Parameter Store
6
6
  class Registry
7
- attr_reader :provider
7
+ attr_reader :provider, :interpolate
8
8
  attr_accessor :path
9
9
 
10
- def initialize(path: nil, provider: nil, provider_args: nil)
10
+ def initialize(path: nil, provider: nil, provider_args: nil, interpolate: true)
11
11
  @path = default_path(path)
12
12
  raise(UndefinedRootError, "Root must start with /") unless @path.start_with?("/")
13
13
 
14
14
  resolved_provider = default_provider(provider)
15
15
  provider_args = nil if resolved_provider != provider
16
16
 
17
- @provider = create_provider(resolved_provider, provider_args)
18
- @cache = Concurrent::Map.new
17
+ @provider = create_provider(resolved_provider, provider_args)
18
+ @cache = Concurrent::Map.new
19
+ @interpolate = interpolate
19
20
  refresh!
20
21
  end
21
22
 
22
23
  # Returns [Hash] a copy of the in memory configuration data.
23
- def configuration(relative: true, filters: SecretConfig.filters)
24
+ #
25
+ # Supply the relative path to start from so that only keys and values in that
26
+ # path will be returned.
27
+ def configuration(path: nil, filters: SecretConfig.filters)
24
28
  h = {}
25
29
  cache.each_pair do |key, value|
26
- key = relative_key(key) if relative
30
+ next if path && !key.start_with?(path)
31
+
27
32
  value = filter_value(key, value, filters)
28
33
  Utils.decompose(key, value, h)
29
34
  end
@@ -32,30 +37,35 @@ module SecretConfig
32
37
 
33
38
  # Returns [String] configuration value for the supplied key, or nil when missing.
34
39
  def [](key)
35
- full_key = expand_key(key)
36
- value = cache[full_key]
40
+ value = cache[key]
37
41
  if value.nil? && SecretConfig.check_env_var?
38
- value = env_var_override(key, value)
39
- cache[full_key] = value unless value.nil?
42
+ value = env_var_override(key, value)
43
+ cache[key] = value unless value.nil?
40
44
  end
41
- value
45
+ value.nil? ? nil : value.to_s
42
46
  end
43
47
 
44
48
  # Returns [String] configuration value for the supplied key, or nil when missing.
45
49
  def key?(key)
46
- cache.key?(expand_key(key))
50
+ cache.key?(key)
47
51
  end
48
52
 
49
53
  # Returns [String] configuration value for the supplied key
50
- def fetch(key, default: :no_default_supplied, type: :string, encoding: nil)
54
+ # Convert the string value into an array of values by supplying a `separator`.
55
+ def fetch(key, default: :no_default_supplied, type: :string, encoding: nil, separator: nil)
51
56
  value = self[key]
52
57
  if value.nil?
53
58
  raise(MissingMandatoryKey, "Missing configuration value for #{path}/#{key}") if default == :no_default_supplied
59
+
54
60
  value = block_given? ? yield : default
55
61
  end
56
62
 
57
63
  value = convert_encoding(encoding, value) if encoding
58
- type == :string ? value : convert_type(type, value)
64
+
65
+ return convert_type(type, value) unless separator
66
+ return value if value.is_a?(Array)
67
+
68
+ value.to_s.split(separator).collect { |element| convert_type(type, element.strip) }
59
69
  end
60
70
 
61
71
  # Set the value for a key in the centralized configuration store.
@@ -65,26 +75,25 @@ module SecretConfig
65
75
 
66
76
  # Set the value for a key in the centralized configuration store.
67
77
  def set(key, value)
68
- key = expand_key(key)
69
- provider.set(key, value)
78
+ full_key = expand_key(key)
79
+ provider.set(full_key, value)
70
80
  cache[key] = value
71
81
  end
72
82
 
73
83
  # Delete a key from the centralized configuration store.
74
84
  def delete(key)
75
- key = expand_key(key)
76
- provider.delete(key)
85
+ full_key = expand_key(key)
86
+ provider.delete(full_key)
77
87
  cache.delete(key)
78
88
  end
79
89
 
80
90
  # Refresh the in-memory cached copy of the centralized configuration information.
81
- # Environment variable values will take precendence over the central store values.
91
+ # Environment variable values will take precedence over the central store values.
82
92
  def refresh!
83
93
  existing_keys = cache.keys
84
94
  updated_keys = []
85
- provider.each(path) do |key, value|
86
- value = interpolator.parse(value) if value.is_a?(String) && value.include?('%{')
87
- cache[key] = env_var_override(relative_key(key), value)
95
+ fetch_path(path).each_pair do |key, value|
96
+ cache[key] = env_var_override(key, value)
88
97
  updated_keys << key
89
98
  end
90
99
 
@@ -98,8 +107,17 @@ module SecretConfig
98
107
 
99
108
  attr_reader :cache
100
109
 
101
- def interpolator
102
- @interpolator ||= SettingInterpolator.new
110
+ # Returns [true|false] whether the supplied key is considered a relative key.
111
+ def relative_key?(key)
112
+ !key.start_with?("/")
113
+ end
114
+
115
+ # Returns a flat path of keys and values from the provider without looking in the local path.
116
+ # Keys are returned with path names relative to the supplied path.
117
+ def fetch_path(path)
118
+ parser = Parser.new(path, self, interpolate: interpolate)
119
+ provider.each(path) { |key, value| parser.parse(key, value) }
120
+ parser.render
103
121
  end
104
122
 
105
123
  # Returns the value from an env var if it is present,
@@ -113,12 +131,7 @@ module SecretConfig
113
131
 
114
132
  # Add the path to the path if it is a relative path.
115
133
  def expand_key(key)
116
- key.start_with?("/") ? key : "#{path}/#{key}"
117
- end
118
-
119
- # Convert the key to a relative path by removing the path.
120
- def relative_key(key)
121
- key.start_with?("/") ? key.sub("#{path}/", "") : key
134
+ relative_key?(key) ? "#{path}/#{key}" : key
122
135
  end
123
136
 
124
137
  def filter_value(key, value, filters)
@@ -140,12 +153,12 @@ module SecretConfig
140
153
 
141
154
  def convert_type(type, value)
142
155
  case type
156
+ when :string
157
+ value.to_s
143
158
  when :integer
144
159
  value.to_i
145
160
  when :float
146
161
  value.to_f
147
- when :string
148
- value
149
162
  when :boolean
150
163
  %w[true 1 t].include?(value.to_s.downcase)
151
164
  when :symbol
@@ -171,7 +184,7 @@ module SecretConfig
171
184
 
172
185
  raise(UndefinedRootError, "Either set env var 'SECRET_CONFIG_PATH' or call SecretConfig.use") unless path
173
186
 
174
- path.start_with?("/") ? path : "/#{path}"
187
+ relative_key?(path) ? "/#{path}" : path
175
188
  end
176
189
 
177
190
  def default_provider(provider)
@@ -1,24 +1,19 @@
1
- require 'date'
2
- require 'socket'
3
- require 'securerandom'
1
+ require "date"
2
+ require "socket"
3
+ require "securerandom"
4
4
  # * SecretConfig Interpolations
5
5
  #
6
6
  # Expanding values inline for date, time, hostname, pid and random values.
7
- # %{date} # Current date in the format of "%Y%m%d" (CCYYMMDD)
8
- # %{date:format} # Current date in the supplied format. See strftime
9
- # %{time} # Current date and time down to ms in the format of "%Y%m%d%Y%H%M%S%L" (CCYYMMDDHHMMSSmmm)
10
- # %{time:format} # Current date and time in the supplied format. See strftime
11
- # %{env:name} # Extract value from the named environment value.
12
- # %{hostname} # Full name of this host.
13
- # %{hostname:short} # Short name of this host. Everything up to the first period.
14
- # %{pid} # Process Id for this process.
15
- # %{random} # URL safe Random 32 byte value.
16
- # %{random:size} # URL safe Random value of `size` bytes.
17
- #
18
- # Retrieve values elsewhere in the registry.
19
- # Paths can be relative to the current root, or absolute paths outside the current root.
20
- # %{fetch:key} # Fetches a single value from a relative or absolute path
21
- # %{include:path} # Fetches a path of keys and values
7
+ # ${date} # Current date in the format of "%Y%m%d" (CCYYMMDD)
8
+ # ${date:format} # Current date in the supplied format. See strftime
9
+ # ${time} # Current date and time down to ms in the format of "%Y%m%d%Y%H%M%S%L" (CCYYMMDDHHMMSSmmm)
10
+ # ${time:format} # Current date and time in the supplied format. See strftime
11
+ # ${env:name} # Extract value from the named environment value.
12
+ # ${hostname} # Full name of this host.
13
+ # ${hostname:short} # Short name of this host. Everything up to the first period.
14
+ # ${pid} # Process Id for this process.
15
+ # ${random} # URL safe Random 32 byte value.
16
+ # ${random:size} # URL safe Random value of `size` bytes.
22
17
  module SecretConfig
23
18
  class SettingInterpolator < StringInterpolator
24
19
  def date(format = "%Y%m%d")
@@ -35,7 +30,7 @@ module SecretConfig
35
30
 
36
31
  def hostname(format = nil)
37
32
  name = Socket.gethostname
38
- name = name.split('.')[0] if format == "short"
33
+ name = name.split(".")[0] if format == "short"
39
34
  name
40
35
  end
41
36
 
@@ -46,13 +41,5 @@ module SecretConfig
46
41
  def random(size = 32)
47
42
  SecureRandom.urlsafe_base64(size)
48
43
  end
49
-
50
- #def fetch(key)
51
- # SecretConfig[key]
52
- #end
53
- #
54
- #def include(path)
55
- #
56
- #end
57
44
  end
58
45
  end
@@ -1,4 +1,4 @@
1
- # Parse strings containing %{key:value1,value2,value3}
1
+ # Parse strings containing ${key:value1,value2,value3}
2
2
  # Where `key` is a method implemented by a class inheriting from this class
3
3
  #
4
4
  # The following `key`s are reserved:
@@ -6,24 +6,25 @@
6
6
  # * initialize
7
7
  #
8
8
  # Notes:
9
- # * To prevent interpolation use %%{...}
10
- # * %% is not touched, only %{...} is identified.
9
+ # * To prevent interpolation use $${...}
10
+ # * $$ is not touched, only ${...} is identified.
11
11
  module SecretConfig
12
12
  class StringInterpolator
13
13
  def initialize(pattern = nil)
14
- @pattern = pattern || /%{1,2}\{([^}]+)\}/
14
+ @pattern = pattern || /\${1,2}\{([^}]+)\}/
15
15
  end
16
16
 
17
17
  def parse(string)
18
- string.gsub(/%{1,2}\{([^}]+)\}/) do |match|
19
- if match.start_with?('%%')
18
+ string.gsub(/\${1,2}\{([^}]+)\}/) do |match|
19
+ if match.start_with?("$$")
20
20
  match[1..-1]
21
21
  else
22
- expr = $1 || $2 || match.tr("%{}", "")
23
- key, args_str = expr.split(':')
22
+ expr = Regexp.last_match(1) || Regexp.last_match(2) || match.tr("${}", "")
23
+ key, args_str = expr.split(":")
24
24
  key = key.strip.to_sym
25
- arguments = args_str&.split(',')&.map { |v| v.strip == '' ? nil : v.strip } || []
25
+ arguments = args_str&.split(",")&.map { |v| v.strip == "" ? nil : v.strip } || []
26
26
  raise(InvalidInterpolation, "Invalid key: #{key} in string: #{match}") unless respond_to?(key)
27
+
27
28
  public_send(key, *arguments)
28
29
  end
29
30
  end