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 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: