configuration_service 2.1.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|