Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Cloud Foundry (cf) #161

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,21 @@ filesystem gets recreated from the git sources on each instance refresh. To use

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

### Working with Cloud Foundry

Cloud Foundry integration will generate a manifest from your CF manifest with the defined ENV variables added
under the `env` section. **ENV variables will be added to all applications specified in the manifest.** By default,
it uses `manifest.yml` and the current `Rails.env`:

bundle exec rake config:cf

You may optionally pass target environment _and_ the name of your CF manifest file (in that case, both are compulsory):

bundle exec rake config:cf[target_env, your_manifest.yml]

The result of this command will create a new manifest file, name suffixed with '-merged'. You can then push your app
with the generated manifest.

### Fine-tuning

You can customize how environment variables are processed:
Expand Down
28 changes: 28 additions & 0 deletions lib/config/integrations/cloud_foundry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'bundler'
require 'yaml'
require_relative '../../../lib/config/integrations/helpers/cf_manifest_merger'

module Config
module Integrations
class CloudFoundry < Struct.new(:target_env, :file_path)

def invoke

manifest_path = file_path
file_name, _ext = manifest_path.split('.yml')

manifest_hash = YAML.load(IO.read(File.join(::Rails.root, manifest_path)))

puts "Generating manifest... (base cf manifest: #{manifest_path})"

merged_hash = Config::CFManifestMerger.new(target_env, manifest_hash).add_to_env

target_manifest_path = File.join(::Rails.root, "#{file_name}-merged.yml")
IO.write(target_manifest_path, merged_hash.to_yaml)

puts "File #{target_manifest_path} generated."
end

end
end
end
39 changes: 39 additions & 0 deletions lib/config/integrations/helpers/cf_manifest_merger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require_relative 'helpers'

module Config
class CFManifestMerger
include Integrations::Helpers

def initialize(target_env, manifest_hash)
@manifest_hash = manifest_hash.dup

raise ArgumentError.new('Target environment & manifest path must be specified') unless target_env && @manifest_hash

config_root = File.join(Rails.root, 'config')
config_setting_files = Config.setting_files(config_root, target_env)
@settings_hash = Config.load_files(config_setting_files).to_hash.stringify_keys
end

def add_to_env

prefix_keys_with_const_name_hash = to_dotted_hash(@settings_hash, namespace: Config.const_name)

apps = @manifest_hash['applications']

apps.each do |app|
check_conflicting_keys(app['env'], @settings_hash)
app['env'].merge!(prefix_keys_with_const_name_hash)
end

@manifest_hash
end

private

def check_conflicting_keys(env_hash, settings_hash)
conflicting_keys = env_hash.keys & settings_hash.keys
raise ArgumentError.new("Conflicting keys: #{conflicting_keys.join(', ')}") if conflicting_keys.any?
end

end
end
21 changes: 21 additions & 0 deletions lib/config/integrations/helpers/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Config::Integrations::Helpers

def to_dotted_hash(source, target: {}, namespace: nil)
raise ArgumentError, "target must be a hash (given: #{target.class.name})" unless target.kind_of? Hash
prefix = "#{namespace}." if namespace
case source
when Hash
source.each do |key, value|
to_dotted_hash(value, target: target, namespace: "#{prefix}#{key}")
end
when Array
source.each_with_index do |value, index|
to_dotted_hash(value, target: target, namespace: "#{prefix}#{index}")
end
else
target[namespace] = source
end
target
end

end
27 changes: 7 additions & 20 deletions lib/config/integrations/heroku.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
require 'bundler'
require_relative 'helpers/helpers'

module Config
module Integrations
class Heroku < Struct.new(:app)
include Integrations::Helpers

def invoke
puts 'Setting vars...'
heroku_command = "config:set #{vars}"
Expand All @@ -14,13 +17,13 @@ def invoke
def vars
# Load only local options to Heroku
Config.load_and_set_settings(
Rails.root.join("config", "settings.local.yml").to_s,
Rails.root.join("config", "settings", "#{environment}.local.yml").to_s,
Rails.root.join("config", "environments", "#{environment}.local.yml").to_s
::Rails.root.join("config", "settings.local.yml").to_s,
::Rails.root.join("config", "settings", "#{environment}.local.yml").to_s,
::Rails.root.join("config", "environments", "#{environment}.local.yml").to_s
)

out = ''
dotted_hash = to_dotted_hash Kernel.const_get(Config.const_name).to_hash, {}, Config.const_name
dotted_hash = to_dotted_hash Kernel.const_get(Config.const_name).to_hash, namespace: Config.const_name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this now needs to be Object.const_get

dotted_hash.each {|key, value| out += " #{key}=#{value} "}
out
end
Expand All @@ -38,22 +41,6 @@ def `(command)
Bundler.with_clean_env { super }
end

def to_dotted_hash(source, target = {}, namespace = nil)
prefix = "#{namespace}." if namespace
case source
when Hash
source.each do |key, value|
to_dotted_hash(value, target, "#{prefix}#{key}")
end
when Array
source.each_with_index do |value, index|
to_dotted_hash(value, target, "#{prefix}#{index}")
end
else
target[namespace] = source
end
target
end
end
end
end
2 changes: 1 addition & 1 deletion lib/config/integrations/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def preload

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

config.before_configuration { preload }
Expand Down
16 changes: 16 additions & 0 deletions lib/config/tasks/cloud_foundry.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'config/integrations/cloud_foundry'

namespace 'config' do

desc 'Create a cf manifest with the env variables defined by config under current environment'
task 'cf', [:target_env, :file_path] => [:environment] do |_, args|

raise ArgumentError, 'Both target_env and file_path arguments must be specified' if args.length == 1

default_args = {:target_env => Rails.env, :file_path => 'manifest.yml'}
merged_args = default_args.merge(args)

Config::Integrations::CloudFoundry.new(*merged_args.values).invoke
end

end
3 changes: 3 additions & 0 deletions lib/config/tasks/heroku.rake
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
require 'config/integrations/heroku'

namespace 'config' do

desc 'Upload to Heroku all env variables defined by config under current environment'
task :heroku, [:app] => :environment do |_, args|
Config::Integrations::Heroku.new(args[:app]).invoke
end

end
11 changes: 11 additions & 0 deletions spec/fixtures/cf/cf_manifest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
applications:
- name: some-cf-app
instances: 1
env:
DEFAULT_HOST: host
DEFAULT_PORT: port
FOO: BAR

- name: app_name
env:
DEFAULT_HOST: host
2 changes: 2 additions & 0 deletions spec/fixtures/cf/config/settings/conflict_settings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEFAULT_HOST: host
DEFAULT_PORT: port
13 changes: 13 additions & 0 deletions spec/fixtures/cf/config/settings/multilevel_settings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
world:
capitals:
europe:
germany: 'Berlin'
poland: 'Warsaw'
array:
- name: 'Alan'
- name: 'Gam'
array_with_index:
0:
name: 'Bob'
1:
name: 'William'
76 changes: 76 additions & 0 deletions spec/integrations/helpers/cf_manifest_merger_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require 'spec_helper'
require_relative '../../../lib/config/integrations/helpers/cf_manifest_merger'

describe Config::CFManifestMerger do

let(:mocked_rails_root_path) { "#{fixture_path}/cf/" }
let(:manifest_hash) { load_manifest('cf_manifest.yml') }

it 'raises an argument error if you do not specify a target environment' do
expect {
Config::CFManifestMerger.new(nil, manifest_hash)
}.to raise_error(ArgumentError, 'Target environment & manifest path must be specified')
end

it 'returns the cf manifest unmodified if no settings are available' do
merger = Config::CFManifestMerger.new('test', manifest_hash)

resulting_hash = merger.add_to_env
expect(resulting_hash).to eq(manifest_hash)
end

it 'adds the settings for the target_env to the manifest_hash' do
allow(Rails).to receive(:root).and_return(mocked_rails_root_path)

# we use the target_env to load the proper settings file
merger = Config::CFManifestMerger.new('multilevel_settings', manifest_hash)

resulting_hash = merger.add_to_env
expect(resulting_hash).to eq({
'applications' => [
{
'name' => 'some-cf-app',
'instances' => 1,
'env' => {
'DEFAULT_HOST' => 'host',
'DEFAULT_PORT' => 'port',
'FOO' => 'BAR',
'Settings.world.capitals.europe.germany' => 'Berlin',
'Settings.world.capitals.europe.poland' => 'Warsaw',
'Settings.world.array.0.name' => 'Alan',
'Settings.world.array.1.name' => 'Gam',
'Settings.world.array_with_index.0.name' => 'Bob',
'Settings.world.array_with_index.1.name' => 'William'
}
},
{
'name' => 'app_name',
'env' => {
'DEFAULT_HOST' => 'host',
'Settings.world.capitals.europe.germany' => 'Berlin',
'Settings.world.capitals.europe.poland' => 'Warsaw',
'Settings.world.array.0.name' => 'Alan',
'Settings.world.array.1.name' => 'Gam',
'Settings.world.array_with_index.0.name' => 'Bob',
'Settings.world.array_with_index.1.name' => 'William'
}
}
]
})
end

it 'raises an exception if there is conflicting keys' do
allow(Rails).to receive(:root).and_return(mocked_rails_root_path)

merger = Config::CFManifestMerger.new('conflict_settings', manifest_hash)

# Config.load_and_set_settings "#{fixture_path}/cf/conflict_settings.yml"
expect {
merger.add_to_env
}.to raise_error(ArgumentError, 'Conflicting keys: DEFAULT_HOST, DEFAULT_PORT')
end

def load_manifest filename
YAML.load(IO.read("#{fixture_path}/cf/#{filename}"))
end
end
54 changes: 54 additions & 0 deletions spec/integrations/helpers/helpers_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require 'spec_helper'
require_relative '../../../lib/config/integrations/helpers/helpers'

describe 'Helpers' do

subject { Class.new.send(:include, Config::Integrations::Helpers).new }

describe '#to_dotted_hash' do

context 'only the source is specified' do

it 'returns a hash with a nil key (default)' do
expect(subject.to_dotted_hash 3).to eq({nil => 3})
end
end

context 'with invalid arguments' do
it 'raises an error' do
expect { subject.to_dotted_hash(3, target: [1, 2, 7], namespace: 2) }
.to raise_error(ArgumentError, 'target must be a hash (given: Array)')
end
end

context 'all arguments specified' do

it 'returns a hash with the namespace as the key' do
expect(subject.to_dotted_hash(3, namespace: 'ns')).to eq({'ns' => 3})
end

it 'returns a new hash with a dotted string key prefixed with namespace' do
expect(subject.to_dotted_hash({hello: {cruel: 'world'}}, namespace: 'ns'))
.to eq({'ns.hello.cruel' => 'world'})
end

it 'returns the same hash as passed as a parameter' do
target = {something: 'inside'}
target_id = target.object_id
result = subject.to_dotted_hash(2, target: target, namespace: 'ns')
expect(result).to eq({:something => 'inside', 'ns' => 2})
expect(result.object_id).to eq target_id
end

it 'returns a hash when given a source with mixed nested types (hashes & arrays)' do
expect(subject.to_dotted_hash(
{hello: {evil: [:cruel, 'world', and: {dark: 'universe'}]}}, namespace: 'ns'))
.to eq(
{"ns.hello.evil.0" => :cruel,
"ns.hello.evil.1" => "world",
"ns.hello.evil.2.and.dark" => "universe"}
)
end
end
end
end
Loading