Skip to content

Commit

Permalink
Add support for Cloud Foundry
Browse files Browse the repository at this point in the history
Signed-off-by: Gamaliel Amaudruz <[email protected]>
  • Loading branch information
Alan Yeo authored and Pair committed Dec 27, 2016
1 parent 5e67861 commit 137094e
Show file tree
Hide file tree
Showing 16 changed files with 348 additions and 21 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,16 @@ 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 adding to your CF manifest.yml the defined ENV variables under the `env` section of specified app in the yaml file.
You must specify the app name and optionally the name of your CF manifest file:

bundle exec rake config:cf[app_name, cf_manifest.yml]

The result of this command will have the manifest file name suffixed with the environment you ran the task in. You can then push your app with the generated manifest.


### Fine-tuning

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

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

def invoke
manifest_path = file_path || 'manifest.yml'
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(app_name, manifest_hash).add_to_env

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

puts "File #{target_manifest_path} generated."
end

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

module Config
class CFManifestMerger
include Integrations::Helpers

def initialize(app_name, manifest_hash)
@app_name = app_name
@manifest_hash = manifest_hash
raise ArgumentError.new("Manifest path & app name must be specified") unless @app_name && @manifest_hash
end

def add_to_env

settings_hash = Config.const_get(Config.const_name).to_hash.stringify_keys

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

app_hash = @manifest_hash['applications'].detect { |hash| hash['name'] == @app_name }

raise ArgumentError, "Application '#{@app_name}' is not specified in your manifest" if app_hash.nil?

check_conflicting_keys(app_hash['env'], settings_hash)

app_hash['env'].merge!(prefix_keys_with_const_name_hash)

@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
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
10 changes: 10 additions & 0 deletions lib/config/tasks/cloud_foundry.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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', [:app_name, :file_path] => :environment do |_, args|
Config::Integrations::CloudFoundry.new(args[:app_name], args[:file_path]).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
2 changes: 2 additions & 0 deletions spec/fixtures/cf/cf_conflict.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEFAULT_HOST: host
DEFAULT_PORT: port
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
13 changes: 13 additions & 0 deletions spec/fixtures/cf/cf_multilevel.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'
69 changes: 69 additions & 0 deletions spec/integrations/helpers/cf_manifest_merger_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require 'spec_helper'
require_relative '../../../lib/config/integrations/helpers/cf_manifest_merger'

describe Config::CFManifestMerger do

after do
Settings.reload_from_files("#{fixture_path}/settings.yml")
end

it 'raises an argument error if you do not specify an app name' do
expect {
Config::CFManifestMerger.new(nil, load_manifest('cf_manifest.yml'))
}.to raise_error(ArgumentError, 'Manifest path & app name must be specified')
end

it 'raises an argument error if the application name is not found in the manifest' do
expect {
Config::CFManifestMerger.new('undefined', load_manifest('cf_manifest.yml')).add_to_env
}.to raise_error(ArgumentError, "Application 'undefined' is not specified in your manifest")
end

it 'returns the cf manifest template if no settings available' do
merger = Config::CFManifestMerger.new('app_name', load_manifest('cf_manifest.yml'))
Config.load_and_set_settings ''

resulting_hash = merger.add_to_env
expect(resulting_hash).to eq(load_manifest('cf_manifest.yml'))
end

it 'merges the given YAML file with the cf manifest YAML file' do
merger = Config::CFManifestMerger.new('some-cf-app', load_manifest('cf_manifest.yml'))
Config.load_and_set_settings "#{fixture_path}/cf/cf_multilevel.yml"

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"}}
]
})
end

it 'raises an exception if there is conflicting keys' do
merger = Config::CFManifestMerger.new('some-cf-app', load_manifest('cf_manifest.yml'))
Config.load_and_set_settings "#{fixture_path}/cf/cf_conflict.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

0 comments on commit 137094e

Please sign in to comment.