Skip to content

Commit ca0391f

Browse files
gamovPair
authored and
Pair
committed
Add support for Cloud Foundry
Signed-off-by: Natalie Tay <[email protected]> Signed-off-by: Alan Yeo <[email protected]> Signed-off-by: Benjamin Tan <[email protected]>
1 parent 5e67861 commit ca0391f

16 files changed

+385
-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,38 @@
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_setting_files = Config.setting_files(Rails.root, target_env)
13+
@settings_hash = Config.load_files(config_setting_files).to_hash.stringify_keys
14+
end
15+
16+
def add_to_env
17+
18+
prefix_keys_with_const_name_hash = to_dotted_hash(@settings_hash, namespace: Config.const_name)
19+
20+
apps = @manifest_hash['applications']
21+
22+
apps.each do |app|
23+
check_conflicting_keys(app['env'], @settings_hash)
24+
app['env'].merge!(prefix_keys_with_const_name_hash)
25+
end
26+
27+
@manifest_hash
28+
end
29+
30+
private
31+
32+
def check_conflicting_keys(env_hash, settings_hash)
33+
conflicting_keys = env_hash.keys & settings_hash.keys
34+
raise ArgumentError.new("Conflicting keys: #{conflicting_keys.join(', ')}") if conflicting_keys.any?
35+
end
36+
37+
end
38+
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,75 @@
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+
merger = Config::CFManifestMerger.new('multilevel_settings', manifest_hash)
26+
27+
resulting_hash = merger.add_to_env
28+
expect(resulting_hash).to eq({
29+
'applications' => [
30+
{
31+
'name' => 'some-cf-app',
32+
'instances' => 1,
33+
'env' => {
34+
'DEFAULT_HOST' => 'host',
35+
'DEFAULT_PORT' => 'port',
36+
'FOO' => 'BAR',
37+
'Settings.world.capitals.europe.germany' => 'Berlin',
38+
'Settings.world.capitals.europe.poland' => 'Warsaw',
39+
'Settings.world.array.0.name' => 'Alan',
40+
'Settings.world.array.1.name' => 'Gam',
41+
'Settings.world.array_with_index.0.name' => 'Bob',
42+
'Settings.world.array_with_index.1.name' => 'William'
43+
}
44+
},
45+
{
46+
'name' => 'app_name',
47+
'env' => {
48+
'DEFAULT_HOST' => 'host',
49+
'Settings.world.capitals.europe.germany' => 'Berlin',
50+
'Settings.world.capitals.europe.poland' => 'Warsaw',
51+
'Settings.world.array.0.name' => 'Alan',
52+
'Settings.world.array.1.name' => 'Gam',
53+
'Settings.world.array_with_index.0.name' => 'Bob',
54+
'Settings.world.array_with_index.1.name' => 'William'
55+
}
56+
}
57+
]
58+
})
59+
end
60+
61+
it 'raises an exception if there is conflicting keys' do
62+
allow(Rails).to receive(:root).and_return(mocked_rails_root_path)
63+
64+
merger = Config::CFManifestMerger.new('conflict_settings', manifest_hash)
65+
66+
# Config.load_and_set_settings "#{fixture_path}/cf/conflict_settings.yml"
67+
expect {
68+
merger.add_to_env
69+
}.to raise_error(ArgumentError, 'Conflicting keys: DEFAULT_HOST, DEFAULT_PORT')
70+
end
71+
72+
def load_manifest filename
73+
YAML.load(IO.read("#{fixture_path}/cf/#{filename}"))
74+
end
75+
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

0 commit comments

Comments
 (0)