Configuration service

The configuration service provides authorized publication and consumption of identified configuration data with metadata. The service supports bootstrapping.

A pluggable Ruby API is provided in ConfigurationService::Client.

The declarative specification of the service is implemented with cucumber, using a pluggable imperative orchestration providers. This allows for implementations that are not providers to the Ruby API. See ConfigurationService::Test.

A stub service provider is provided in ConfigurationService::Provider::Stub. This is used to validate the test framework architecture, and may be used as a stub configuration service in tests. Other providers are available as gems.

Service providers are registered against the ConfigurationService::ProviderRegistry.

Clients can be constructed with by the ConfigurationService::Factory.

Usage

The recommended approach to creating a configuration service client is to use the Factory.

For example, we can use the factory to create and configure a configuration service client backed by the vault provider as follows.

Our main.rb (or config.ru or whatever) is simple:

require 'bundler'
Bundler.require(:default)

service = ConfigurationService::Factory.create_client
configuraton = service.request_configuration
AcmeApplication.new(configuration.data).run

This relies on a / bundler Gemfile to load the gem that contains whatever service provider we configure in the environment:

source 'https://rubygems.org'

gem 'configuration_service-provider-vault'
gem 'acme_application'

Now we use the process environment to specify the factory context:

CFGSRV_IDENTIFIER="acme" \
CFGSRV_TOKEN="0b2a80f4-54ce-45f4-8267-f6558fee64af" \
CFGSRV_PROVIDER="vault" \
CFGSRV_PROVIDER_ADDRESS="http://127.0.0.1:8200" \
bundle exec main.rb

Note that main.rb is completely decoupled from the selection of provider and provider configuration. We could swap and/or reconfigure the provider by manipulating only the Gemfile and the environment.

Administrative client

The factory in the example above uses the process environment and (on JRuby) system properties as its context because ConfigurationService::Factory.create_client defaults to ConfigurationService::Factory::EnvironmentContext. To better understand the factory context, consider the example of a simple configuration service frontend that needs a multi-identifier (admin) client for administrative access to the service:

# Bad example (see below for discussion)
context = ConfigurationService::Factory::Context.new(
  "token" => "c3935418-f621-40de-ada3-cc8169f1348a",
  # Note: no identifier provided
  "provider_id" => "vault",
  "provider_config" => {
    "address" => "http://127.0.0.1:8200"
  }
)
admin_client = ConfigurationService::Factory.create_client(context)

app = Rack::Builder.new do
  map "/configurations" do
    run ->(env) { [200, {'Content-Type' => 'application/json'}, [admin_client.list_configurations.to_json] }
  end
end
run app

The example above is terrible because it hardcodes an administrative token into the source. In practice, the administrative context would most likely come from the configuration service itself.

Complex example

In the following example, the ConfigurationService::Factory defaults to using the EnvironmentContext to acquire application configuration from a non-administrative ConfigurationService::Client client. Part of the application configuration is then used as the context for a multi-identifier admin client, which is used as a model in the application.

configuration = ConfigurationService::Factory.create_client.request_configuration
app_name = configuration.data["app_name"]
admin_client = ConfigurationService::Factory.create_client(configuration.data["cs_config"])

app = Rack::Builder.new do
  map "/" do
    run ->(env) { [200, {"Content-Type" => "application/json"}, ["Welcome to #{app_name}"]] }
  end
  map "/configurations" do
    run ->(env) { [200, {'Content-Type' => 'application/json'}, [admin_client.list_configurations.to_json] }
  end
end
run app

The above code would rely on configuration data that looked something like this:

---
app_name: ACME config service UI
cs_config:
  token: c3935418-f621-40de-ada3-cc8169f1348a
  # Note: no identifier provided
  provider_id: vault
  provider_config:
    address: http://127.0.0.1:8200

If this configuration data was stored under the configuration identifier “acme”, then access to the data (via ConfigurationService::Factory::EnvironmentContext) would be arranged with a process environment like this:

CFGSRV_IDENTIFIER="acme" \
CFGSRV_TOKEN="0b2a80f4-54ce-45f4-8267-f6558fee64af" \
CFGSRV_PROVIDER="vault" \
CFGSRV_PROVIDER_ADDRESS="http://127.0.0.1:8200" \
bundle exec main.rb

So this application would use the token “0b2…4af” to access its configuration data under the identifier “acme” with a non-administrative ConfigurationService::Client. Part of its configuration data, in this case under the key “cs_config”, provides ConfigurationService::Factory::Context for creating a multi-identifier admin client, which it would then use the token “c39…48a” to operate over multiple configuration identifiers on behalf of users.

Decorators

The service supports decoration of the provider to extend functionality. A ConfigurationService::Decorator is a class that take a service provider (or another decorator) as its only constructor argument, and provides the same methods as a service provider.

This allows the service provider to be composed into a chain of multiple decorators to extend the functionality of the service.

Decorator authors should be sure to register their decorators into the ConfigurationService::DecoratorRegistry. The gem comes with a sample decorator, ConfigurationService::Decorator::ReferenceResolver, although this is likely to be moved into a separate gem in a later release.

The easiest way to compose decorators into the service is with the ConfigurationService::Factory.

Here is sample environment that indicates to the factory that it should compose the reference resolver into the service it creates:

CFGSRV_IDENTIFIER="acme"
CFGSRV_TOKEN="0b2a80f4-54ce-45f4-8267-f6558fee64af"
CFGSRV_DECORATORS="reference_resolver"
CFGSRV_PROVIDER="vault"
CFGSRV_PROVIDER_ADDRESS="http://127.0.0.1:8200"