secret_config 0.8.0 → 0.10.3

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.
@@ -5,6 +5,9 @@ module SecretConfig
5
5
  class MissingMandatoryKey < Error
6
6
  end
7
7
 
8
+ class MissingEnvironmentVariable < Error
9
+ end
10
+
8
11
  class UndefinedRootError < Error
9
12
  end
10
13
 
@@ -2,26 +2,26 @@ module SecretConfig
2
2
  class Parser
3
3
  attr_reader :tree, :path, :registry, :interpolator
4
4
 
5
- def initialize(path, registry)
5
+ def initialize(path, registry, interpolate: true)
6
6
  @path = path
7
7
  @registry = registry
8
8
  @fetch_list = {}
9
9
  @import_list = {}
10
10
  @tree = {}
11
- @interpolator = SettingInterpolator.new
11
+ @interpolator = interpolate ? SettingInterpolator.new : nil
12
12
  end
13
13
 
14
14
  # Returns a flat path of keys and values from the provider without looking in the local path.
15
15
  # Keys are returned with path names relative to the supplied path.
16
16
  def parse(key, value)
17
17
  relative_key = relative_key?(key) ? key : key.sub("#{path}/", "")
18
- tree[relative_key] = value.is_a?(String) && value.include?("%{") ? interpolator.parse(value) : value
18
+ value = interpolator.parse(value) if interpolator && value.is_a?(String) && value.include?("${")
19
+ tree[relative_key] = value
19
20
  end
20
21
 
21
22
  # Returns a flat Hash of the rendered paths.
22
23
  def render
23
- # apply_fetches
24
- apply_imports
24
+ apply_imports if interpolator
25
25
  tree
26
26
  end
27
27
 
@@ -38,10 +38,11 @@ module SecretConfig
38
38
  # - Imports cannot reference other imports at this time.
39
39
  def apply_imports
40
40
  tree.keys.each do |key|
41
- next unless key =~ /\/__import__\Z/
41
+ next unless (key =~ %r{/__import__\Z}) || (key == "__import__")
42
42
 
43
43
  import_key = tree.delete(key)
44
- key, _ = ::File.split(key)
44
+ key, = ::File.split(key)
45
+ key = nil if key == "."
45
46
 
46
47
  # binding.irb
47
48
 
@@ -50,16 +51,16 @@ module SecretConfig
50
51
 
51
52
  if relative_key?(import_key)
52
53
  tree.keys.each do |current_key|
53
- match = current_key.match(/\A#{import_key}\/(.*)/)
54
+ match = current_key.match(%r{\A#{import_key}/(.*)})
54
55
  next unless match
55
56
 
56
- imported_key = ::File.join(key, match[1])
57
+ imported_key = key.nil? ? match[1] : ::File.join(key, match[1])
57
58
  tree[imported_key] = tree[current_key] unless tree.key?(imported_key)
58
59
  end
59
60
  else
60
61
  relative_paths = registry.send(:fetch_path, import_key)
61
62
  relative_paths.each_pair do |relative_key, value|
62
- imported_key = ::File.join(key, relative_key)
63
+ imported_key = key.nil? ? relative_key : ::File.join(key, relative_key)
63
64
  tree[imported_key] = value unless tree.key?(imported_key)
64
65
  end
65
66
  end
@@ -70,6 +71,5 @@ module SecretConfig
70
71
  def relative_key?(key)
71
72
  !key.start_with?("/")
72
73
  end
73
-
74
74
  end
75
75
  end
@@ -23,9 +23,9 @@ module SecretConfig
23
23
  end
24
24
 
25
25
  # Returns the value or `nil` if not found
26
- def fetch(key)
26
+ def fetch(_key)
27
27
  values = fetch_path(path)
28
- value.is_a?(Hash) ? nil : value
28
+ values.is_a?(Hash) ? nil : values
29
29
  end
30
30
 
31
31
  private
@@ -4,18 +4,19 @@ 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
 
@@ -61,11 +62,10 @@ module SecretConfig
61
62
 
62
63
  value = convert_encoding(encoding, value) if encoding
63
64
 
64
- if separator
65
- value.to_s.split(separator).collect { |element| convert_type(type, element.strip) }
66
- else
67
- convert_type(type, value)
68
- end
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) }
69
69
  end
70
70
 
71
71
  # Set the value for a key in the centralized configuration store.
@@ -115,7 +115,7 @@ module SecretConfig
115
115
  # Returns a flat path of keys and values from the provider without looking in the local path.
116
116
  # Keys are returned with path names relative to the supplied path.
117
117
  def fetch_path(path)
118
- parser = Parser.new(path, self)
118
+ parser = Parser.new(path, self, interpolate: interpolate)
119
119
  provider.each(path) { |key, value| parser.parse(key, value) }
120
120
  parser.render
121
121
  end
@@ -4,16 +4,19 @@ 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.
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
+ # # Raises SecretConfig::MissingEnvironmentVariable when the env var is not defined.
13
+ # ${env:name,default} # Extract value from the named environment value.
14
+ # # Returns the supplied default value when the env var is not defined.
15
+ # ${hostname} # Full name of this host.
16
+ # ${hostname:short} # Short name of this host. Everything up to the first period.
17
+ # ${pid} # Process Id for this process.
18
+ # ${random} # URL safe Random 32 byte value.
19
+ # ${random:size} # URL safe Random value of `size` bytes.
17
20
  module SecretConfig
18
21
  class SettingInterpolator < StringInterpolator
19
22
  def date(format = "%Y%m%d")
@@ -24,8 +27,12 @@ module SecretConfig
24
27
  Time.now.strftime(format)
25
28
  end
26
29
 
27
- def env(name)
28
- ENV[name]
30
+ def env(name, default = :no_default_supplied)
31
+ return ENV[name] if ENV.key?(name)
32
+
33
+ return default unless default == :no_default_supplied
34
+
35
+ raise(MissingEnvironmentVariable, "Missing mandatory environment variable: #{name}")
29
36
  end
30
37
 
31
38
  def hostname(format = nil)
@@ -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,20 +6,20 @@
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 = Regexp.last_match(1) || Regexp.last_match(2) || match.tr("%{}", "")
22
+ expr = Regexp.last_match(1) || Regexp.last_match(2) || match.tr("${}", "")
23
23
  key, args_str = expr.split(":")
24
24
  key = key.strip.to_sym
25
25
  arguments = args_str&.split(",")&.map { |v| v.strip == "" ? nil : v.strip } || []
@@ -7,7 +7,7 @@ module SecretConfig
7
7
  if key == NODE_KEY
8
8
  yield(path, value)
9
9
  else
10
- name = path.nil? ? key : "#{path}/#{key}"
10
+ name = path.nil? ? key : File.join(path, key)
11
11
  value.is_a?(Hash) ? flatten_each(value, name, &block) : yield(name, value)
12
12
  end
13
13
  end
@@ -1,3 +1,3 @@
1
1
  module SecretConfig
2
- VERSION = "0.8.0".freeze
2
+ VERSION = "0.10.3".freeze
3
3
  end
@@ -45,7 +45,7 @@ test:
45
45
  mongo:
46
46
  database: secret_config_test
47
47
  primary: 127.0.0.1:27017
48
- secondary: "%{hostname}:27018"
48
+ secondary: "${hostname}:27018"
49
49
 
50
50
  secrets:
51
51
  secret_key_base: somereallylongteststring
@@ -59,15 +59,15 @@ test:
59
59
  key: key0
60
60
 
61
61
  mysql:
62
- # database: "%{fetch: /test/my_application/mysql/database }"
62
+ # database: "${fetch: /test/my_application/mysql/database }"
63
63
  username: other
64
64
  password: otherrules
65
- host: "%{hostname}"
65
+ host: "${hostname}"
66
66
 
67
67
  mongo:
68
68
  database: secret_config_test
69
69
  primary: localhost:27017
70
- secondary: "%{hostname}:27018"
70
+ secondary: "${hostname}:27018"
71
71
 
72
72
  mongo2:
73
73
  __import__: mongo
@@ -26,7 +26,7 @@ class ParserTest < Minitest::Test
26
26
  #
27
27
  # Retrieve values elsewhere in the registry.
28
28
  # Paths can be relative to the current root, or absolute paths outside the current root.
29
- # %{fetch:key} # Fetches a single value from a relative or absolute path
29
+ # ${fetch:key} # Fetches a single value from a relative or absolute path
30
30
  # Return the value of the supplied key.
31
31
  #
32
32
  # With a relative key, look for the value in the current registry.
@@ -47,7 +47,7 @@ class ParserTest < Minitest::Test
47
47
  # end
48
48
  # end
49
49
 
50
- # %{import:path} # Imports a path of keys and values into the current path
50
+ # ${import:path} # Imports a path of keys and values into the current path
51
51
  # Replace the current value with a tree of values with the supplied path.
52
52
  #
53
53
  describe "#import" do
@@ -15,7 +15,7 @@ module Providers
15
15
  {
16
16
  "/test/my_application/mongo/database" => "secret_config_test",
17
17
  "/test/my_application/mongo/primary" => "127.0.0.1:27017",
18
- "/test/my_application/mongo/secondary" => "%{hostname}:27018",
18
+ "/test/my_application/mongo/secondary" => "${hostname}:27018",
19
19
  "/test/my_application/mysql/database" => "secret_config_test",
20
20
  "/test/my_application/mysql/password" => "secret_configrules",
21
21
  "/test/my_application/mysql/username" => "secret_config",
@@ -15,7 +15,7 @@ module Providers
15
15
  {
16
16
  "/test/my_application/mongo/database" => "secret_config_test",
17
17
  "/test/my_application/mongo/primary" => "127.0.0.1:27017",
18
- "/test/my_application/mongo/secondary" => "%{hostname}:27018",
18
+ "/test/my_application/mongo/secondary" => "${hostname}:27018",
19
19
  "/test/my_application/mysql/database" => "secret_config_test",
20
20
  "/test/my_application/mysql/password" => "secret_configrules",
21
21
  "/test/my_application/mysql/username" => "secret_config",
@@ -132,12 +132,17 @@ class RegistryTest < Minitest::Test
132
132
 
133
133
  it "of integers" do
134
134
  value = registry.fetch("mysql/ports", type: :integer, separator: ",")
135
- assert_equal([12345, 5343, 26815], value)
135
+ assert_equal([12_345, 5343, 26_815], value)
136
136
  end
137
137
 
138
138
  it "of integers with spaces" do
139
139
  value = registry.fetch("mysql/ports2", type: :integer, separator: ",")
140
- assert_equal([12345, 5343, 26815], value)
140
+ assert_equal([12_345, 5343, 26_815], value)
141
+ end
142
+
143
+ it "accepts a default without requiring conversion" do
144
+ value = registry.fetch("mysql/ports5", type: :integer, separator: ",", default: [23, 45, 72])
145
+ assert_equal([23, 45, 72], value)
141
146
  end
142
147
  end
143
148
 
@@ -96,6 +96,5 @@ class SecretConfigTest < Minitest::Test
96
96
  assert_equal true, database
97
97
  end
98
98
  end
99
-
100
99
  end
101
100
  end
@@ -6,23 +6,23 @@ module SecretConfig
6
6
 
7
7
  describe "#parse" do
8
8
  it "handles good key" do
9
- string = "Set a date of %{date} here."
10
- expected = string.gsub("%{date}", Date.today.strftime("%Y%m%d"))
9
+ string = "Set a date of ${date} here."
10
+ expected = string.gsub("${date}", Date.today.strftime("%Y%m%d"))
11
11
  actual = interpolator.parse(string)
12
12
  assert_equal expected, actual, string
13
13
  end
14
14
 
15
15
  it "handles multiple keys" do
16
- string = "%{pid}: Set a date of %{date} here and a %{time:%H%M} here and for luck %{pid}"
17
- expected = string.gsub("%{date}", Date.today.strftime("%Y%m%d"))
18
- expected = expected.gsub("%{time:%H%M}", Time.now.strftime("%H%M"))
19
- expected = expected.gsub("%{pid}", $$.to_s)
16
+ string = "${pid}: Set a date of ${date} here and a ${time:%H%M} here and for luck ${pid}"
17
+ expected = string.gsub("${date}", Date.today.strftime("%Y%m%d"))
18
+ expected = expected.gsub("${time:%H%M}", Time.now.strftime("%H%M"))
19
+ expected = expected.gsub("${pid}", $$.to_s)
20
20
  actual = interpolator.parse(string)
21
21
  assert_equal expected, actual, string
22
22
  end
23
23
 
24
24
  it "handles bad key" do
25
- string = "Set a date of %{blah} here."
25
+ string = "Set a date of ${blah} here."
26
26
  assert_raises InvalidInterpolation do
27
27
  interpolator.parse(string)
28
28
  end
@@ -31,22 +31,22 @@ module SecretConfig
31
31
 
32
32
  describe "#date" do
33
33
  it "interpolates date only" do
34
- string = "%{date}"
34
+ string = "${date}"
35
35
  expected = Date.today.strftime("%Y%m%d")
36
36
  actual = interpolator.parse(string)
37
37
  assert_equal expected, actual, string
38
38
  end
39
39
 
40
40
  it "interpolates date" do
41
- string = "Set a date of %{date} here."
42
- expected = string.gsub("%{date}", Date.today.strftime("%Y%m%d"))
41
+ string = "Set a date of ${date} here."
42
+ expected = string.gsub("${date}", Date.today.strftime("%Y%m%d"))
43
43
  actual = interpolator.parse(string)
44
44
  assert_equal expected, actual, string
45
45
  end
46
46
 
47
47
  it "interpolates date with custom format" do
48
- string = "Set a custom %{date:%m%d%Y} here."
49
- expected = string.gsub("%{date:%m%d%Y}", Date.today.strftime("%m%d%Y"))
48
+ string = "Set a custom ${date:%m%d%Y} here."
49
+ expected = string.gsub("${date:%m%d%Y}", Date.today.strftime("%m%d%Y"))
50
50
  actual = interpolator.parse(string)
51
51
  assert_equal expected, actual, string
52
52
  end
@@ -54,7 +54,7 @@ module SecretConfig
54
54
 
55
55
  describe "#time" do
56
56
  it "interpolates time only" do
57
- string = "%{time}"
57
+ string = "${time}"
58
58
  time = Time.now
59
59
  Time.stub(:now, time) do
60
60
  expected = Time.now.strftime("%Y%m%d%H%M%S%L")
@@ -64,18 +64,18 @@ module SecretConfig
64
64
  end
65
65
 
66
66
  it "interpolates time" do
67
- string = "Set a time of %{time} here."
67
+ string = "Set a time of ${time} here."
68
68
  time = Time.now
69
69
  Time.stub(:now, time) do
70
- expected = string.gsub("%{time}", Time.now.strftime("%Y%m%d%H%M%S%L"))
70
+ expected = string.gsub("${time}", Time.now.strftime("%Y%m%d%H%M%S%L"))
71
71
  actual = interpolator.parse(string)
72
72
  assert_equal expected, actual, string
73
73
  end
74
74
  end
75
75
 
76
76
  it "interpolates time with custom format" do
77
- string = "Set a custom time of %{time:%H%M} here."
78
- expected = string.gsub("%{time:%H%M}", Time.now.strftime("%H%M"))
77
+ string = "Set a custom time of ${time:%H%M} here."
78
+ expected = string.gsub("${time:%H%M}", Time.now.strftime("%H%M"))
79
79
  actual = interpolator.parse(string)
80
80
  assert_equal expected, actual, string
81
81
  end
@@ -87,34 +87,41 @@ module SecretConfig
87
87
  end
88
88
 
89
89
  it "fetches existing ENV var" do
90
- string = "%{env:TEST_SETTING}"
90
+ string = "${env:TEST_SETTING}"
91
91
  actual = interpolator.parse(string)
92
92
  assert_equal "Secret", actual, string
93
93
  end
94
94
 
95
95
  it "fetches existing ENV var into a larger string" do
96
- string = "Hello %{env:TEST_SETTING}. How are you?"
96
+ string = "Hello ${env:TEST_SETTING}. How are you?"
97
97
  actual = interpolator.parse(string)
98
- expected = string.gsub("%{env:TEST_SETTING}", "Secret")
98
+ expected = string.gsub("${env:TEST_SETTING}", "Secret")
99
99
  assert_equal expected, actual, string
100
100
  end
101
101
 
102
102
  it "handles missing ENV var" do
103
- string = "%{env:OTHER_TEST_SETTING}"
103
+ string = "${env:OTHER_TEST_SETTING}"
104
+ assert_raises SecretConfig::MissingEnvironmentVariable do
105
+ interpolator.parse(string)
106
+ end
107
+ end
108
+
109
+ it "uses default value for missing ENV var" do
110
+ string = "${env:OTHER_TEST_SETTING,My default value}"
104
111
  actual = interpolator.parse(string)
105
- assert_equal "", actual, string
112
+ assert_equal "My default value", actual, string
106
113
  end
107
114
  end
108
115
 
109
116
  describe "#hostname" do
110
117
  it "returns hostname" do
111
- string = "%{hostname}"
118
+ string = "${hostname}"
112
119
  actual = interpolator.parse(string)
113
120
  assert_equal Socket.gethostname, actual, string
114
121
  end
115
122
 
116
123
  it "returns short hostname" do
117
- string = "%{hostname:short}"
124
+ string = "${hostname:short}"
118
125
  actual = interpolator.parse(string)
119
126
  assert_equal Socket.gethostname.split(".")[0], actual, string
120
127
  end
@@ -122,7 +129,7 @@ module SecretConfig
122
129
 
123
130
  describe "#pid" do
124
131
  it "returns process id" do
125
- string = "%{pid}"
132
+ string = "${pid}"
126
133
  actual = interpolator.parse(string)
127
134
  assert_equal $$.to_s, actual, string
128
135
  end
@@ -130,7 +137,7 @@ module SecretConfig
130
137
 
131
138
  describe "#random" do
132
139
  it "interpolates random 32 byte string" do
133
- string = "%{random}"
140
+ string = "${random}"
134
141
  random = SecureRandom.urlsafe_base64(32)
135
142
  SecureRandom.stub(:urlsafe_base64, random) do
136
143
  actual = interpolator.parse(string)
@@ -139,7 +146,7 @@ module SecretConfig
139
146
  end
140
147
 
141
148
  it "interpolates custom length random string" do
142
- string = "%{random:64}"
149
+ string = "${random:64}"
143
150
  random = SecureRandom.urlsafe_base64(64)
144
151
  SecureRandom.stub(:urlsafe_base64, random) do
145
152
  actual = interpolator.parse(string)