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