Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 247119b

Browse files
gamovPair
authored and
Pair
committedJan 13, 2017
Add support for Cloud Foundry
Signed-off-by: Natalie Tay <ntay@pivotal.io> Signed-off-by: Alan Yeo <ayeo@pivotal.io> Signed-off-by: Benjamin Tan <btan@pivotal.io>
1 parent 5e67861 commit 247119b

16 files changed

+383
-21
lines changed
 

‎README.md

+15
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,21 @@ filesystem gets recreated from the git sources on each instance refresh. To use
333333

334334
To upload your local values to Heroku you could ran `bundle exec rake config:heroku`.
335335

336+
### Working with Cloud Foundry
337+
338+
Cloud Foundry integration will generate a manifest from your CF manifest with the defined ENV variables added
339+
under the `env` section. **ENV variables will be added to all applications specified in the manifest.** By default,
340+
it uses `manifest.yml` and the current `Rails.env`:
341+
342+
bundle exec rake config:cf
343+
344+
You may optionally pass target environment _and_ the name of your CF manifest file (in that case, both are compulsory):
345+
346+
bundle exec rake config:cf[target_env, your_manifest.yml]
347+
348+
The result of this command will create a new manifest file, name suffixed with '-merged'. You can then push your app
349+
with the generated manifest.
350+
336351
### Fine-tuning
337352

338353
You can customize how environment variables are processed:
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
require 'bundler'
2+
require 'yaml'
3+
require_relative '../../../lib/config/integrations/helpers/cf_manifest_merger'
4+
5+
module Config
6+
module Integrations
7+
class CloudFoundry < Struct.new(:target_env, :file_path)
8+
9+
def invoke
10+
11+
manifest_path = file_path
12+
file_name, _ext = manifest_path.split('.yml')
13+
14+
manifest_hash = YAML.load(IO.read(File.join(::Rails.root, manifest_path)))
15+
16+
puts "Generating manifest... (base cf manifest: #{manifest_path})"
17+
18+
merged_hash = Config::CFManifestMerger.new(target_env, manifest_hash).add_to_env
19+
20+
target_manifest_path = File.join(::Rails.root, "#{file_name}-merged.yml")
21+
IO.write(target_manifest_path, merged_hash.to_yaml)
22+
23+
puts "File #{target_manifest_path} generated."
24+
end
25+
26+
end
27+
end
28+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
require_relative 'helpers'
2+
3+
module Config
4+
class CFManifestMerger
5+
include Integrations::Helpers
6+
7+
def initialize(target_env, manifest_hash)
8+
@manifest_hash = manifest_hash.dup
9+
10+
raise ArgumentError.new('Target environment & manifest path must be specified') unless target_env && @manifest_hash
11+
12+
config_root = File.join(Rails.root, 'config')
13+
config_setting_files = Config.setting_files(config_root, target_env)
14+
@settings_hash = Config.load_files(config_setting_files).to_hash.stringify_keys
15+
end
16+
17+
def add_to_env
18+
19+
prefix_keys_with_const_name_hash = to_dotted_hash(@settings_hash, namespace: Config.const_name)
20+
21+
apps = @manifest_hash['applications']
22+
23+
apps.each do |app|
24+
check_conflicting_keys(app['env'], @settings_hash)
25+
app['env'].merge!(prefix_keys_with_const_name_hash)
26+
end
27+
28+
@manifest_hash
29+
end
30+
31+
private
32+
33+
def check_conflicting_keys(env_hash, settings_hash)
34+
conflicting_keys = env_hash.keys & settings_hash.keys
35+
raise ArgumentError.new("Conflicting keys: #{conflicting_keys.join(', ')}") if conflicting_keys.any?
36+
end
37+
38+
end
39+
end
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Config::Integrations::Helpers
2+
3+
def to_dotted_hash(source, target: {}, namespace: nil)
4+
raise ArgumentError, "target must be a hash (given: #{target.class.name})" unless target.kind_of? Hash
5+
prefix = "#{namespace}." if namespace
6+
case source
7+
when Hash
8+
source.each do |key, value|
9+
to_dotted_hash(value, target: target, namespace: "#{prefix}#{key}")
10+
end
11+
when Array
12+
source.each_with_index do |value, index|
13+
to_dotted_hash(value, target: target, namespace: "#{prefix}#{index}")
14+
end
15+
else
16+
target[namespace] = source
17+
end
18+
target
19+
end
20+
21+
end

‎lib/config/integrations/heroku.rb

+7-20
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
require 'bundler'
2+
require_relative 'helpers/helpers'
23

34
module Config
45
module Integrations
56
class Heroku < Struct.new(:app)
7+
include Integrations::Helpers
8+
69
def invoke
710
puts 'Setting vars...'
811
heroku_command = "config:set #{vars}"
@@ -14,13 +17,13 @@ def invoke
1417
def vars
1518
# Load only local options to Heroku
1619
Config.load_and_set_settings(
17-
Rails.root.join("config", "settings.local.yml").to_s,
18-
Rails.root.join("config", "settings", "#{environment}.local.yml").to_s,
19-
Rails.root.join("config", "environments", "#{environment}.local.yml").to_s
20+
::Rails.root.join("config", "settings.local.yml").to_s,
21+
::Rails.root.join("config", "settings", "#{environment}.local.yml").to_s,
22+
::Rails.root.join("config", "environments", "#{environment}.local.yml").to_s
2023
)
2124

2225
out = ''
23-
dotted_hash = to_dotted_hash Kernel.const_get(Config.const_name).to_hash, {}, Config.const_name
26+
dotted_hash = to_dotted_hash Kernel.const_get(Config.const_name).to_hash, namespace: Config.const_name
2427
dotted_hash.each {|key, value| out += " #{key}=#{value} "}
2528
out
2629
end
@@ -38,22 +41,6 @@ def `(command)
3841
Bundler.with_clean_env { super }
3942
end
4043

41-
def to_dotted_hash(source, target = {}, namespace = nil)
42-
prefix = "#{namespace}." if namespace
43-
case source
44-
when Hash
45-
source.each do |key, value|
46-
to_dotted_hash(value, target, "#{prefix}#{key}")
47-
end
48-
when Array
49-
source.each_with_index do |value, index|
50-
to_dotted_hash(value, target, "#{prefix}#{index}")
51-
end
52-
else
53-
target[namespace] = source
54-
end
55-
target
56-
end
5744
end
5845
end
5946
end

‎lib/config/integrations/rails/railtie.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def preload
1515

1616
# Load rake tasks (eg. Heroku)
1717
rake_tasks do
18-
Dir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each { |f| load f }
18+
Dir[File.join(File.dirname(__FILE__), '../../tasks/*.rake')].each { |f| load f }
1919
end
2020

2121
config.before_configuration { preload }

‎lib/config/tasks/cloud_foundry.rake

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require 'config/integrations/cloud_foundry'
2+
3+
namespace 'config' do
4+
5+
desc 'Create a cf manifest with the env variables defined by config under current environment'
6+
task 'cf', [:target_env, :file_path] => [:environment] do |_, args|
7+
8+
raise ArgumentError, 'Both target_env and file_path arguments must be specified' if args.length == 1
9+
10+
default_args = {:target_env => Rails.env, :file_path => 'manifest.yml'}
11+
merged_args = default_args.merge(args)
12+
13+
Config::Integrations::CloudFoundry.new(*merged_args.values).invoke
14+
end
15+
16+
end

‎lib/config/tasks/heroku.rake

+3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
require 'config/integrations/heroku'
22

33
namespace 'config' do
4+
5+
desc 'Upload to Heroku all env variables defined by config under current environment'
46
task :heroku, [:app] => :environment do |_, args|
57
Config::Integrations::Heroku.new(args[:app]).invoke
68
end
9+
710
end

‎spec/fixtures/cf/cf_manifest.yml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
applications:
2+
- name: some-cf-app
3+
instances: 1
4+
env:
5+
DEFAULT_HOST: host
6+
DEFAULT_PORT: port
7+
FOO: BAR
8+
9+
- name: app_name
10+
env:
11+
DEFAULT_HOST: host
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DEFAULT_HOST: host
2+
DEFAULT_PORT: port
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
world:
2+
capitals:
3+
europe:
4+
germany: 'Berlin'
5+
poland: 'Warsaw'
6+
array:
7+
- name: 'Alan'
8+
- name: 'Gam'
9+
array_with_index:
10+
0:
11+
name: 'Bob'
12+
1:
13+
name: 'William'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
require 'spec_helper'
2+
require_relative '../../../lib/config/integrations/helpers/cf_manifest_merger'
3+
4+
describe Config::CFManifestMerger do
5+
6+
let(:mocked_rails_root_path) { "#{fixture_path}/cf/" }
7+
let(:manifest_hash) { load_manifest('cf_manifest.yml') }
8+
9+
it 'raises an argument error if you do not specify a target environment' do
10+
expect {
11+
Config::CFManifestMerger.new(nil, manifest_hash)
12+
}.to raise_error(ArgumentError, 'Target environment & manifest path must be specified')
13+
end
14+
15+
it 'returns the cf manifest unmodified if no settings are available' do
16+
merger = Config::CFManifestMerger.new('test', manifest_hash)
17+
18+
resulting_hash = merger.add_to_env
19+
expect(resulting_hash).to eq(manifest_hash)
20+
end
21+
22+
it 'adds the settings for the target_env to the manifest_hash' do
23+
allow(Rails).to receive(:root).and_return(mocked_rails_root_path)
24+
25+
# we use the target_env to load the proper settings file
26+
merger = Config::CFManifestMerger.new('multilevel_settings', manifest_hash)
27+
28+
resulting_hash = merger.add_to_env
29+
expect(resulting_hash).to eq({
30+
'applications' => [
31+
{
32+
'name' => 'some-cf-app',
33+
'instances' => 1,
34+
'env' => {
35+
'DEFAULT_HOST' => 'host',
36+
'DEFAULT_PORT' => 'port',
37+
'FOO' => 'BAR',
38+
'Settings.world.capitals.europe.germany' => 'Berlin',
39+
'Settings.world.capitals.europe.poland' => 'Warsaw',
40+
'Settings.world.array.0.name' => 'Alan',
41+
'Settings.world.array.1.name' => 'Gam',
42+
'Settings.world.array_with_index.0.name' => 'Bob',
43+
'Settings.world.array_with_index.1.name' => 'William'
44+
}
45+
},
46+
{
47+
'name' => 'app_name',
48+
'env' => {
49+
'DEFAULT_HOST' => 'host',
50+
'Settings.world.capitals.europe.germany' => 'Berlin',
51+
'Settings.world.capitals.europe.poland' => 'Warsaw',
52+
'Settings.world.array.0.name' => 'Alan',
53+
'Settings.world.array.1.name' => 'Gam',
54+
'Settings.world.array_with_index.0.name' => 'Bob',
55+
'Settings.world.array_with_index.1.name' => 'William'
56+
}
57+
}
58+
]
59+
})
60+
end
61+
62+
it 'raises an exception if there is conflicting keys' do
63+
allow(Rails).to receive(:root).and_return(mocked_rails_root_path)
64+
65+
merger = Config::CFManifestMerger.new('conflict_settings', manifest_hash)
66+
67+
# Config.load_and_set_settings "#{fixture_path}/cf/conflict_settings.yml"
68+
expect {
69+
merger.add_to_env
70+
}.to raise_error(ArgumentError, 'Conflicting keys: DEFAULT_HOST, DEFAULT_PORT')
71+
end
72+
73+
def load_manifest filename
74+
YAML.load(IO.read("#{fixture_path}/cf/#{filename}"))
75+
end
76+
end
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
require 'spec_helper'
2+
require_relative '../../../lib/config/integrations/helpers/helpers'
3+
4+
describe 'Helpers' do
5+
6+
subject { Class.new.send(:include, Config::Integrations::Helpers).new }
7+
8+
describe '#to_dotted_hash' do
9+
10+
context 'only the source is specified' do
11+
12+
it 'returns a hash with a nil key (default)' do
13+
expect(subject.to_dotted_hash 3).to eq({nil => 3})
14+
end
15+
end
16+
17+
context 'with invalid arguments' do
18+
it 'raises an error' do
19+
expect { subject.to_dotted_hash(3, target: [1, 2, 7], namespace: 2) }
20+
.to raise_error(ArgumentError, 'target must be a hash (given: Array)')
21+
end
22+
end
23+
24+
context 'all arguments specified' do
25+
26+
it 'returns a hash with the namespace as the key' do
27+
expect(subject.to_dotted_hash(3, namespace: 'ns')).to eq({'ns' => 3})
28+
end
29+
30+
it 'returns a new hash with a dotted string key prefixed with namespace' do
31+
expect(subject.to_dotted_hash({hello: {cruel: 'world'}}, namespace: 'ns'))
32+
.to eq({'ns.hello.cruel' => 'world'})
33+
end
34+
35+
it 'returns the same hash as passed as a parameter' do
36+
target = {something: 'inside'}
37+
target_id = target.object_id
38+
result = subject.to_dotted_hash(2, target: target, namespace: 'ns')
39+
expect(result).to eq({:something => 'inside', 'ns' => 2})
40+
expect(result.object_id).to eq target_id
41+
end
42+
43+
it 'returns a hash when given a source with mixed nested types (hashes & arrays)' do
44+
expect(subject.to_dotted_hash(
45+
{hello: {evil: [:cruel, 'world', and: {dark: 'universe'}]}}, namespace: 'ns'))
46+
.to eq(
47+
{"ns.hello.evil.0" => :cruel,
48+
"ns.hello.evil.1" => "world",
49+
"ns.hello.evil.2.and.dark" => "universe"}
50+
)
51+
end
52+
end
53+
end
54+
end

‎spec/tasks/cloud_foundry_spec.rb

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
require 'spec_helper'
2+
3+
describe 'config:cf' do
4+
include_context 'rake'
5+
let(:test_settings_file) { "#{fixture_path}/cf/config/settings/multilevel_settings.yml" }
6+
let(:test_manifest_file) { "#{fixture_path}/cf/cf_manifest.yml" }
7+
let(:development_settings_file) { "#{fixture_path}/development.yml" }
8+
let(:settings_dir) { Rails.root.join('config', 'settings') }
9+
10+
def setup_temp_rails_root
11+
allow(Rails).to receive(:root).and_return(Pathname.new Dir.mktmpdir)
12+
end
13+
14+
before :all do
15+
load File.expand_path('../../../lib/config/tasks/cloud_foundry.rake', __FILE__)
16+
Rake::Task.define_task(:environment)
17+
end
18+
19+
before { allow($stdout).to receive(:puts) } # suppressing console output during testing
20+
21+
after :all do
22+
Settings.reload_from_files("#{fixture_path}/settings.yml")
23+
end
24+
25+
it 'raises an error if the manifest file is missing' do
26+
expect {
27+
Rake::Task['config:cf'].execute
28+
}.to raise_error(SystemCallError)
29+
end
30+
31+
it 'raises an error if the settings file is missing' do
32+
expect {
33+
Rake::Task['config:cf'].execute({target_env: 'not_existing_env', file_path: 'manifest.yml'})
34+
}.to raise_error(SystemCallError)
35+
end
36+
37+
describe 'without arguments' do
38+
it 'creates the merged manifest file with the settings ENV variables included for all cf applications' do
39+
setup_temp_rails_root
40+
settings_dir.mkpath
41+
FileUtils.cp(test_settings_file, settings_dir.join('test.yml'))
42+
FileUtils.cp(test_manifest_file, Rails.root.join('manifest.yml'))
43+
44+
Rake::Task['config:cf'].execute
45+
46+
merged_manifest_file = File.join(Rails.root, 'manifest-merged.yml')
47+
merged_manifest_file_contents = YAML.load(IO.read(merged_manifest_file))
48+
49+
expect(merged_manifest_file_contents['applications'][0]['name']).to eq 'some-cf-app'
50+
expect(merged_manifest_file_contents['applications'][0]['env']['DEFAULT_HOST']).to eq 'host'
51+
expect(merged_manifest_file_contents['applications'][0]['env']['Settings.world.array.0.name']).to eq 'Alan'
52+
53+
expect(merged_manifest_file_contents['applications'][1]['name']).to eq 'app_name'
54+
expect(merged_manifest_file_contents['applications'][1]['env']['Settings.world.array.0.name']).to eq 'Alan'
55+
end
56+
end
57+
58+
describe 'with arguments' do
59+
it 'raises an error if only one argument is provided' do
60+
expect {
61+
Rake::Task['config:cf'].execute({target_env:'target_env_name'})
62+
}.to raise_error(ArgumentError)
63+
end
64+
65+
it 'takes in account the provided arguments' do
66+
setup_temp_rails_root
67+
settings_dir.mkpath
68+
FileUtils.cp(test_manifest_file, Rails.root.join('cf_manifest.yml'))
69+
FileUtils.cp(development_settings_file, settings_dir.join('development.yml'))
70+
FileUtils.cp(test_settings_file, settings_dir.join('test.yml'))
71+
72+
Rake::Task['config:cf'].execute({target_env: 'development', file_path: 'cf_manifest.yml'})
73+
74+
merged_manifest_file = File.join(Rails.root, 'cf_manifest-merged.yml')
75+
merged_manifest_file_contents = YAML.load(IO.read(merged_manifest_file))
76+
77+
expect(merged_manifest_file_contents['applications'][0]['env']['Settings.size']).to eq 2
78+
end
79+
end
80+
end

‎spec/tasks/db_spec.rb

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
describe 'db:create' do
44
include_context 'rake'
55

6+
before { allow($stdout).to receive(:puts) } # suppressing console output during testing
7+
68
it 'has access to Settings object and can read databases from settings.yml file' do
79
Rake::Task['db:create'].invoke
810
end

‎spec/tasks/heroku_spec.rb

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
require 'spec_helper'
2+
3+
describe 'config:heroku' do
4+
include_context 'rake'
5+
6+
before do
7+
load File.expand_path("../../../lib/config/tasks/heroku.rake", __FILE__)
8+
Rake::Task.define_task(:environment)
9+
end
10+
11+
it 'includes the helper module that defines to_dotted_hash' do
12+
h = Config::Integrations::Heroku.new
13+
expect(h.public_methods(:true)).to include(:to_dotted_hash)
14+
end
15+
end

0 commit comments

Comments
 (0)
Please sign in to comment.