configuration_service 0.0.1
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 +7 -0
- data/.gitignore +36 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +41 -0
- data/README.md +30 -0
- data/README.rdoc +17 -0
- data/Rakefile +16 -0
- data/configuration_service.gemspec +24 -0
- data/lib/configuration_service/base.rb +82 -0
- data/lib/configuration_service/configuration.rb +29 -0
- data/lib/configuration_service/errors.rb +18 -0
- data/lib/configuration_service/provider/broken.rb +30 -0
- data/lib/configuration_service/provider/stub.rb +96 -0
- data/lib/configuration_service/provider/stub_store.rb +58 -0
- data/lib/configuration_service/provider.rb +13 -0
- data/lib/configuration_service/test/orchestration_provider.rb +252 -0
- data/lib/configuration_service/test/orchestration_provider_registry.rb +52 -0
- data/lib/configuration_service/test/orchestrator.rb +240 -0
- data/lib/configuration_service/test/orchestrator_environment_factory.rb +31 -0
- data/lib/configuration_service/test/response.rb +141 -0
- data/lib/configuration_service/test/stub_orchestration_provider.rb +61 -0
- data/lib/configuration_service/test.rb +25 -0
- data/lib/configuration_service.rb +17 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8c1d020e9ee176a21a78ca58785734d98ebda535
|
4
|
+
data.tar.gz: 5401e260c0d0a9da6ca31029b847b49fdaa8e6c7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 842390201eb9dc49e9fc37c7a9f9913a6f08ea28fc710000ae220245d53f7503f27a9b2df3446cc74b54acc68492b889159b7485d2282fdf0a42ca22e738262f
|
7
|
+
data.tar.gz: cc8bdbf31cbaed9751562d69295a521ac0c089c65c3da176fb2b74a521d7e08221c58ad61954c4d00a33f10a7682b855259ed80fb1a2f63613404d6c1db5312b
|
data/.gitignore
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
## Specific to RubyMotion:
|
14
|
+
.dat*
|
15
|
+
.repl_history
|
16
|
+
build/
|
17
|
+
|
18
|
+
## Documentation cache and generated files:
|
19
|
+
/.yardoc/
|
20
|
+
/_yardoc/
|
21
|
+
/doc/
|
22
|
+
/rdoc/
|
23
|
+
|
24
|
+
## Environment normalisation:
|
25
|
+
/.bundle/
|
26
|
+
/vendor/bundle
|
27
|
+
/lib/bundler/man/
|
28
|
+
|
29
|
+
# for a library or gem, you might want to ignore these files since the code is
|
30
|
+
# intended to run in multiple environments; otherwise, check them in:
|
31
|
+
# Gemfile.lock
|
32
|
+
# .ruby-version
|
33
|
+
# .ruby-gemset
|
34
|
+
|
35
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
36
|
+
.rvmrc
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
configuration_service (1.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
builder (3.2.2)
|
10
|
+
cucumber (2.0.2)
|
11
|
+
builder (>= 2.1.2)
|
12
|
+
cucumber-core (~> 1.2.0)
|
13
|
+
diff-lcs (>= 1.1.3)
|
14
|
+
gherkin (~> 2.12)
|
15
|
+
multi_json (>= 1.7.5, < 2.0)
|
16
|
+
multi_test (>= 0.1.2)
|
17
|
+
cucumber-core (1.2.0)
|
18
|
+
gherkin (~> 2.12.0)
|
19
|
+
diff-lcs (1.2.5)
|
20
|
+
gherkin (2.12.2)
|
21
|
+
multi_json (~> 1.3)
|
22
|
+
multi_json (1.11.2)
|
23
|
+
multi_test (0.1.2)
|
24
|
+
rake (10.4.2)
|
25
|
+
rspec-expectations (3.3.1)
|
26
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
27
|
+
rspec-support (~> 3.3.0)
|
28
|
+
rspec-support (3.3.0)
|
29
|
+
|
30
|
+
PLATFORMS
|
31
|
+
ruby
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
bundler (~> 1.10)
|
35
|
+
configuration_service!
|
36
|
+
cucumber (~> 2.0)
|
37
|
+
rake (~> 10.0)
|
38
|
+
rspec-expectations (~> 3.3)
|
39
|
+
|
40
|
+
BUNDLED WITH
|
41
|
+
1.10.3
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Configuration service
|
2
|
+
|
3
|
+
The configuration service provides authorized publication and consumption of identified configuration data with metadata.
|
4
|
+
|
5
|
+
## Documentation
|
6
|
+
|
7
|
+
Comprehensive rdoc documentation is available:
|
8
|
+
|
9
|
+
```shell
|
10
|
+
git clone git@github.com:hetznerZA/configuration_service.git
|
11
|
+
cd configuration_service
|
12
|
+
bundle install
|
13
|
+
bundle exec rake doc
|
14
|
+
xdg-open html/index.html
|
15
|
+
```
|
16
|
+
|
17
|
+
OSX users may need to use `open` instead of `xdg-open`.
|
18
|
+
|
19
|
+
## Testing
|
20
|
+
|
21
|
+
Test as follows:
|
22
|
+
|
23
|
+
```shell
|
24
|
+
git clone git@github.com:hetznerZA/configuration_service.git
|
25
|
+
cd configuration_service
|
26
|
+
bundle install
|
27
|
+
bundle exec rake test
|
28
|
+
```
|
29
|
+
|
30
|
+
Note that the tests are applied to a stub service provider. This just tests the test framework architecture.
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= Configuration service
|
2
|
+
|
3
|
+
The configuration service provides
|
4
|
+
{authorized}[link:features/authorization_feature.html]
|
5
|
+
{publication}[link:features/publishing_feature.html] and
|
6
|
+
{consumption}[link:features/consuming_feature.html]
|
7
|
+
of identified configuration data with metadata.
|
8
|
+
|
9
|
+
A pluggable Ruby API is provided in ConfigurationService::Base.
|
10
|
+
|
11
|
+
The declarative specification of the service is implemented with
|
12
|
+
cucumber, using a pluggable imperative orchestration providers. This
|
13
|
+
allows for implementations that are not providers to the Ruby API.
|
14
|
+
See ConfigurationService::Test.
|
15
|
+
|
16
|
+
A stub service provider is provided in
|
17
|
+
ConfigurationService::Provider::Stub.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rdoc/task'
|
3
|
+
|
4
|
+
desc "Test the stub test orchestrator"
|
5
|
+
task :test do
|
6
|
+
sh %{TEST_ORCHESTRATION_PROVIDER=stub bundle exec cucumber}
|
7
|
+
end
|
8
|
+
|
9
|
+
RDoc::Task.new do |rdoc|
|
10
|
+
rdoc.title = "Configuration service"
|
11
|
+
rdoc.main = "README.rdoc"
|
12
|
+
rdoc.rdoc_dir = "rdoc"
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Regenerate documentation"
|
16
|
+
task :doc => :rerdoc
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "configuration_service"
|
7
|
+
spec.version = "0.0.1"
|
8
|
+
spec.authors = ["Sheldon Hearn"]
|
9
|
+
spec.email = ["sheldonh@starjuice.net"]
|
10
|
+
|
11
|
+
spec.summary = %q{Configuration service}
|
12
|
+
spec.description = %q{Configuration service}
|
13
|
+
spec.homepage = "https://github.com/hetznerZA/configuration_service"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
21
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
22
|
+
spec.add_development_dependency "cucumber", "~> 2.0"
|
23
|
+
spec.add_development_dependency "rspec-expectations", "~> 3.3"
|
24
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module ConfigurationService
|
2
|
+
|
3
|
+
##
|
4
|
+
# The configuration service API
|
5
|
+
#
|
6
|
+
# The service provides
|
7
|
+
# {authorized}[link:features/authorization_feature.html]
|
8
|
+
# {publication}[link:features/publishing_feature.html] and
|
9
|
+
# {consumption}[link:features/consuming_feature.html]
|
10
|
+
# of identified configuration data with metadata.
|
11
|
+
#
|
12
|
+
class Base
|
13
|
+
|
14
|
+
##
|
15
|
+
# Creates a new API instance for the +provider+
|
16
|
+
#
|
17
|
+
def initialize(provider)
|
18
|
+
@provider = provider
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Requests configuration data and metadata
|
23
|
+
#
|
24
|
+
# Delegates the request to the provider. An optional +metadata_filter+
|
25
|
+
# may be supplied to restrict access to, for example, a specific revision
|
26
|
+
# of the configuration.
|
27
|
+
#
|
28
|
+
# Returns a Configuration object or raises an Error on failure.
|
29
|
+
# Returns +nil+ if no matching configuration was found.
|
30
|
+
#
|
31
|
+
def request_configuration(metadata_filter = {})
|
32
|
+
@provider.request_configuration(metadata_filter)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Publishes configuration +data+ and optional +metadata+
|
37
|
+
#
|
38
|
+
# Delegates the publication to the provider. The +data+ and the +metadata+ (if specified)
|
39
|
+
# must be dictionaries (responding to #to_hash or #to_h). The provider receives a
|
40
|
+
# copy of the +metadata+ decorated with the following keys:
|
41
|
+
#
|
42
|
+
# * "timestamp" - the current UTC time in ISO8601 format
|
43
|
+
# * "revision" - a UUID for this publication
|
44
|
+
#
|
45
|
+
# Returns a Configuration object or raises an Error on failure. When a Configuration object
|
46
|
+
# is returned, its +metadata+ is the decorated copy, allowing access to additional metadata
|
47
|
+
# added by the API or the provider.
|
48
|
+
#
|
49
|
+
def publish_configuration(data, metadata = {})
|
50
|
+
dictionary?(data) or raise ConfigurationService::Error, "data must be a dictionary"
|
51
|
+
dictionary?(metadata) or raise ConfigurationService::Error, "metadata must be a dictionary"
|
52
|
+
|
53
|
+
@provider.publish_configuration(data, decorate(metadata))
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# True if the +metadata+ matches the +metadata_filter+
|
58
|
+
#
|
59
|
+
# The +metadata+ matches the +metadata_filter+ if every key in the +metadata_filter+
|
60
|
+
# is present in the +metadata+, and the values of both keys are equal (==).
|
61
|
+
#
|
62
|
+
# This is a utility method intended for use by providers.
|
63
|
+
#
|
64
|
+
def self.metadata_matches_filter?(metadata, metadata_filter)
|
65
|
+
metadata_filter.all? { |k, v| v == metadata[k] }
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def dictionary?(o)
|
71
|
+
o.respond_to?(:to_hash) or o.respond_to?(:to_h)
|
72
|
+
end
|
73
|
+
|
74
|
+
def decorate(metadata)
|
75
|
+
revision = SecureRandom.uuid
|
76
|
+
metadata = metadata.merge("revision" => revision) unless metadata["revision"]
|
77
|
+
metadata = metadata.merge("timestamp" => Time.now.utc.iso8601) unless metadata["timestamp"]
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ConfigurationService
|
2
|
+
|
3
|
+
##
|
4
|
+
# Encapsulates configuration +data+ and its +metadata+
|
5
|
+
#
|
6
|
+
# * +data+ is a dictionary of (possibly sensitive) configuration data,
|
7
|
+
# which providers are expected to secure.
|
8
|
+
# * +metadata+ is a dictionary of data about the configuration data,
|
9
|
+
# which providers are not expected to secure.
|
10
|
+
#
|
11
|
+
# It is recommended that both dictionaries use strings as keys, and that
|
12
|
+
# values be limited to those that can be serialized to JSON.
|
13
|
+
#
|
14
|
+
class Configuration
|
15
|
+
|
16
|
+
attr_reader :data, :metadata, :revision # :nodoc:
|
17
|
+
|
18
|
+
##
|
19
|
+
# Returns a new Configuration
|
20
|
+
#
|
21
|
+
def initialize(data, metadata)
|
22
|
+
@data = data
|
23
|
+
@metadata = metadata
|
24
|
+
@revision = metadata["revision"]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ConfigurationService
|
2
|
+
|
3
|
+
##
|
4
|
+
# The base of the configuration service exception tree
|
5
|
+
#
|
6
|
+
# * Error
|
7
|
+
# * AuthorizationError
|
8
|
+
#
|
9
|
+
class Error < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# A configuration service authorization Error
|
14
|
+
#
|
15
|
+
class AuthorizationError < Error
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ConfigurationService
|
2
|
+
|
3
|
+
module Provider
|
4
|
+
|
5
|
+
##
|
6
|
+
# A stub broken ConfigurationService::Base service provider
|
7
|
+
#
|
8
|
+
# Used to validate the test framework architecture.
|
9
|
+
#
|
10
|
+
class Broken
|
11
|
+
|
12
|
+
##
|
13
|
+
# Raises Error
|
14
|
+
#
|
15
|
+
def publish_configuration(data, metadata)
|
16
|
+
raise ConfigurationService::Error, "error requested by test"
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Raises Error
|
21
|
+
#
|
22
|
+
def request_configuration(metadata_filter = {})
|
23
|
+
raise ConfigurationService::Error, "error requested by test"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "time"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
require "configuration_service"
|
5
|
+
|
6
|
+
require_relative "broken"
|
7
|
+
require_relative "stub_store"
|
8
|
+
|
9
|
+
module ConfigurationService
|
10
|
+
|
11
|
+
module Provider
|
12
|
+
|
13
|
+
##
|
14
|
+
# A stub ConfigurationService::Base service provider
|
15
|
+
#
|
16
|
+
# Used to validate the test framework architecture.
|
17
|
+
#
|
18
|
+
class Stub
|
19
|
+
|
20
|
+
##
|
21
|
+
# Maps roles to authorization tokens
|
22
|
+
#
|
23
|
+
BUILTIN_TOKENS = {
|
24
|
+
:consumer => '64867ebd-6364-0bd3-3fda-81-requestor',
|
25
|
+
:publisher => 'f53606cb-7f3c-4432-afe8-44-publisher',
|
26
|
+
:nothing => '2972abd7-b055-4841-8ad1-4a34-nothing',
|
27
|
+
} unless defined?(BUILTIN_TOKENS)
|
28
|
+
|
29
|
+
##
|
30
|
+
# Returns a new Stub
|
31
|
+
#
|
32
|
+
# The object is initialized with a configuration +identifier+ and an
|
33
|
+
# authorization +token+ and holds a reference to the singleton StubStore.
|
34
|
+
#
|
35
|
+
def initialize(identifier, token)
|
36
|
+
@identifier = identifier
|
37
|
+
@token = token
|
38
|
+
@configurations = StubStore.instance
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Requests configuration data and metadata
|
43
|
+
#
|
44
|
+
# See Base#request_configuration.
|
45
|
+
#
|
46
|
+
# The configured +identifier+ is used to fetch the data from the StubStore.
|
47
|
+
#
|
48
|
+
# An optional +metadata_filter+ may be supplied to restrict access to, for
|
49
|
+
# example, a specific revision of the configuration.
|
50
|
+
#
|
51
|
+
# Returns a Configuration object or raises AuthorizationError if the
|
52
|
+
# configured +token+ is not for the +:consumer+ role.
|
53
|
+
# Returns +nil+ if no matching configuration was found.
|
54
|
+
#
|
55
|
+
def request_configuration(metadata_filter)
|
56
|
+
authorize_request(:consumer)
|
57
|
+
data, metadata = @configurations.fetch(@identifier)
|
58
|
+
if Base.metadata_matches_filter?(metadata, metadata_filter)
|
59
|
+
Configuration.new(data, metadata)
|
60
|
+
end
|
61
|
+
rescue KeyError
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Publishes configuration +data+ and optional +metadata+
|
67
|
+
#
|
68
|
+
# See Base#publish_configuration.
|
69
|
+
#
|
70
|
+
# The +data+ and the +metadata+ (if specified) must be dictionaries
|
71
|
+
# (responding to #to_hash or #to_h). The provider receives a copy of the
|
72
|
+
# +metadata+ decorated with the following keys:
|
73
|
+
#
|
74
|
+
# * "timestamp" - the current UTC time in ISO8601 format
|
75
|
+
# * "revision" - a UUID for this publication
|
76
|
+
#
|
77
|
+
# Returns a Configuration object or raises an Error on failure.
|
78
|
+
#
|
79
|
+
def publish_configuration(data, metadata)
|
80
|
+
authorize_request(:publisher)
|
81
|
+
@configurations.store(@identifier, data, metadata)
|
82
|
+
Configuration.new(data, metadata)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def authorize_request(role)
|
88
|
+
raise AuthorizationError.new("missing token") unless @token
|
89
|
+
raise AuthorizationError.new("authorization failed") unless @token == BUILTIN_TOKENS[role]
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "singleton"
|
2
|
+
|
3
|
+
unless defined?(ConfigurationService::Provider::StubStore)
|
4
|
+
|
5
|
+
module ConfigurationService
|
6
|
+
|
7
|
+
module Provider
|
8
|
+
|
9
|
+
##
|
10
|
+
# A singleton store for the Stub service provider
|
11
|
+
#
|
12
|
+
class StubStore
|
13
|
+
|
14
|
+
include Singleton
|
15
|
+
|
16
|
+
def initialize # :nodoc:
|
17
|
+
@configurations = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Returns the configuration data and metadata for +identifier+
|
22
|
+
#
|
23
|
+
# The dictionaries are returned as an array, to be used as follows:
|
24
|
+
#
|
25
|
+
# data, metadata = StubStore.instance.fetch("acme")
|
26
|
+
#
|
27
|
+
def fetch(identifier)
|
28
|
+
@configurations.fetch(identifier)
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Stores the +data+ and +metadata associated with the +identifier+
|
33
|
+
#
|
34
|
+
def store(identifier, data, metadata)
|
35
|
+
@configurations.store(identifier, [data, metadata])
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Deletes the data and metadata associated with the +identifier+
|
40
|
+
#
|
41
|
+
# It is not an error to delete a non-existent +identifier+
|
42
|
+
#
|
43
|
+
def delete(identifier)
|
44
|
+
@configurations.delete(identifier)
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Return the singleton store instance
|
50
|
+
# :singleton-method: instance
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ConfigurationService
|
2
|
+
|
3
|
+
##
|
4
|
+
# A configuration service provider is an imperative implementation
|
5
|
+
# of the ConfigurationService API, to be plugged into a
|
6
|
+
# ConfigurationService::Base.
|
7
|
+
#
|
8
|
+
# A Stub implementation is provided to validate the test framework architecture.
|
9
|
+
#
|
10
|
+
module Provider
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|