kumo_config 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kumo_config.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # KumoConfig [![Build status](https://badge.buildkite.com/1d42699430881063b95a0082c8f23f0acc6d5bad5909716720.svg)](https://buildkite.com/redbubble/kumo-config-gem) [![Code Climate](https://codeclimate.com/github/redbubble/kumo_keisei_config/badges/gpa.svg)](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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -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
@@ -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"
@@ -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,2 @@
1
+ common_key: common_value
2
+ overridden_key: original value
@@ -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,5 @@
1
+ development_secret: >
2
+ [ENC,AQECAHiRAIBw6aG5PR5XWhLpFs0CtWZCoVenu5eErfT6U2j+PgAAAHYwdAYJ
3
+ KoZIhvcNAQcGoGcwZQIBADBgBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEE
4
+ DMcUwkdxmGkeBjJf8AIBEIAztkU80u2FKQ1o+72j3vQE2Haxv0L5tdrG0nBr
5
+ reDdZ/miq66KN92ijTTPWwrrrJH6GGkz
@@ -0,0 +1,2 @@
1
+ production_key: production_value
2
+ overridden_key: production override 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=