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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: edd7e401896ff28294ff48a3de1efcb4ff421ba4
4
- data.tar.gz: 9d839d5159631e31c845a84165058a19ca369af3
3
+ metadata.gz: d695abf3843bb642d1bfed73e61ad6be4675007f
4
+ data.tar.gz: 23ff9de0ec5d4911aa5f83a96919527ed53f115a
5
5
  SHA512:
6
- metadata.gz: 9a11deec68d0ec63f6a0502a62d1feae552b7f552bca92d63f8627a431860322f9c2acc39a79e1f4cbdc965102d877e98d3dbb24069461a9b9006bc8ce8728ef
7
- data.tar.gz: 567661134fd7d41f715d96c5b48db0f8b5e6eb23a2017ad82518f3010767aa356b5f26df58ad7a4adb96f83281335bfd4c84f72cc123976c90f7ccc84dabaf7a
6
+ metadata.gz: 958979a72b6b19483a5983b8da46e1e6c01ce9febd67c0fac74c0a42128117d7218bcfd178a443fcadd3b214b4dc4d34e059e5f5183ffc23ee14b6632be28132
7
+ data.tar.gz: dec28262702b779b8ec85b453f1441dcb006475e68b859182772074f92ebbc6d2d0e47229659f7b17cccb9f706570ca229d649935b0b625b4e7dc05c5a76e4f5
data/.gitignore CHANGED
@@ -35,3 +35,7 @@ build/
35
35
 
36
36
  # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
37
37
  .rvmrc
38
+
39
+ # vim
40
+ *.swp
41
+ *.swo
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
- Factories for creating and configuring service instances are provided in {ConfigurationService::Factory}.
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 a {ConfigurationService::Factory Factory}.
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 {ConfigurationService::Factory::EnvironmentContext EnvironmentContext} factory
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::EnvironmentContext.create
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 configure the {ConfigurationService::Factory::EnvironmentContext EnvironmentContext} factory:
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
- If you prefer to hard-code everything,
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
- # Bad example (hardcoding token into source):
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
- require 'configuration_service/provider/vault'
66
- require 'acme_application'
67
-
68
- service = ConfigurationService.new(
69
- "acme",
70
- "0b2a80f4-54ce-45f4-8267-f6558fee64af",
71
- ConfigurationService::Provider::Vault.new(
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
- configuraton = service.request_configuration
76
- AcmeApplication.new(configuration.data).run
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.
@@ -1,7 +1,7 @@
1
1
  @consuming @consume
2
2
  Feature: Consuming configuration data
3
3
  In order to access configuration data
4
- As a Consumer
4
+ As a consumer
5
5
  I want to request access to configuration data
6
6
 
7
7
  Scenario: Metadata-free hit
@@ -0,0 +1,8 @@
1
+ @listing @list
2
+ Feature: Listing configurations
3
+ In order to identify a specific configuration
4
+ As an administrator
5
+ I want to see a list of published configurations
6
+
7
+ Scenario: pending
8
+
@@ -1,7 +1,7 @@
1
1
  @publishing @publish
2
2
  Feature: Publishing configuration data
3
3
  In order to make configuration data available for retrieval
4
- As a publisher
4
+ As a publisher
5
5
  I want to publish configuration data
6
6
 
7
7
  Scenario: Revision added to metadata
@@ -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 creating a configuration service bootstrapped from environmental configuration
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
- # require 'bundler'
28
- # Bundler.require(:default) # Registers the vault provider
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
- # configuration_service = ConfigurationService::Factory::EnvironmentContext.create
31
- # configuraton = configuration_service.request_configuration
32
- # AcmeApplication.new(configuration.data).run
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
- # Most consumers will not need to call this method;
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
- # On JRuby, system properties are also scanned. Where a system property and environment variable of the
77
- # same name exist, the environment variable is preferred.
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 create
93
- ConfigurationService.new(@env[:identifier], @env[:token], provider).tap do
94
- @env.scrub!
95
- end
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
- # Return a configuration service bootstrapped with environmental configuration
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
- # Instantiates a new factory with the process +ENV+ and the {DEFAULT_PREFIX} and uses it to create a configuration service.
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
- # @see #create
88
+ # Every property that matches the prefix is removed from all sources.
104
89
  #
105
- def self.create
106
- new.create
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
- # Configuration service factories
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
- factory = ConfigurationService::Factory::EnvironmentContext.new(@env, "CFGSRV")
256
- @service = factory.create
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
@@ -1,5 +1,5 @@
1
1
  module ConfigurationService
2
2
 
3
- VERSION = "2.1.1"
3
+ VERSION = "2.2.0"
4
4
 
5
5
  end
@@ -1,3 +1,4 @@
1
+ require "configuration_service/admin_client"
1
2
  require "configuration_service/base"
2
3
  require "configuration_service/configuration"
3
4
  require "configuration_service/errors"
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.1.1
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-05-11 00:00:00.000000000 Z
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: