kumo_config 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/.buildkite/pipeline.yml +18 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/Gemfile +4 -0
- data/README.md +121 -0
- data/Rakefile +2 -0
- data/VERSION +1 -0
- data/kumo_config.gemspec +25 -0
- data/lib/kumo_config/environment_config.rb +125 -0
- data/lib/kumo_config/file_loader.rb +38 -0
- data/lib/kumo_config.rb +1 -0
- data/script/build.sh +12 -0
- data/script/integration_test.sh +13 -0
- data/script/unit_test.sh +13 -0
- data/spec/integration/.gitkeep +0 -0
- data/spec/integration/config_spec.rb +67 -0
- data/spec/integration/fixtures/.gitkeep +0 -0
- data/spec/integration/fixtures/common.yml +2 -0
- data/spec/integration/fixtures/common_secrets.yml +11 -0
- data/spec/integration/fixtures/development.yml +1 -0
- data/spec/integration/fixtures/development_secrets.yml +5 -0
- data/spec/integration/fixtures/production.yml +2 -0
- data/spec/integration/fixtures/production_secrets.yml +11 -0
- data/spec/lib/environment_config_spec.rb +202 -0
- data/spec/lib/file_loader_spec.rb +64 -0
- data/spec/spec_helper.rb +28 -0
- metadata +153 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# KumoConfig [](https://buildkite.com/redbubble/kumo-config-gem) [](https://codeclimate.com/github/redbubble/kumo_config_gem)
|
2
|
+
|
3
|
+
A utility for resolving environment configuration.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
This gem is automatically installed in the kumo container, so any `apply-env` or `deploy` scripts have access to it.
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'kumo_config'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install kumo_config
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
### Basic Usage
|
26
|
+
|
27
|
+
The basic usage will give you an EnvironmentConfig based on the paths you specify.
|
28
|
+
|
29
|
+
`config_path`: Where environments' configuration files live, e.g. `production.yml`, `staging.yml`, `production_secrets.yml`, `staging_secrets.yml`
|
30
|
+
`template_path`: Where the CloudFormation template lives - **TODO: THIS BELONGS IN KUMO_KEISEI, NOT HERE**
|
31
|
+
`params_template_file_path`: The location of the template for submitting paramters to CloudFormation. **AGAIN, THIS DOES NOT BELONG HERE**
|
32
|
+
`injected_config`: A Hash containing any configuration you want to inject at runtime rather than loading from a file. For example, you might want to pull some settings from a VPC.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
EnvironmentConfig.new(
|
36
|
+
config_path: File.join('/app', 'env', 'config'),
|
37
|
+
template_path: File.join('/app', 'env', 'cloudformation', 'redbubble.json'),
|
38
|
+
params_template_file_path: File.join('/app', 'env', 'cloudformation', 'redbubble.yml.erb')
|
39
|
+
injected_config: { key: 'value' }
|
40
|
+
)
|
41
|
+
```
|
42
|
+
|
43
|
+
### Configuration Hierarchy
|
44
|
+
|
45
|
+
Configuration will be loaded from the following sources:
|
46
|
+
|
47
|
+
1. `common.yml` and `common_secrets.yml` if they exist.
|
48
|
+
2. `{environment}.yml` and `{environment}_secrets.yml` or `development.yml` and `development_secrets.yml` if environment specific config does not exist.
|
49
|
+
|
50
|
+
### Injecting Configuration
|
51
|
+
|
52
|
+
You can also inject configuration at run time by adding it to the object provided to the `apply!` call:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
stack_config = {
|
56
|
+
config_path: File.join('/app', 'env', 'config'),
|
57
|
+
template_path: File.join('/app', 'env', 'cloudformation', 'myapp.json'),
|
58
|
+
injected_config: {
|
59
|
+
'Seed' => random_seed,
|
60
|
+
}
|
61
|
+
}
|
62
|
+
stack.apply!(stack_config)
|
63
|
+
```
|
64
|
+
|
65
|
+
### Getting the configuration and secrets without an `apply!`
|
66
|
+
|
67
|
+
If you need to inspect the configuration without applying a stack, call `config`:
|
68
|
+
```ruby
|
69
|
+
stack_config = {
|
70
|
+
config_path: File.join('/app', 'env', 'config'),
|
71
|
+
template_path: File.join('/app', 'env', 'cloudformation', 'myapp.json'),
|
72
|
+
injected_config: {
|
73
|
+
'Seed' => random_seed,
|
74
|
+
}
|
75
|
+
}
|
76
|
+
marshalled_config = stack.config(stack_config)
|
77
|
+
marshalled_secrets = stack.plain_text_secrets(stack_config)
|
78
|
+
|
79
|
+
if marshalled_config['DB_HOST'].start_with? '192.' then
|
80
|
+
passwd = marshalled_secrets['DB_PASS']
|
81
|
+
...
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
## Dependencies
|
86
|
+
|
87
|
+
#### Ruby Versions
|
88
|
+
|
89
|
+
This gem is tested with Ruby (MRI) versions 1.9.3 and 2.2.3.
|
90
|
+
|
91
|
+
## Release
|
92
|
+
|
93
|
+
1. Upgrade version in VERSION
|
94
|
+
2. Run ./script/release-gem
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
|
98
|
+
1. Fork it ( https://github.com/[my-github-username]/kumo_config/fork )
|
99
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
100
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
101
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
102
|
+
5. Create a new Pull Request
|
103
|
+
|
104
|
+
### Automated AWS Integration Tests - TODO: REVISE
|
105
|
+
|
106
|
+
You can test the Cloudformation responsibilities of this gem by extending the integration tests at `spec/integration`.
|
107
|
+
|
108
|
+
To run these tests you need a properly configured AWS environment (with `AWS_DEFAULT_REGION`, `AWS_ACCESS_KEY` and `AWS_SECRET_ACCESS_KEY` set) and then run `./script/integration_test.sh`.
|
109
|
+
|
110
|
+
If you run this within a Buildkite job then you will have a stack named "kumokeisei-test-$buildnumber" created and torn down for each integration test context. If you run this outside of a Buildkite job then the stack will be named "kumokeisei-test-$username".
|
111
|
+
|
112
|
+
### Manual testing with Kumo Tools container
|
113
|
+
|
114
|
+
Changes to the gem can be manually tested end to end in a project that uses the gem (i.e. http-wala).
|
115
|
+
|
116
|
+
1. First start the dev-tools container: `kumo tools debug non-production`
|
117
|
+
1. gem install specific_install
|
118
|
+
1. Re-install the gem: `gem specific_install https://github.com/redbubble/kumo_keisei_gem.git -b <your_branch>`
|
119
|
+
1. Fire up a console: `irb`
|
120
|
+
1. Require the gem: `require "kumo_keisei"`
|
121
|
+
1. Interact with the gem's classes. `KumoKeisei::Stack.new(...).apply!`
|
data/Rakefile
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/kumo_config.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
version = File.read(File.expand_path('../VERSION', __FILE__)).strip
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "kumo_config"
|
8
|
+
spec.version = version
|
9
|
+
spec.authors = ["Redbubble"]
|
10
|
+
spec.email = ["delivery-engineering@redbubble.com"]
|
11
|
+
spec.summary = "A utility for reading Kumo configuration"
|
12
|
+
spec.homepage = "http://redbubble.com"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency 'kumo_ki'
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
24
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
25
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'kumo_ki'
|
2
|
+
require 'logger'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require_relative 'file_loader'
|
6
|
+
|
7
|
+
module KumoConfig
|
8
|
+
# Environment Configuration for a cloud formation stack
|
9
|
+
class EnvironmentConfig
|
10
|
+
class ConfigurationError < StandardError; end
|
11
|
+
|
12
|
+
LOGGER = Logger.new(STDOUT)
|
13
|
+
|
14
|
+
attr_reader :app_name, :env_name
|
15
|
+
|
16
|
+
def initialize(options, logger = LOGGER)
|
17
|
+
@env_name = options[:env_name]
|
18
|
+
@params_template_file_path = options[:params_template_file_path]
|
19
|
+
@injected_config = options[:injected_config] || {}
|
20
|
+
@log = logger
|
21
|
+
|
22
|
+
if options[:config_path]
|
23
|
+
@config_file_loader = KumoConfig::FileLoader.new(config_dir_path: options[:config_path])
|
24
|
+
elsif options[:config_dir_path]
|
25
|
+
@log.warn "[DEPRECATION] `:config_dir_path` is deprecated, please pass in `:config_path` instead"
|
26
|
+
@config_file_loader = KumoConfig::FileLoader.new(config_dir_path: options[:config_dir_path])
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def production?
|
32
|
+
env_name == 'production'
|
33
|
+
end
|
34
|
+
|
35
|
+
def development?
|
36
|
+
!%w(production staging).include? env_name
|
37
|
+
end
|
38
|
+
|
39
|
+
def plain_text_secrets
|
40
|
+
@plain_text_secrets ||= decrypt_secrets(encrypted_secrets)
|
41
|
+
end
|
42
|
+
|
43
|
+
def config
|
44
|
+
# a hash of all settings that apply to this environment
|
45
|
+
@config ||= common_config.merge(env_config).merge(@injected_config)
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_binding
|
49
|
+
binding
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def kms
|
55
|
+
@kms ||= KumoKi::KMS.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def params_template_erb
|
59
|
+
return nil unless @params_template_file_path && File.exist?(@params_template_file_path)
|
60
|
+
template_file_loader = KumoConfig::FileLoader.new(config_dir_path: File.dirname(@params_template_file_path))
|
61
|
+
template_file_loader.load_erb(File.basename(@params_template_file_path))
|
62
|
+
end
|
63
|
+
|
64
|
+
def decrypt_secrets(secrets)
|
65
|
+
Hash[
|
66
|
+
secrets.map do |name, cipher_text|
|
67
|
+
@log.debug "Decrypting '#{name}'"
|
68
|
+
decrypt_cipher name, cipher_text
|
69
|
+
end
|
70
|
+
]
|
71
|
+
end
|
72
|
+
|
73
|
+
def decrypt_cipher(name, cipher_text)
|
74
|
+
if cipher_text.start_with? '[ENC,'
|
75
|
+
begin
|
76
|
+
[name, kms.decrypt(cipher_text[5, cipher_text.size]).to_s]
|
77
|
+
rescue
|
78
|
+
@log.error "Error decrypting secret '#{name}'"
|
79
|
+
raise
|
80
|
+
end
|
81
|
+
else
|
82
|
+
[name, cipher_text]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def env_config_file_name
|
87
|
+
"#{env_name}.yml"
|
88
|
+
end
|
89
|
+
|
90
|
+
def env_secrets_file_name
|
91
|
+
"#{env_name}_secrets.yml"
|
92
|
+
end
|
93
|
+
|
94
|
+
def encrypted_secrets
|
95
|
+
encrypted_common_secrets.merge(encrypted_env_secrets)
|
96
|
+
end
|
97
|
+
|
98
|
+
def encrypted_common_secrets
|
99
|
+
@config_file_loader.load_hash('common_secrets.yml')
|
100
|
+
end
|
101
|
+
|
102
|
+
def encrypted_env_secrets
|
103
|
+
secrets = @config_file_loader.load_hash(env_secrets_file_name)
|
104
|
+
|
105
|
+
if !secrets.empty?
|
106
|
+
secrets
|
107
|
+
else
|
108
|
+
@config_file_loader.load_hash('development_secrets.yml')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def common_config
|
113
|
+
@config_file_loader.load_hash('common.yml')
|
114
|
+
end
|
115
|
+
|
116
|
+
def env_config
|
117
|
+
config = @config_file_loader.load_hash(env_config_file_name)
|
118
|
+
if !config.empty?
|
119
|
+
config
|
120
|
+
else
|
121
|
+
@config_file_loader.load_hash('development.yml')
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module KumoConfig
|
4
|
+
class FileLoader
|
5
|
+
def initialize(options)
|
6
|
+
@config_dir_path = options[:config_dir_path]
|
7
|
+
end
|
8
|
+
|
9
|
+
def load_hash(file_name, optional = true)
|
10
|
+
# reads a file presuming it's a yml in form of key: value, returning it as a hash
|
11
|
+
path = file_path(file_name)
|
12
|
+
|
13
|
+
begin
|
14
|
+
YAML::load(File.read(path))
|
15
|
+
rescue Errno::ENOENT => ex
|
16
|
+
# file not found, return empty dictionary if that is ok
|
17
|
+
return {} if optional
|
18
|
+
raise ex
|
19
|
+
rescue StandardError => ex
|
20
|
+
# this is an error we weren't expecting
|
21
|
+
raise ex
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_erb(file_name)
|
26
|
+
# loads a file, constructs an ERB object from it and returns the ERB object
|
27
|
+
# DOES NOT RENDER A RESULT!!
|
28
|
+
path = file_path(file_name)
|
29
|
+
ERB.new(File.read(path))
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def file_path(file_name)
|
35
|
+
File.join(@config_dir_path, file_name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/kumo_config.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "kumo_config/environment_config"
|
data/script/build.sh
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
bundle install
|
6
|
+
if [[ -z "$KUMO_CONFIG_VERSION" && -n "$BUILDKITE_BUILD_NUMBER" ]]; then
|
7
|
+
export KUMO_CONFIG_VERSION="$BUILDKITE_BUILD_NUMBER"
|
8
|
+
fi
|
9
|
+
|
10
|
+
echo "--- :wind_chime: Building gem :wind_chime:"
|
11
|
+
|
12
|
+
gem build kumo_config.gemspec
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
echo "--- :clock1: :clock2: running specs :clock3: :clock4:"
|
6
|
+
bundle install && bundle exec rspec --pattern "spec/integration/*_spec.rb"
|
7
|
+
|
8
|
+
function inline_image {
|
9
|
+
printf '\033]1338;url='"$1"';alt='"$2"'\a\n'
|
10
|
+
}
|
11
|
+
|
12
|
+
echo "+++ Done! :thumbsup: :shipit:"
|
13
|
+
inline_image "https://giftoppr.desktopprassets.com/uploads/f828c372186b5fa80a1c553adbcd4bc4d331396b/tumblr_m2cg550aq21ql201ao1_500.gif" "Yuss"
|
data/script/unit_test.sh
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
echo "--- :clock1: :clock2: running specs :clock3: :clock4:"
|
6
|
+
bundle install && bundle exec rspec --exclude-pattern "spec/integration/*_spec.rb"
|
7
|
+
|
8
|
+
function inline_image {
|
9
|
+
printf '\033]1338;url='"$1"';alt='"$2"'\a\n'
|
10
|
+
}
|
11
|
+
|
12
|
+
echo "+++ Done! :thumbsup: :shipit:"
|
13
|
+
inline_image "https://giftoppr.desktopprassets.com/uploads/f828c372186b5fa80a1c553adbcd4bc4d331396b/tumblr_m2cg550aq21ql201ao1_500.gif" "Yuss"
|
File without changes
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'KumoConfig' do
|
4
|
+
let(:config_path) { File.join(File.dirname(__FILE__), 'fixtures') }
|
5
|
+
let(:environment_name) { 'production' }
|
6
|
+
|
7
|
+
subject { KumoConfig::EnvironmentConfig.new(
|
8
|
+
env_name: environment_name,
|
9
|
+
config_path: config_path
|
10
|
+
) }
|
11
|
+
|
12
|
+
it 'resolves the configuration for the specified environment' do
|
13
|
+
expect(subject.config['production_key']).to eq('production_value')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'uses commmon configuration if nothing is specified in an environment or injected config' do
|
17
|
+
expect(subject.config['common_key']).to eq('common_value')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'gives the environment config precedence over common config' do
|
21
|
+
expect(subject.config['overridden_key']).to eq('production override value')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'resolves secret configuration into a Hash' do
|
25
|
+
expect(subject.plain_text_secrets['production_secret_key']).to eq('production_secret_value')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'uses commmon secret configuration if nothing is specified in the environment' do
|
29
|
+
expect(subject.plain_text_secrets['common_secret_key']).to eq('common_secret_value')
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'gives the environment secret precedence over common secrets' do
|
33
|
+
expect(subject.plain_text_secrets['overridden_secret']).to eq('production_override_secret')
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'when there is injected configuration' do
|
37
|
+
subject { KumoConfig::EnvironmentConfig.new(
|
38
|
+
env_name: environment_name,
|
39
|
+
config_path: config_path,
|
40
|
+
injected_config: {
|
41
|
+
'injected_key' => 'injected value',
|
42
|
+
'overridden_key' => 'injected override value'
|
43
|
+
}
|
44
|
+
) }
|
45
|
+
|
46
|
+
it 'resolves injected configuration items' do
|
47
|
+
expect(subject.config['injected_key']).to eq('injected value')
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'gives injected config precedence over config from files' do
|
51
|
+
expect(subject.config['overridden_key']).to eq('injected override value')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
context 'when an environment is specified for which we do not have configuration files' do
|
57
|
+
let(:environment_name) { 'nonsense' }
|
58
|
+
|
59
|
+
it 'uses development environment as a default' do
|
60
|
+
expect(subject.config['development_key']).to eq('development value')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'uses development secrets' do
|
64
|
+
expect(subject.plain_text_secrets['development_secret']).to eq('development_secret_value')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
File without changes
|
@@ -0,0 +1,11 @@
|
|
1
|
+
common_secret_key: >
|
2
|
+
[ENC,AQECAHiRAIBw6aG5PR5XWhLpFs0CtWZCoVenu5eErfT6U2j+PgAAAHEwbwYJ
|
3
|
+
KoZIhvcNAQcGoGIwYAIBADBbBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEE
|
4
|
+
DFIEC1gYWtlJQlCFnwIBEIAuKctvUp9pni1Ij1M5tbyBkAms4mcKedYIRzUO
|
5
|
+
FASJonP8HpTe11oL0gbQQFP5vA==
|
6
|
+
|
7
|
+
overridden_secret: >
|
8
|
+
[ENC,AQECAHiRAIBw6aG5PR5XWhLpFs0CtWZCoVenu5eErfT6U2j+PgAAAG4wbAYJ
|
9
|
+
KoZIhvcNAQcGoF8wXQIBADBYBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEE
|
10
|
+
DPBwsLbzuz9H4WL7twIBEIAre1We74YNLNu3xKLLvF3J5UJD8eH1+7IHn0II
|
11
|
+
Bne6CbUPITvPc2N1Hq9KYA==
|
@@ -0,0 +1 @@
|
|
1
|
+
development_key: development value
|
@@ -0,0 +1,11 @@
|
|
1
|
+
production_secret_key: >
|
2
|
+
[ENC,AQECAHiRAIBw6aG5PR5XWhLpFs0CtWZCoVenu5eErfT6U2j+PgAAAHUwcwYJ
|
3
|
+
KoZIhvcNAQcGoGYwZAIBADBfBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEE
|
4
|
+
DOYHUsqhXlV2Chl9AQIBEIAyPvjZe1pWhGOZ6RGP8srNB1oMC/4JwX+nYdjC
|
5
|
+
+i1U5PusiCrRERJdk3B0Cm4yIz3GexY=
|
6
|
+
|
7
|
+
overridden_secret: >
|
8
|
+
[ENC,AQECAHiRAIBw6aG5PR5XWhLpFs0CtWZCoVenu5eErfT6U2j+PgAAAHgwdgYJ
|
9
|
+
KoZIhvcNAQcGoGkwZwIBADBiBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEE
|
10
|
+
DEgzP81EmQgMXTuNFwIBEIA1lkR9cGUtdAOQjT3S19RKKL3w2KgIHWnrZh05
|
11
|
+
tM25nWeufrLe4wldF73/4zSXfEZqlxycfZ0=
|