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.
- checksums.yaml +4 -4
- data/README.md +17 -800
- data/bin/{secret_config → secret-config} +0 -0
- data/lib/secret_config.rb +1 -1
- data/lib/secret_config/cli.rb +134 -92
- data/lib/secret_config/config.rb +14 -0
- data/lib/secret_config/errors.rb +3 -0
- data/lib/secret_config/parser.rb +11 -11
- data/lib/secret_config/providers/file.rb +2 -2
- data/lib/secret_config/registry.rb +10 -10
- data/lib/secret_config/setting_interpolator.rb +19 -12
- data/lib/secret_config/string_interpolator.rb +7 -7
- data/lib/secret_config/utils.rb +1 -1
- data/lib/secret_config/version.rb +1 -1
- data/test/config/application.yml +4 -4
- data/test/parser_test.rb +2 -2
- data/test/providers/file_test.rb +1 -1
- data/test/providers/ssm_test.rb +1 -1
- data/test/registry_test.rb +7 -2
- data/test/secret_config_test.rb +0 -1
- data/test/setting_interpolator_test.rb +34 -27
- data/test/test_helper.rb +1 -1
- metadata +12 -12
data/lib/secret_config/errors.rb
CHANGED
data/lib/secret_config/parser.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 =~
|
41
|
+
next unless (key =~ %r{/__import__\Z}) || (key == "__import__")
|
42
42
|
|
43
43
|
import_key = tree.delete(key)
|
44
|
-
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(
|
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
|
@@ -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
|
18
|
-
@cache
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
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
|
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
|
-
# *
|
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 ||
|
14
|
+
@pattern = pattern || /\${1,2}\{([^}]+)\}/
|
15
15
|
end
|
16
16
|
|
17
17
|
def parse(string)
|
18
|
-
string.gsub(
|
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 } || []
|
data/lib/secret_config/utils.rb
CHANGED
data/test/config/application.yml
CHANGED
@@ -45,7 +45,7 @@ test:
|
|
45
45
|
mongo:
|
46
46
|
database: secret_config_test
|
47
47
|
primary: 127.0.0.1:27017
|
48
|
-
secondary: "
|
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: "
|
62
|
+
# database: "${fetch: /test/my_application/mysql/database }"
|
63
63
|
username: other
|
64
64
|
password: otherrules
|
65
|
-
host: "
|
65
|
+
host: "${hostname}"
|
66
66
|
|
67
67
|
mongo:
|
68
68
|
database: secret_config_test
|
69
69
|
primary: localhost:27017
|
70
|
-
secondary: "
|
70
|
+
secondary: "${hostname}:27018"
|
71
71
|
|
72
72
|
mongo2:
|
73
73
|
__import__: mongo
|
data/test/parser_test.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
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
|
data/test/providers/file_test.rb
CHANGED
@@ -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" => "
|
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",
|
data/test/providers/ssm_test.rb
CHANGED
@@ -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" => "
|
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",
|
data/test/registry_test.rb
CHANGED
@@ -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([
|
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([
|
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
|
|
data/test/secret_config_test.rb
CHANGED
@@ -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
|
10
|
-
expected = string.gsub("
|
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 = "
|
17
|
-
expected = string.gsub("
|
18
|
-
expected = expected.gsub("
|
19
|
-
expected = expected.gsub("
|
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
|
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 = "
|
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
|
42
|
-
expected = string.gsub("
|
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
|
49
|
-
expected = string.gsub("
|
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 = "
|
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
|
67
|
+
string = "Set a time of ${time} here."
|
68
68
|
time = Time.now
|
69
69
|
Time.stub(:now, time) do
|
70
|
-
expected = string.gsub("
|
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
|
78
|
-
expected = string.gsub("
|
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 = "
|
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
|
96
|
+
string = "Hello ${env:TEST_SETTING}. How are you?"
|
97
97
|
actual = interpolator.parse(string)
|
98
|
-
expected = string.gsub("
|
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 = "
|
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 = "
|
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 = "
|
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 = "
|
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 = "
|
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 = "
|
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)
|