configuration_service 2.1.1 → 2.2.0
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/.gitignore +4 -0
- data/README.rdoc +76 -20
- data/features/consuming.feature +1 -1
- data/features/listing.feature +8 -0
- data/features/publishing.feature +1 -1
- data/lib/configuration_service/admin_client.rb +44 -0
- data/lib/configuration_service/base.rb +4 -15
- data/lib/configuration_service/factory/context/symbolic_access_wrapper.rb +44 -0
- data/lib/configuration_service/factory/context.rb +84 -0
- data/lib/configuration_service/factory/environment_context/backward_compatibility.rb +89 -0
- data/lib/configuration_service/factory/environment_context/env_dict.rb +0 -13
- data/lib/configuration_service/factory/environment_context.rb +50 -73
- data/lib/configuration_service/factory.rb +92 -1
- data/lib/configuration_service/provider/stub.rb +2 -1
- data/lib/configuration_service/test/orchestrator.rb +2 -2
- data/lib/configuration_service/utils.rb +15 -0
- data/lib/configuration_service/version.rb +1 -1
- data/lib/configuration_service.rb +1 -0
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d695abf3843bb642d1bfed73e61ad6be4675007f
|
4
|
+
data.tar.gz: 23ff9de0ec5d4911aa5f83a96919527ed53f115a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 958979a72b6b19483a5983b8da46e1e6c01ce9febd67c0fac74c0a42128117d7218bcfd178a443fcadd3b214b4dc4d34e059e5f5183ffc23ee14b6632be28132
|
7
|
+
data.tar.gz: dec28262702b779b8ec85b453f1441dcb006475e68b859182772074f92ebbc6d2d0e47229659f7b17cccb9f706570ca229d649935b0b625b4e7dc05c5a76e4f5
|
data/.gitignore
CHANGED
data/README.rdoc
CHANGED
@@ -19,13 +19,13 @@ This is used to validate the test framework architecture, and may be used as a s
|
|
19
19
|
|
20
20
|
Service providers are registered against the {ConfigurationService::ProviderRegistry}.
|
21
21
|
|
22
|
-
|
22
|
+
Clients can be constructed with by the {ConfigurationService::Factory}.
|
23
23
|
|
24
24
|
== Usage
|
25
25
|
|
26
|
-
The recommended approach to creating a configuration service client is to use
|
26
|
+
The recommended approach to creating a configuration service client is to use the {ConfigurationService::Factory Factory}.
|
27
27
|
|
28
|
-
For example, we can use the
|
28
|
+
For example, we can use the factory
|
29
29
|
to create and configure a configuration service client backed by the vault provider as follows.
|
30
30
|
|
31
31
|
Our +main.rb+ (or +config.ru+ or whatever) is simple:
|
@@ -33,7 +33,7 @@ Our +main.rb+ (or +config.ru+ or whatever) is simple:
|
|
33
33
|
require 'bundler'
|
34
34
|
Bundler.require(:default)
|
35
35
|
|
36
|
-
service = ConfigurationService::Factory
|
36
|
+
service = ConfigurationService::Factory.create_client
|
37
37
|
configuraton = service.request_configuration
|
38
38
|
AcmeApplication.new(configuration.data).run
|
39
39
|
|
@@ -45,7 +45,7 @@ that contains whatever service provider we configure in the environment:
|
|
45
45
|
gem 'configuration_service-provider-vault'
|
46
46
|
gem 'acme_application'
|
47
47
|
|
48
|
-
Now we use the process environment to
|
48
|
+
Now we use the process environment to specify the factory context:
|
49
49
|
|
50
50
|
CFGSRV_IDENTIFIER="acme" \
|
51
51
|
CFGSRV_TOKEN="0b2a80f4-54ce-45f4-8267-f6558fee64af" \
|
@@ -56,22 +56,78 @@ Now we use the process environment to configure the {ConfigurationService::Facto
|
|
56
56
|
Note that +main.rb+ is completely decoupled from the selection of provider and provider configuration.
|
57
57
|
We could swap and/or reconfigure the provider by manipulating only the +Gemfile+ and the environment.
|
58
58
|
|
59
|
-
|
60
|
-
or if your strategy for bootstrapping the configuration service isn't expressed by an existing factory yet,
|
61
|
-
you can construct the service yourself:
|
59
|
+
=== Administrative client
|
62
60
|
|
63
|
-
|
61
|
+
The factory in the example above uses the process environment and (on JRuby) system properties as its context
|
62
|
+
because {ConfigurationService::Factory.create_client} defaults to {ConfigurationService::Factory::EnvironmentContext}.
|
63
|
+
To better understand the factory context, consider the example of a simple configuration service frontend
|
64
|
+
that needs an {ConfigurationService::AdminClient AdminClient} for administrative access to the service:
|
64
65
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
"
|
70
|
-
|
71
|
-
|
72
|
-
address: "http://127.0.0.1:8200"
|
73
|
-
)
|
66
|
+
# Bad example (see below for discussion)
|
67
|
+
context = ConfigurationService::Factory::Context.new(
|
68
|
+
"token" => "c3935418-f621-40de-ada3-cc8169f1348a",
|
69
|
+
"provider_id" => "vault",
|
70
|
+
"provider_config" => {
|
71
|
+
"address" => "http://127.0.0.1:8200"
|
72
|
+
}
|
74
73
|
)
|
75
|
-
|
76
|
-
|
74
|
+
admin_client = ConfigurationService::Factory.create_admin_client(context)
|
75
|
+
|
76
|
+
app = Rack::Builder.new do
|
77
|
+
map "/configurations" do
|
78
|
+
run ->(env) { [200, {'Content-Type' => 'application/json'}, [admin_client.list_configurations.to_json] }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
run app
|
82
|
+
|
83
|
+
The example above is terrible because it hardcodes an administrative token into the source.
|
84
|
+
In practice, the administrative context would most likely come from the configuration service itself.
|
85
|
+
|
86
|
+
=== Complex example
|
87
|
+
|
88
|
+
In the following example, the {ConfigurationService::Factory} defaults to using the
|
89
|
+
{ConfigurationService::Factory::EnvironmentContext EnvironmentContext} to acquire application configuration
|
90
|
+
from a non-administrative {ConfigurationService::Base} client.
|
91
|
+
Part of the application configuration is then used as the context for an administrative {ConfigurationService::AdminClient},
|
92
|
+
which is used as a model in the application.
|
93
|
+
|
94
|
+
configuration = ConfigurationService::Factory.create_client.request_configuration
|
95
|
+
app_name = configuration.data["app_name"]
|
96
|
+
admin_client = ConfigurationService::Factory.create_admin_client(configuration.data["cs_config"])
|
97
|
+
|
98
|
+
app = Rack::Builder.new do
|
99
|
+
map "/" do
|
100
|
+
run ->(env) { [200, {"Content-Type" => "application/json"}, ["Welcome to #{app_name}"]] }
|
101
|
+
end
|
102
|
+
map "/configurations" do
|
103
|
+
run ->(env) { [200, {'Content-Type' => 'application/json'}, [admin_client.list_configurations.to_json] }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
run app
|
107
|
+
|
108
|
+
The above code would rely on configuration data that looked something like this:
|
109
|
+
|
110
|
+
---
|
111
|
+
app_name: ACME config service UI
|
112
|
+
cs_config:
|
113
|
+
token: c3935418-f621-40de-ada3-cc8169f1348a
|
114
|
+
interface: admin
|
115
|
+
provider_id: vault
|
116
|
+
provider_config:
|
117
|
+
address: http://127.0.0.1:8200
|
118
|
+
|
119
|
+
If this configuration data was stored under the configuration identifier "acme", then access to the data
|
120
|
+
(via {ConfigurationService::Factory::EnvironmentContext}) would be arranged with a process environment like this:
|
121
|
+
|
122
|
+
CFGSRV_IDENTIFIER="acme" \
|
123
|
+
CFGSRV_TOKEN="0b2a80f4-54ce-45f4-8267-f6558fee64af" \
|
124
|
+
CFGSRV_PROVIDER="vault" \
|
125
|
+
CFGSRV_PROVIDER_ADDRESS="http://127.0.0.1:8200" \
|
126
|
+
bundle exec main.rb
|
77
127
|
|
128
|
+
So this application would use the token "0b2...4af" to access its configuration data
|
129
|
+
under the identifier "acme"
|
130
|
+
with a non-administrative {ConfigurationService::Base} client.
|
131
|
+
Part of its configuration data, in this case under the key "cs_config",
|
132
|
+
provides {ConfigurationService::Factory::Context} for creating an administrative {ConfigurationService::AdminClient},
|
133
|
+
which it would then use the token "c39...48a" to operate over multiple configuration identifiers on behalf of users.
|
data/features/consuming.feature
CHANGED
data/features/publishing.feature
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
require "configuration_service/utils"
|
2
|
+
|
3
|
+
module ConfigurationService
|
4
|
+
class AdminClient
|
5
|
+
|
6
|
+
##
|
7
|
+
# @param [String] token
|
8
|
+
# @param [String] provider
|
9
|
+
# @return [ConfigurationService::AdminClient] object
|
10
|
+
#
|
11
|
+
def initialize(token, provider)
|
12
|
+
@token = token
|
13
|
+
@provider = provider
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# @param [String] identifier
|
18
|
+
# @return [ConfigurationService::Configuration] the requested configuration
|
19
|
+
# @raise [ConfigurationService::ConfigurationNotFoundError]
|
20
|
+
#
|
21
|
+
def request_configuration(identifier)
|
22
|
+
@provider.request_configuration(identifier, @token) or
|
23
|
+
raise ConfigurationNotFoundError, "configuration not found for identifier: #{identifier}"
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# @param [String] identifier
|
28
|
+
# @param [Hash] data
|
29
|
+
# @param [Hash] metadata
|
30
|
+
# @return [ConfigurationService::Configuration] the published configuration
|
31
|
+
# @raise [ConfigurationaService::Error] if data or metadata is not a hash
|
32
|
+
#
|
33
|
+
def publish_configuration(identifier, data, metadata = {})
|
34
|
+
Utils.dictionary?(data) or raise ConfigurationService::Error, "data must be a dictionary"
|
35
|
+
Utils.dictionary?(metadata) or raise ConfigurationService::Error, "metadata must be a dictionary"
|
36
|
+
|
37
|
+
metadata = Utils.decorate(metadata)
|
38
|
+
configuration = Configuration.new(identifier, data, metadata)
|
39
|
+
@provider.publish_configuration(configuration, @token)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "securerandom"
|
2
|
+
require "configuration_service/utils"
|
2
3
|
|
3
4
|
module ConfigurationService
|
4
5
|
|
@@ -70,26 +71,14 @@ module ConfigurationService
|
|
70
71
|
# @raise [ConfigurationService::Error] if publication failed
|
71
72
|
#
|
72
73
|
def publish_configuration(data, metadata = {})
|
73
|
-
dictionary?(data) or raise ConfigurationService::Error, "data must be a dictionary"
|
74
|
-
dictionary?(metadata) or raise ConfigurationService::Error, "metadata must be a dictionary"
|
74
|
+
Utils.dictionary?(data) or raise ConfigurationService::Error, "data must be a dictionary"
|
75
|
+
Utils.dictionary?(metadata) or raise ConfigurationService::Error, "metadata must be a dictionary"
|
75
76
|
|
76
|
-
metadata = decorate(metadata)
|
77
|
+
metadata = Utils.decorate(metadata)
|
77
78
|
configuration = Configuration.new(@identifier, data, metadata)
|
78
79
|
@provider.publish_configuration(configuration, @token)
|
79
80
|
end
|
80
81
|
|
81
|
-
private
|
82
|
-
|
83
|
-
def dictionary?(o)
|
84
|
-
o.respond_to?(:to_hash) or o.respond_to?(:to_h)
|
85
|
-
end
|
86
|
-
|
87
|
-
def decorate(metadata)
|
88
|
-
revision = SecureRandom.uuid
|
89
|
-
metadata = metadata.merge("revision" => revision) unless metadata["revision"]
|
90
|
-
metadata = metadata.merge("timestamp" => Time.now.utc.iso8601) unless metadata["timestamp"]
|
91
|
-
end
|
92
|
-
|
93
82
|
end
|
94
83
|
|
95
84
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ConfigurationService
|
2
|
+
|
3
|
+
module Factory
|
4
|
+
|
5
|
+
class Context
|
6
|
+
|
7
|
+
class SymbolicAccessWrapper < Hash
|
8
|
+
|
9
|
+
def initialize(hash)
|
10
|
+
hash.each do |k, v|
|
11
|
+
if v.is_a?(Hash)
|
12
|
+
self.store(k.intern, self.class.new(v))
|
13
|
+
else
|
14
|
+
self.store(k.intern, v)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
self.default_proc = indifferent_access_default_proc
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def indifferent_access_default_proc
|
23
|
+
proc do |_, key|
|
24
|
+
try_string(key) or raise_key_error(key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def try_string(k)
|
29
|
+
if (skey = k.intern rescue false) and self.include?(skey)
|
30
|
+
self.fetch(skey)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def raise_key_error(k)
|
35
|
+
raise KeyError, "missing key #{k}"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module ConfigurationService
|
2
|
+
|
3
|
+
module Factory
|
4
|
+
|
5
|
+
##
|
6
|
+
# A factory context for static factory configuration
|
7
|
+
#
|
8
|
+
# The typical use case for this class is in constructing an {ConfigurationService::AdminClient AdminClient}
|
9
|
+
# using a data structure acquired from a secure source that does not need to be scrubbed.
|
10
|
+
#
|
11
|
+
# @see ConfigurationService::Factory.create_client
|
12
|
+
# @see ConfigurationService::Factory.create_admin_client
|
13
|
+
#
|
14
|
+
class Context
|
15
|
+
|
16
|
+
##
|
17
|
+
# Returns a new factory context
|
18
|
+
#
|
19
|
+
# @param [Hash] source
|
20
|
+
# a string- or symbol-keyed dictionary source
|
21
|
+
#
|
22
|
+
# The following keys are used by the {ConfigurationService::Factory}:
|
23
|
+
#
|
24
|
+
# [identifier] the unique identity of the configuration data
|
25
|
+
# (see {ConfigurationService::Base#initialize})
|
26
|
+
# [token] authorization token for the identified configuration data
|
27
|
+
# (see {ConfigurationService::Base#initialize})
|
28
|
+
# [provider_id] the unique identity of the service provider
|
29
|
+
# (see {ConfigurationService::ProviderRegistry})
|
30
|
+
# [provider_config] configuration options for the service provider
|
31
|
+
# (see service provider documentation)
|
32
|
+
#
|
33
|
+
def initialize(source)
|
34
|
+
@env = SymbolicAccessWrapper.new(source)
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# The unique identity of the configuration data
|
39
|
+
#
|
40
|
+
# Required for {ConfigurationService::Base}. Not used by {ConfigurationService::AdminClient}.
|
41
|
+
#
|
42
|
+
def identifier
|
43
|
+
@env[:identifier]
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Authorization token for the identified configuration data
|
48
|
+
#
|
49
|
+
def token
|
50
|
+
@env[:token]
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# The unique identity of the service provider
|
55
|
+
#
|
56
|
+
# @see ConfigurationService::ProviderRegistry
|
57
|
+
#
|
58
|
+
def provider_id
|
59
|
+
@env[:provider_id]
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Configuration options for the service provider
|
64
|
+
#
|
65
|
+
# See service provider documentation.
|
66
|
+
#
|
67
|
+
def provider_config
|
68
|
+
@env[:provider_config]
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Does nothing
|
73
|
+
#
|
74
|
+
# The source is assumed to be secure. Sources such as the process +ENV+ and (on JRuby) system properties
|
75
|
+
# should not be used with this class; instead, they should be accessed through {ConfigurationService::Factory::EnvironmentContext}.
|
76
|
+
#
|
77
|
+
def scrub!
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module ConfigurationService
|
2
|
+
|
3
|
+
module Factory
|
4
|
+
|
5
|
+
class EnvironmentContext
|
6
|
+
|
7
|
+
##
|
8
|
+
# Support {ConfigurationService::Factory::EnvironmentContext}'s deprecated use as a factory
|
9
|
+
#
|
10
|
+
# New code should use {ConfigurationService::Factory} instead.
|
11
|
+
#
|
12
|
+
module BackwardCompatibility
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
base.extend(ClassMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Create a configuration service bootstrapped with environmental configuration
|
20
|
+
#
|
21
|
+
# The environment is scanned for {#prefix} matches, within which the following variables are used:
|
22
|
+
#
|
23
|
+
# [IDENTIFIER] the unique identity of the configuration data
|
24
|
+
# (see {ConfigurationService::Base#initialize})
|
25
|
+
# [TOKEN] authorization token for the identified configuration data
|
26
|
+
# (see {ConfigurationService::Base#initialize})
|
27
|
+
# [PROVIDER] the unique identity of the service provider
|
28
|
+
# (see {ConfigurationService::ProviderRegistry})
|
29
|
+
# [PROVIDER_*] configuration options for the service provider
|
30
|
+
# (see service provider documentation)
|
31
|
+
#
|
32
|
+
# On JRuby, system properties are also scanned. Where a system property and environment variable of the
|
33
|
+
# same name exist, the environment variable is preferred.
|
34
|
+
#
|
35
|
+
# The service provider class is fetched from the {ConfigurationService::ProviderRegistry} using +PROVIDER+.
|
36
|
+
# A service provider instance is then constructed with a dictionary of the +PROVIDER_*+ variables,
|
37
|
+
# in which the keys are the name of the variable without +PROVIDER_+, downcased and intern'd.
|
38
|
+
#
|
39
|
+
# Then a service {ConfigurationService::Base} is constructed with the +IDENTIFIER+, +TOKEN+ and service provider instance.
|
40
|
+
#
|
41
|
+
# And finally, the environment is scrubbed of the variables used, to protect them from accidental exposure
|
42
|
+
# (e.g. in an exception handler that prints the environment). On JRuby, system properties are scrubbed of variables used as well,
|
43
|
+
# regardless of whether they were overridden by environment variables.
|
44
|
+
#
|
45
|
+
# @return [ConfigurationService::Base] the configuration service instance created
|
46
|
+
# @raise [ProviderNotFoundError] if no service provider has been registered with the name given by +PROVIDER+
|
47
|
+
#
|
48
|
+
# @deprecated Use {ConfigurationService::Factory} instead.
|
49
|
+
#
|
50
|
+
def create
|
51
|
+
ConfigurationService.new(@env[:identifier], @env[:token], provider).tap do
|
52
|
+
scrub!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module ClassMethods
|
57
|
+
|
58
|
+
##
|
59
|
+
# Return a configuration service bootstrapped with environmental configuration
|
60
|
+
#
|
61
|
+
# Instantiates a new factory with the process +ENV+ and the {DEFAULT_PREFIX} and uses it to create a configuration service.
|
62
|
+
#
|
63
|
+
# @see ConfigurationService::Factory::EnvironmentContext::BackwardCompatibility#create
|
64
|
+
#
|
65
|
+
# @deprecated Use {ConfigurationService::Factory} instead.
|
66
|
+
#
|
67
|
+
def create
|
68
|
+
new.create
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def provider
|
76
|
+
provider_id = @env[:provider]
|
77
|
+
provider_config = @env.subslice(:provider)
|
78
|
+
provider_class = ConfigurationService::ProviderRegistry.instance.lookup(provider_id)
|
79
|
+
provider_class or raise ProviderNotFoundError, "provider not registered: #{provider_id}"
|
80
|
+
provider_class.new(provider_config)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -24,11 +24,6 @@ module ConfigurationService
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
def subslice!(key)
|
28
|
-
delete_keys_prefixed_with!(key)
|
29
|
-
subslice(key)
|
30
|
-
end
|
31
|
-
|
32
27
|
def scrub!
|
33
28
|
@path or raise RuntimeError, "refusing to scrub without path"
|
34
29
|
|
@@ -84,14 +79,6 @@ module ConfigurationService
|
|
84
79
|
end
|
85
80
|
end
|
86
81
|
|
87
|
-
def delete_keys_prefixed_with!(key)
|
88
|
-
self.keys.each do |k|
|
89
|
-
if k.to_s.start_with?("#{key}_")
|
90
|
-
self.delete(k)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
82
|
def assert_not_scrubbed!
|
96
83
|
!@scrubbed or raise SecurityError, "can't access scrubbed environment dictionary"
|
97
84
|
end
|
@@ -1,41 +1,25 @@
|
|
1
|
+
require_relative "environment_context/backward_compatibility"
|
2
|
+
|
1
3
|
module ConfigurationService
|
2
4
|
|
3
5
|
module Factory
|
4
6
|
|
5
7
|
##
|
6
|
-
# A factory for
|
7
|
-
#
|
8
|
-
# @example Requesting configuration from the {https://github.com/hetznerZA/configuration_service-provider-vault Vault service provider}
|
9
|
-
#
|
10
|
-
# # With the following in the environment:
|
11
|
-
# #
|
12
|
-
# # CFGSRV_IDENTIFIER="acme"
|
13
|
-
# # CFGSRV_TOKEN="0b2a80f4-54ce-45f4-8267-f6558fee64af"
|
14
|
-
# # CFGSRV_PROVIDER="vault"
|
15
|
-
# # CFGSRV_PROVIDER_ADDRESS="http://127.0.0.1:8200"
|
16
|
-
#
|
17
|
-
# # And the following in Gemfile:
|
18
|
-
# #
|
19
|
-
# # source 'https://rubygems.org'
|
20
|
-
# #
|
21
|
-
# # gem 'configuration_service-provider-vault'
|
22
|
-
# # gem 'acme_application'
|
23
|
-
#
|
24
|
-
# # Now main.rb (or config.ru or whatever) is decoupled from provider
|
25
|
-
# # selection and service configuration:
|
8
|
+
# A factory context for the process +ENV+ and (on JRuby) system properties
|
26
9
|
#
|
27
|
-
#
|
28
|
-
#
|
10
|
+
# Most consumers will not need to interact with this class directly.
|
11
|
+
# It is used by {ConfigurationService::Factory} to create the default factory context
|
12
|
+
# if none is specified.
|
29
13
|
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
# @attr_reader [String] prefix
|
35
|
-
# prefix for matching environment variable names. Names match if they begin with the prefix and an underscore.
|
14
|
+
# Prior to version 2.2.0, this class provided factory functionality.
|
15
|
+
# Use of this class as a factory is deprecated, supported through the
|
16
|
+
# {ConfigurationService::Factory::EnvironmentContext::BackwardCompatibility} module.
|
17
|
+
# Consumers should use {ConfigurationService::Factory} instead.
|
36
18
|
#
|
37
19
|
class EnvironmentContext
|
38
20
|
|
21
|
+
include BackwardCompatibility
|
22
|
+
|
39
23
|
attr_reader :prefix
|
40
24
|
|
41
25
|
##
|
@@ -44,25 +28,14 @@ module ConfigurationService
|
|
44
28
|
DEFAULT_PREFIX = "CFGSRV" unless defined?(DEFAULT_PREFIX)
|
45
29
|
|
46
30
|
##
|
47
|
-
# Returns a new factory
|
31
|
+
# Returns a new factory context
|
48
32
|
#
|
49
33
|
# @param [Hash, Array<Hash>] sources
|
50
34
|
# one or a list of the string-keyed environments
|
51
35
|
# @param [String] prefix
|
52
36
|
# the prefix for matching environment variable names
|
53
37
|
#
|
54
|
-
#
|
55
|
-
# they can use {.create} which instantiates a factory with default +env+ and +prefix+
|
56
|
-
# and uses that internally.
|
57
|
-
#
|
58
|
-
def initialize(sources = default_sources, prefix = DEFAULT_PREFIX)
|
59
|
-
@env = EnvDict.new(sources, prefix)
|
60
|
-
end
|
61
|
-
|
62
|
-
##
|
63
|
-
# Create a configuration service bootstrapped with environmental configuration
|
64
|
-
#
|
65
|
-
# The environment is scanned for {#prefix} matches, within which the following variables are used:
|
38
|
+
# The sources are scanned for {#prefix} matches, within which the following variables are used:
|
66
39
|
#
|
67
40
|
# [IDENTIFIER] the unique identity of the configuration data
|
68
41
|
# (see {ConfigurationService::Base#initialize})
|
@@ -73,37 +46,49 @@ module ConfigurationService
|
|
73
46
|
# [PROVIDER_*] configuration options for the service provider
|
74
47
|
# (see service provider documentation)
|
75
48
|
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
# The service provider class is fetched from the {ConfigurationService::ProviderRegistry} using +PROVIDER+.
|
80
|
-
# A service provider instance is then constructed with a dictionary of the +PROVIDER_*+ variables,
|
81
|
-
# in which the keys are the name of the variable without +PROVIDER_+, downcased and intern'd.
|
82
|
-
#
|
83
|
-
# Then a service {ConfigurationService::Base} is constructed with the +IDENTIFIER+, +TOKEN+ and service provider instance.
|
84
|
-
#
|
85
|
-
# And finally, the environment is scrubbed of the variables used, to protect them from accidental exposure
|
86
|
-
# (e.g. in an exception handler that prints the environment). On JRuby, system properties are scrubbed of variables used as well,
|
87
|
-
# regardless of whether they were overridden by environment variables.
|
88
|
-
#
|
89
|
-
# @return [ConfigurationService::Base] the configuration service instance created
|
90
|
-
# @raise [ProviderNotFoundError] if no service provider has been registered with the name given by +PROVIDER+
|
49
|
+
# If sources are not specified, the process +ENV+ and (on JRuby) system properties are used.
|
50
|
+
# Where a system property and environment variable of the same name exist,
|
51
|
+
# the environment variable is preferred.
|
91
52
|
#
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
53
|
+
def initialize(sources = default_sources, prefix = DEFAULT_PREFIX)
|
54
|
+
@env = EnvDict.new(sources, prefix)
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# @see ConfigurationService::Factory::Context#identifier
|
59
|
+
def identifier
|
60
|
+
@env[:identifier]
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# @see ConfigurationService::Factory::Context#token
|
65
|
+
def token
|
66
|
+
@env[:token]
|
96
67
|
end
|
97
68
|
|
98
69
|
##
|
99
|
-
#
|
70
|
+
# @see ConfigurationService::Factory::Context#provider_id
|
71
|
+
def provider_id
|
72
|
+
@env[:provider]
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# @see ConfigurationService::Factory::Context#provider_config
|
77
|
+
def provider_config
|
78
|
+
@env.subslice(:provider)
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Scrub sources
|
100
83
|
#
|
101
|
-
#
|
84
|
+
# Called by the {ConfigurationService::Factory} after the client is created,
|
85
|
+
# to prevent in-process compromise of the context (e.g. by an exception handler
|
86
|
+
# that dumps the environment in a web application).
|
102
87
|
#
|
103
|
-
#
|
88
|
+
# Every property that matches the prefix is removed from all sources.
|
104
89
|
#
|
105
|
-
def
|
106
|
-
|
90
|
+
def scrub!
|
91
|
+
@env.scrub!
|
107
92
|
end
|
108
93
|
|
109
94
|
private
|
@@ -112,14 +97,6 @@ module ConfigurationService
|
|
112
97
|
(RUBY_PLATFORM == "java" ? [java.lang.System.properties, ENV] : ENV)
|
113
98
|
end
|
114
99
|
|
115
|
-
def provider
|
116
|
-
provider_id = @env[:provider]
|
117
|
-
provider_config = @env.subslice(:provider)
|
118
|
-
provider_class = ConfigurationService::ProviderRegistry.instance.lookup(provider_id)
|
119
|
-
provider_class or raise ProviderNotFoundError, "provider not registered: #{provider_id}"
|
120
|
-
provider_class.new(provider_config)
|
121
|
-
end
|
122
|
-
|
123
100
|
end
|
124
101
|
|
125
102
|
end
|
@@ -3,10 +3,101 @@ Dir.glob(File.expand_path("../factory/**/*.rb", __FILE__), &method(:require))
|
|
3
3
|
module ConfigurationService
|
4
4
|
|
5
5
|
##
|
6
|
-
#
|
6
|
+
# A factory for creating a configuration service client
|
7
|
+
#
|
8
|
+
# @example Requesting configuration from the {https://github.com/hetznerZA/configuration_service-provider-vault Vault service provider}
|
9
|
+
#
|
10
|
+
# # With the following in the environment:
|
11
|
+
# #
|
12
|
+
# # CFGSRV_IDENTIFIER="acme"
|
13
|
+
# # CFGSRV_TOKEN="0b2a80f4-54ce-45f4-8267-f6558fee64af"
|
14
|
+
# # CFGSRV_PROVIDER="vault"
|
15
|
+
# # CFGSRV_PROVIDER_ADDRESS="http://127.0.0.1:8200"
|
16
|
+
#
|
17
|
+
# # And the following in Gemfile:
|
18
|
+
# #
|
19
|
+
# # source 'https://rubygems.org'
|
20
|
+
# #
|
21
|
+
# # gem 'configuration_service-provider-vault'
|
22
|
+
# # gem 'acme_application'
|
23
|
+
#
|
24
|
+
# # Now main.rb (or config.ru or whatever) is decoupled from provider
|
25
|
+
# # selection and service configuration:
|
26
|
+
#
|
27
|
+
# require 'bundler'
|
28
|
+
# Bundler.require(:default) # Registers the vault provider
|
29
|
+
#
|
30
|
+
# configuration_service = ConfigurationService::Factory.create_client
|
31
|
+
# configuraton = configuration_service.request_configuration
|
32
|
+
# AcmeApplication.new(configuration.data).run
|
33
|
+
#
|
34
|
+
# @example Creating an {ConfigurationService::AdminClient AdminClient} with static bootstrap configuration
|
35
|
+
#
|
36
|
+
# admin_client = ConfigurationService::Factory.create_admin_client(
|
37
|
+
# "token" => "c3935418-f621-40de-ada3-cc8169f1348a",
|
38
|
+
# "provider_id" => "vault",
|
39
|
+
# "provider_config" => {
|
40
|
+
# "address" => "http://127.0.0.1:8200"
|
41
|
+
# }
|
42
|
+
# )
|
7
43
|
#
|
8
44
|
module Factory
|
9
45
|
|
46
|
+
##
|
47
|
+
# Create a configuration service client
|
48
|
+
#
|
49
|
+
# @param [ConfigurationService::Factory::EnvironmentContext, ConfigurationService::Factory::Context, Hash] context
|
50
|
+
# the factory context
|
51
|
+
# @return [ConfigurationService::Base] the configuration service client created
|
52
|
+
#
|
53
|
+
# When the +context+ is a Hash, it is wrapped in a {ConfigurationService::Factory::Context}, which does not scrub
|
54
|
+
# sources after the configuration service client is created. For this reason, the process +ENV+ should never
|
55
|
+
# be given as the +context+; rather give no +context+, so that the default {ConfigurationService::Factory::EnvironmentContext}
|
56
|
+
# will be used to safely scrub the process +ENV+ and (on JRuby) system properties after the configuration service client is created.
|
57
|
+
#
|
58
|
+
# The context must provide an +identifier+ property.
|
59
|
+
#
|
60
|
+
def self.create_client(context = EnvironmentContext.new)
|
61
|
+
context = Context.new(context) if context.is_a?(Hash)
|
62
|
+
provider = create_provider(context)
|
63
|
+
ConfigurationService::Base.new(context.identifier, context.token, provider)
|
64
|
+
ensure
|
65
|
+
context.scrub!
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Create a configuration service admin client
|
70
|
+
#
|
71
|
+
# @param [ConfigurationService::Factory::EnvironmentContext, ConfigurationService::Factory::Context, Hash] context
|
72
|
+
# the factory context
|
73
|
+
# @return [ConfigurationService::AdminClient] the configuration service admin client created
|
74
|
+
#
|
75
|
+
# When the +context+ is a Hash, it is wrapped in a {ConfigurationService::Factory::Context}, which does not scrub
|
76
|
+
# sources after the configuration service client is created. For this reason, the process +ENV+ should never
|
77
|
+
# be given as the +context+; rather give no +context+, so that the default {ConfigurationService::Factory::EnvironmentContext}
|
78
|
+
# will be used to safely scrub the process +ENV+ and (on JRuby) system properties after the configuration service client is created.
|
79
|
+
#
|
80
|
+
# Because the {ConfigurationService::AdminClient} operates over multiple configuration identifiers,
|
81
|
+
# it does not require an +identifier+ property from the context.
|
82
|
+
#
|
83
|
+
def self.create_admin_client(context = EnvironmentContext.new)
|
84
|
+
context = Context.new(context) if context.is_a?(Hash)
|
85
|
+
provider = create_provider(context)
|
86
|
+
ConfigurationService::AdminClient.new(context.token, provider)
|
87
|
+
ensure
|
88
|
+
context.scrub!
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def self.create_provider(context)
|
94
|
+
provider_id = context.provider_id
|
95
|
+
provider_config = context.provider_config
|
96
|
+
provider_class = ConfigurationService::ProviderRegistry.instance.lookup(provider_id)
|
97
|
+
provider_class or raise ProviderNotFoundError, "provider not registered: #{provider_id}"
|
98
|
+
provider_class.new(provider_config)
|
99
|
+
end
|
100
|
+
|
10
101
|
end
|
11
102
|
|
12
103
|
end
|
@@ -19,6 +19,7 @@ module ConfigurationService
|
|
19
19
|
class Stub
|
20
20
|
|
21
21
|
BUILTIN_TOKENS = {
|
22
|
+
:admin => 'b72b9ae7-3849-a335-67b6-administrator',
|
22
23
|
:consumer => '64867ebd-6364-0bd3-3fda-81-requestor',
|
23
24
|
:publisher => 'f53606cb-7f3c-4432-afe8-44-publisher',
|
24
25
|
:nothing => '2972abd7-b055-4841-8ad1-4a34-nothing',
|
@@ -74,7 +75,7 @@ module ConfigurationService
|
|
74
75
|
|
75
76
|
def authorize_request(role, token)
|
76
77
|
raise AuthorizationError.new("missing token") unless token
|
77
|
-
raise AuthorizationError.new("authorization failed") unless token == BUILTIN_TOKENS[role]
|
78
|
+
raise AuthorizationError.new("authorization failed") unless token == BUILTIN_TOKENS[role] or token == BUILTIN_TOKENS[:admin]
|
78
79
|
end
|
79
80
|
|
80
81
|
end
|
@@ -252,8 +252,8 @@ module ConfigurationService
|
|
252
252
|
# @todo replace with declarative test that delegates to orchestration provider
|
253
253
|
#
|
254
254
|
def bootstrap_configuration_service_environmentally
|
255
|
-
|
256
|
-
@service =
|
255
|
+
context = ConfigurationService::Factory::EnvironmentContext.new(@env, "CFGSRV")
|
256
|
+
@service = ConfigurationService::Factory.create_client(context)
|
257
257
|
end
|
258
258
|
|
259
259
|
##
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ConfigurationService
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
def self.dictionary?(o)
|
5
|
+
o.respond_to?(:to_hash) or o.respond_to?(:to_h)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.decorate(metadata)
|
9
|
+
revision = SecureRandom.uuid
|
10
|
+
metadata = metadata.merge("revision" => revision) unless metadata["revision"]
|
11
|
+
metadata = metadata.merge("timestamp" => Time.now.utc.iso8601) unless metadata["timestamp"]
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: configuration_service
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sheldon Hearn
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-06-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -99,6 +99,7 @@ files:
|
|
99
99
|
- features/authorization.feature
|
100
100
|
- features/bootstrapping.feature
|
101
101
|
- features/consuming.feature
|
102
|
+
- features/listing.feature
|
102
103
|
- features/publishing.feature
|
103
104
|
- features/step_definitions/authorization_steps.rb
|
104
105
|
- features/step_definitions/bootstrapping_steps.rb
|
@@ -106,11 +107,15 @@ files:
|
|
106
107
|
- features/step_definitions/publishing_steps.rb
|
107
108
|
- features/support/env.rb
|
108
109
|
- lib/configuration_service.rb
|
110
|
+
- lib/configuration_service/admin_client.rb
|
109
111
|
- lib/configuration_service/base.rb
|
110
112
|
- lib/configuration_service/configuration.rb
|
111
113
|
- lib/configuration_service/errors.rb
|
112
114
|
- lib/configuration_service/factory.rb
|
115
|
+
- lib/configuration_service/factory/context.rb
|
116
|
+
- lib/configuration_service/factory/context/symbolic_access_wrapper.rb
|
113
117
|
- lib/configuration_service/factory/environment_context.rb
|
118
|
+
- lib/configuration_service/factory/environment_context/backward_compatibility.rb
|
114
119
|
- lib/configuration_service/factory/environment_context/env_dict.rb
|
115
120
|
- lib/configuration_service/provider.rb
|
116
121
|
- lib/configuration_service/provider/broken.rb
|
@@ -124,6 +129,7 @@ files:
|
|
124
129
|
- lib/configuration_service/test/orchestrator_environment_factory.rb
|
125
130
|
- lib/configuration_service/test/response.rb
|
126
131
|
- lib/configuration_service/test/stub_orchestration_provider.rb
|
132
|
+
- lib/configuration_service/utils.rb
|
127
133
|
- lib/configuration_service/version.rb
|
128
134
|
homepage: https://github.com/hetznerZA/configuration_service
|
129
135
|
licenses: []
|
@@ -149,4 +155,3 @@ signing_key:
|
|
149
155
|
specification_version: 4
|
150
156
|
summary: Configuration service
|
151
157
|
test_files: []
|
152
|
-
has_rdoc:
|