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.
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=