cf_deployer 1.2.8
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.
- checksums.yaml +7 -0
- data/.gitignore +29 -0
- data/ChangeLog.md +16 -0
- data/DETAILS.md +268 -0
- data/FAQ.md +61 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +51 -0
- data/LICENSE +22 -0
- data/QUICKSTART.md +96 -0
- data/README.md +36 -0
- data/Rakefile +32 -0
- data/bin/cf_deploy +10 -0
- data/cf_deployer.gemspec +23 -0
- data/lib/cf_deployer/application.rb +74 -0
- data/lib/cf_deployer/application_error.rb +4 -0
- data/lib/cf_deployer/aws_constants.rb +3 -0
- data/lib/cf_deployer/cli.rb +111 -0
- data/lib/cf_deployer/component.rb +103 -0
- data/lib/cf_deployer/config_loader.rb +189 -0
- data/lib/cf_deployer/config_validation.rb +138 -0
- data/lib/cf_deployer/defaults.rb +10 -0
- data/lib/cf_deployer/deployment_strategy/auto_scaling_group_swap.rb +102 -0
- data/lib/cf_deployer/deployment_strategy/base.rb +88 -0
- data/lib/cf_deployer/deployment_strategy/blue_green.rb +70 -0
- data/lib/cf_deployer/deployment_strategy/cname_swap.rb +108 -0
- data/lib/cf_deployer/deployment_strategy/create_or_update.rb +57 -0
- data/lib/cf_deployer/driver/auto_scaling_group.rb +86 -0
- data/lib/cf_deployer/driver/cloud_formation_driver.rb +85 -0
- data/lib/cf_deployer/driver/dry_run.rb +27 -0
- data/lib/cf_deployer/driver/elb_driver.rb +17 -0
- data/lib/cf_deployer/driver/instance.rb +29 -0
- data/lib/cf_deployer/driver/route53_driver.rb +79 -0
- data/lib/cf_deployer/driver/verisign_driver.rb +21 -0
- data/lib/cf_deployer/hook.rb +32 -0
- data/lib/cf_deployer/logger.rb +34 -0
- data/lib/cf_deployer/stack.rb +154 -0
- data/lib/cf_deployer/status_presenter.rb +195 -0
- data/lib/cf_deployer/version.rb +3 -0
- data/lib/cf_deployer.rb +97 -0
- data/spec/fakes/instance.rb +32 -0
- data/spec/fakes/route53_client.rb +23 -0
- data/spec/fakes/stack.rb +65 -0
- data/spec/functional/deploy_spec.rb +73 -0
- data/spec/functional/kill_inactive_spec.rb +57 -0
- data/spec/functional_spec_helper.rb +3 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/unit/application_spec.rb +191 -0
- data/spec/unit/component_spec.rb +142 -0
- data/spec/unit/config_loader_spec.rb +356 -0
- data/spec/unit/config_validation_spec.rb +480 -0
- data/spec/unit/deployment_strategy/auto_scaling_group_swap_spec.rb +435 -0
- data/spec/unit/deployment_strategy/base_spec.rb +44 -0
- data/spec/unit/deployment_strategy/cname_swap_spec.rb +294 -0
- data/spec/unit/deployment_strategy/create_or_update_spec.rb +113 -0
- data/spec/unit/deployment_strategy/deployment_strategy_spec.rb +29 -0
- data/spec/unit/driver/auto_scaling_group_spec.rb +127 -0
- data/spec/unit/driver/cloud_formation_spec.rb +32 -0
- data/spec/unit/driver/elb_spec.rb +11 -0
- data/spec/unit/driver/instance_spec.rb +30 -0
- data/spec/unit/driver/route53_spec.rb +85 -0
- data/spec/unit/driver/verisign_spec.rb +18 -0
- data/spec/unit/hook_spec.rb +64 -0
- data/spec/unit/stack_spec.rb +150 -0
- data/spec/unit/status_presenter_spec.rb +108 -0
- metadata +197 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
module CfDeployer
|
2
|
+
class Application
|
3
|
+
attr_reader :components
|
4
|
+
|
5
|
+
def initialize(context = {})
|
6
|
+
@context = context
|
7
|
+
get_components
|
8
|
+
add_component_dependencies
|
9
|
+
@components.sort!
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_components
|
13
|
+
@components = []
|
14
|
+
@context[:components].keys.each do | key |
|
15
|
+
component = Component.new(@context[:application], @context[:environment], key.to_s, @context[:components][key])
|
16
|
+
@components << component
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_component_dependencies
|
21
|
+
@context[:components].keys.each do | key |
|
22
|
+
component = @components.find { |c| c.name == key.to_s }
|
23
|
+
dependencies = @context[:components][key][:'depends-on'] || []
|
24
|
+
dependencies.each do | parent_name |
|
25
|
+
parent = @components.find { |c| c.name == parent_name }
|
26
|
+
if parent
|
27
|
+
parent.children << component
|
28
|
+
component.dependencies << parent
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def deploy
|
35
|
+
Log.debug @context
|
36
|
+
components = get_targets().sort
|
37
|
+
components.each &:deploy
|
38
|
+
end
|
39
|
+
|
40
|
+
def json
|
41
|
+
components = get_targets().sort
|
42
|
+
components.each &:json
|
43
|
+
end
|
44
|
+
|
45
|
+
def status component_name, verbosity
|
46
|
+
statuses = {}
|
47
|
+
@components.select { |component| component_name.nil? || component_name == component.name }.each do |component|
|
48
|
+
statuses[component.name] = component.status(verbosity != 'stacks')
|
49
|
+
end
|
50
|
+
statuses
|
51
|
+
end
|
52
|
+
|
53
|
+
def destroy
|
54
|
+
components = get_targets.sort { |a, b| b <=> a }
|
55
|
+
components.each &:destroy
|
56
|
+
end
|
57
|
+
|
58
|
+
def kill_inactive
|
59
|
+
component = get_targets.first
|
60
|
+
component.kill_inactive
|
61
|
+
end
|
62
|
+
|
63
|
+
def switch
|
64
|
+
@context[:targets].each do | component_name |
|
65
|
+
@components.find { |c| c.name == component_name }.switch
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def get_targets
|
71
|
+
targets = @components.select { |c| @context[:targets].include?(c.name) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module CfDeployer
|
4
|
+
class CLI < Thor
|
5
|
+
def self.exit_on_failure?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
class_option :'config-file', :aliases => '-f', :desc => "cf_deployer config file", :default => 'config/cf_deployer.yml'
|
10
|
+
class_option :'log-level', :aliases => '-l', :desc => "logging level", :enum => %w(info debug aws-debug), :default => 'info'
|
11
|
+
class_option :'dry-run', :aliases => '-d', :desc => "Say what we would do but don't actually make changes to anything", :banner => ''
|
12
|
+
class_option :settings, :aliases => '-s', :type => :hash, :desc => "key:value pair to overwrite setting in config"
|
13
|
+
class_option :inputs, :aliases => '-i', :type => :hash, :desc => "key:value pair to overwrite in template inputs"
|
14
|
+
class_option :region, :aliases => '-r', :desc => "Amazon region", :default => 'us-east-1'
|
15
|
+
|
16
|
+
desc "deploy [ENVIRONMENT] [COMPONENT]\t", 'Deploy the specified components'
|
17
|
+
def deploy environment, component = nil
|
18
|
+
prep_for_action :deploy, environment, component
|
19
|
+
CfDeployer.deploy merged_options
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "destroy [ENVIRONMENT] [COMPONENT]\t", 'Destroy the specified environment/component'
|
23
|
+
def destroy environment, component = nil
|
24
|
+
prep_for_action :destroy, environment, component
|
25
|
+
CfDeployer.destroy merged_options
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "config [ENVIRONMENT]", 'Show parsed config'
|
29
|
+
def config environment
|
30
|
+
prep_for_action :config, environment
|
31
|
+
CfDeployer.config merged_options
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "json [ENVIRONMENT] [COMPONENT]", 'Show parsed CloudFormation JSON for the target component'
|
35
|
+
def json environment, component = nil
|
36
|
+
prep_for_action :json, environment, component
|
37
|
+
CfDeployer.json merged_options
|
38
|
+
end
|
39
|
+
|
40
|
+
desc 'status [ENVIRONMENT] [COMPONENT]', 'Show the status of the specified Cloud Formation components specified in your yml'
|
41
|
+
method_option :verbosity, :aliases => '-v', :desc => 'Verbosity level', :enum => ['stacks','instances','all'], :default => 'instances'
|
42
|
+
method_option :'output-format', :aliases => '-o', :enum => ['human','json'], :default => 'human', :desc => 'Output format'
|
43
|
+
def status environment, component = nil
|
44
|
+
prep_for_action :status, environment, component
|
45
|
+
CfDeployer.status merged_options
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'kill_inactive [ENVIRONMENT] [COMPONENT]', 'Destroy the inactive stack for a given component/environment'
|
49
|
+
def kill_inactive environment, component
|
50
|
+
prep_for_action :kill_inactive, environment, component
|
51
|
+
CfDeployer.kill_inactive merged_options
|
52
|
+
end
|
53
|
+
|
54
|
+
desc 'switch [ENVIRONMENT] [COMPONENT]', 'Switch active and inactive stacks'
|
55
|
+
def switch environment, component
|
56
|
+
prep_for_action :switch, environment, component
|
57
|
+
CfDeployer.switch merged_options
|
58
|
+
end
|
59
|
+
|
60
|
+
no_commands do
|
61
|
+
|
62
|
+
def prep_for_action action, environment, component = nil
|
63
|
+
no_component_required_actions = [:config, :status]
|
64
|
+
if environment == 'help' || (no_component_required_actions.include? action && component == nil)
|
65
|
+
self.class.command_help shell, action
|
66
|
+
exit 0
|
67
|
+
end
|
68
|
+
@environment = environment
|
69
|
+
@component = [component].compact
|
70
|
+
validate_cli options
|
71
|
+
set_log_level
|
72
|
+
detect_dry_run
|
73
|
+
end
|
74
|
+
|
75
|
+
def merged_options
|
76
|
+
symbolize_all_keys options.merge({:environment => @environment, :component => @component})
|
77
|
+
end
|
78
|
+
|
79
|
+
def set_log_level
|
80
|
+
if options[:'log-level'] == 'aws-debug'
|
81
|
+
CfDeployer::Log.level = 'debug'
|
82
|
+
AWS.config :logger => Logger.new($stdout)
|
83
|
+
else
|
84
|
+
CfDeployer::Log.level = options[:'log-level']
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def detect_dry_run
|
89
|
+
CfDeployer::Driver::DryRun.enable if options[:'dry-run']
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_cli cli_options
|
93
|
+
unless File.file?(options[:'config-file'])
|
94
|
+
error_exit "ERROR: #{options[:'config-file']} is not a file."
|
95
|
+
end
|
96
|
+
error_exit "ERROR: No environment specified!" unless @environment
|
97
|
+
end
|
98
|
+
|
99
|
+
def symbolize_all_keys(hash)
|
100
|
+
return hash unless hash.is_a?(Hash)
|
101
|
+
hash.inject({}){|memo,(k,v)| memo.delete(k); memo[k.to_sym] = symbolize_all_keys(v); memo}
|
102
|
+
end
|
103
|
+
|
104
|
+
def error_exit message
|
105
|
+
puts message
|
106
|
+
exit 1
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module CfDeployer
|
2
|
+
class Component
|
3
|
+
attr_reader :name, :dependencies, :children
|
4
|
+
|
5
|
+
def initialize(application_name, environment_name, component_name, context)
|
6
|
+
@application_name = application_name
|
7
|
+
@environment_name = environment_name
|
8
|
+
@name = component_name
|
9
|
+
@context = context
|
10
|
+
@dependencies = []
|
11
|
+
@children = []
|
12
|
+
Log.debug "initializing #{name}.."
|
13
|
+
Log.debug @context
|
14
|
+
end
|
15
|
+
|
16
|
+
def exists?
|
17
|
+
strategy.exists?
|
18
|
+
end
|
19
|
+
|
20
|
+
def kill_inactive
|
21
|
+
strategy.kill_inactive
|
22
|
+
end
|
23
|
+
|
24
|
+
def deploy
|
25
|
+
Log.debug "deploying #{name}..."
|
26
|
+
@dependencies.each do |parent|
|
27
|
+
parent.deploy unless(parent.exists?)
|
28
|
+
end
|
29
|
+
resolve_settings
|
30
|
+
strategy.deploy
|
31
|
+
end
|
32
|
+
|
33
|
+
def json
|
34
|
+
resolve_settings
|
35
|
+
puts "#{name} json template:"
|
36
|
+
puts ConfigLoader.component_json(name, @context)
|
37
|
+
end
|
38
|
+
|
39
|
+
def destroy
|
40
|
+
raise ApplicationError.new("Unable to destroy #{name}, it is depended on by other components") if any_children_exist?
|
41
|
+
strategy.destroy
|
42
|
+
end
|
43
|
+
|
44
|
+
def switch
|
45
|
+
exists? ? strategy.switch : (raise ApplicationError.new("No stack exists for component: #{name}"))
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def output_value(key)
|
50
|
+
strategy.output_value(key)
|
51
|
+
end
|
52
|
+
|
53
|
+
def <=>(other)
|
54
|
+
i_am_depednent = depends_on? other
|
55
|
+
it_is_dependent = other.depends_on? self
|
56
|
+
|
57
|
+
if i_am_depednent
|
58
|
+
1
|
59
|
+
elsif it_is_dependent
|
60
|
+
-1
|
61
|
+
else
|
62
|
+
0
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
"component: #{name}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def depends_on?(component, source=self)
|
71
|
+
raise ApplicationError.new("Cyclic dependency") if @dependencies.include?(source)
|
72
|
+
@dependencies.include?(component) || @dependencies.any? { |d| d.depends_on?(component, source) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def status get_resource_statuses
|
76
|
+
strategy.status get_resource_statuses
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def resolve_settings
|
82
|
+
inputs.each do |key, value|
|
83
|
+
if(value.is_a? Hash)
|
84
|
+
dependency = @dependencies.find { |d| d.name == value[:component] }
|
85
|
+
output_key = value[:'output-key']
|
86
|
+
inputs[key] = dependency.output_value(output_key)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def any_children_exist?
|
92
|
+
children.any?(&:exists?)
|
93
|
+
end
|
94
|
+
|
95
|
+
def inputs
|
96
|
+
@context[:inputs]
|
97
|
+
end
|
98
|
+
|
99
|
+
def strategy
|
100
|
+
@strategy ||= DeploymentStrategy.create(@application_name, @environment_name, name, @context)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module CfDeployer
|
2
|
+
class ConfigLoader
|
3
|
+
|
4
|
+
def self.component_json component, config
|
5
|
+
json_file = File.join(config[:config_dir], "#{component}.json")
|
6
|
+
raise ApplicationError.new("#{json_file} is missing") unless File.exists?(json_file)
|
7
|
+
ERB.new(File.read(json_file)).result(binding)
|
8
|
+
end
|
9
|
+
|
10
|
+
def load(options)
|
11
|
+
config_text = File.read(options[:'config-file'])
|
12
|
+
erbed_config = erb_with_environment_and_region(config_text, options[:environment], options[:region])
|
13
|
+
yaml = symbolize_all_keys(load_yaml(erbed_config))
|
14
|
+
@config = options.merge(yaml)
|
15
|
+
@config[:components] ||= {}
|
16
|
+
@config[:settings] ||= {}
|
17
|
+
@config[:environments] ||= {}
|
18
|
+
@config[:tags] ||= {}
|
19
|
+
get_targets
|
20
|
+
copy_config_dir
|
21
|
+
merge_section(:settings)
|
22
|
+
merge_section(:inputs)
|
23
|
+
merge_section(:tags)
|
24
|
+
copy_region_app_env_component
|
25
|
+
get_cf_template_keys('Parameters')
|
26
|
+
get_cf_template_keys('Outputs')
|
27
|
+
set_default_settings
|
28
|
+
@config.delete(:settings)
|
29
|
+
@config
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def load_yaml(text)
|
35
|
+
YAML.load text
|
36
|
+
rescue
|
37
|
+
raise ApplicationError.new("The config file is not a valid yaml file")
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def merge_section(section)
|
42
|
+
merge_component_options section
|
43
|
+
merge_environment_options(@config[:environment], section)
|
44
|
+
merge_environment_variables section
|
45
|
+
@config[:cli_overrides] ||= {}
|
46
|
+
merge_options(@config[:cli_overrides][section] || {}, section)
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_targets
|
50
|
+
@config[:component] ||= []
|
51
|
+
@config[:targets] = @config[:component].length == 0 ? @config[:components].keys.map(&:to_s) : @config[:component]
|
52
|
+
end
|
53
|
+
|
54
|
+
def copy_region_app_env_component
|
55
|
+
@config[:components].each do |component_name, component|
|
56
|
+
component[:settings][:region] = @config[:region]
|
57
|
+
component[:inputs][:region] = @config[:region]
|
58
|
+
|
59
|
+
component[:settings][:application] = @config[:application]
|
60
|
+
component[:inputs][:application] = @config[:application]
|
61
|
+
|
62
|
+
component[:settings][:component] = component_name.to_s
|
63
|
+
component[:inputs][:component] = component_name.to_s
|
64
|
+
|
65
|
+
component[:settings][:environment] = @config[:environment]
|
66
|
+
component[:inputs][:environment] = @config[:environment]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_default_settings
|
71
|
+
@config[:components].each do |component_name, component|
|
72
|
+
if component[:'deployment-strategy'] == 'cname-swap'
|
73
|
+
component[:settings][:'elb-name-output'] ||= Defaults::ELBName
|
74
|
+
component[:settings][:'dns-driver'] ||= Defaults::DNSDriver
|
75
|
+
end
|
76
|
+
component[:settings][:'raise-error-for-unused-inputs'] ||= Defaults::RaiseErrorForUnusedInputs
|
77
|
+
component[:settings][:'auto-scaling-group-name-output'] ||= [Defaults::AutoScalingGroupName] if component[:'deployment-strategy'] == 'auto-scaling-group-swap'
|
78
|
+
component[:settings][:'auto-scaling-group-name-output'] ||= [Defaults::AutoScalingGroupName] if component[:'defined_outputs'].keys.include?(Defaults::AutoScalingGroupName.to_sym)
|
79
|
+
if component[:settings][:'keep-previous-stack'] == nil
|
80
|
+
component[:settings][:'keep-previous-stack'] = Defaults::KeepPreviousStack
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def get_cf_template_keys(name)
|
86
|
+
@config[:components].keys.each do |component|
|
87
|
+
parameters = cf_template(component)[name] || {}
|
88
|
+
@config[:components][component]["defined_#{name.downcase}".to_sym] = symbolize_all_keys(parameters)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def cf_template(component)
|
93
|
+
config = deep_dup(@config[:components][component])
|
94
|
+
config[:inputs].each do |key, value|
|
95
|
+
if value.is_a?(Hash)
|
96
|
+
output_key = value[:'output-key']
|
97
|
+
config[:inputs][key] = "#{value[:component]}::#{output_key}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
json_content = self.class.component_json component.to_s, config
|
102
|
+
begin
|
103
|
+
JSON.load json_content
|
104
|
+
rescue JSON::ParserError => e
|
105
|
+
puts json_content
|
106
|
+
puts '=' * 80
|
107
|
+
puts e.message[0..300]
|
108
|
+
puts '=' * 80
|
109
|
+
raise "Couldn't parse JSON for component #{component}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def config_dir
|
114
|
+
File.dirname(@config[:'config-file'])
|
115
|
+
end
|
116
|
+
|
117
|
+
def copy_config_dir
|
118
|
+
@config[:components].each do |component_name, component|
|
119
|
+
component ||= {}
|
120
|
+
@config[:components][component_name] = component
|
121
|
+
component[:config_dir] = config_dir
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def merge_component_options(section)
|
126
|
+
common_options = @config[section] || {}
|
127
|
+
@config[:components].keys.each do |component|
|
128
|
+
@config[:components][component] ||= {}
|
129
|
+
component_options = @config[:components][component].delete(section) || {}
|
130
|
+
@config[:components][component][section] = common_options.merge(component_options)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def merge_environment_options(environment_name, section)
|
135
|
+
return unless environment_name
|
136
|
+
environment = @config[:environments][environment_name.to_sym] || {}
|
137
|
+
environment_options = environment[section] || {}
|
138
|
+
merge_options(environment_options, section)
|
139
|
+
environment_components = environment[:components] || {}
|
140
|
+
merge_environment_component(environment_components, section)
|
141
|
+
end
|
142
|
+
|
143
|
+
def merge_options(options, section)
|
144
|
+
@config[:components].keys.each do |component|
|
145
|
+
component_options = @config[:components][component].delete(section) || {}
|
146
|
+
@config[:components][component][section] = component_options.merge(options)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def merge_environment_component(environment_components, section)
|
151
|
+
environment_components.keys.each do |component|
|
152
|
+
@config[:components][component] ||= {}
|
153
|
+
component_options = @config[:components][component].delete(section) || {}
|
154
|
+
@config[:components][component][section] = component_options.merge(environment_components[component][section] || {})
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def merge_environment_variables(section)
|
159
|
+
@config[:components].keys.each do |component|
|
160
|
+
merge_environment_variables_to_options( @config[:components][component][section], section)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def merge_environment_variables_to_options(options, section)
|
165
|
+
options.keys.each do |key|
|
166
|
+
environment_variable = ENV["cfdeploy_#{section}_#{key.to_s}"]
|
167
|
+
options[key] = environment_variable if environment_variable
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def symbolize_all_keys(hash)
|
172
|
+
return hash unless hash.is_a?(Hash)
|
173
|
+
hash.inject({}){|memo,(k,v)| memo.delete(k); memo[k.to_sym] = symbolize_all_keys(v); memo}
|
174
|
+
end
|
175
|
+
|
176
|
+
def erb_with_environment_and_region(contents, environment, region)
|
177
|
+
ERB.new(contents).result(binding)
|
178
|
+
end
|
179
|
+
|
180
|
+
def deep_dup(hash)
|
181
|
+
new_hash = {}
|
182
|
+
hash.each do |key, value|
|
183
|
+
value.is_a?(Hash) ? new_hash[key] = deep_dup(value) : new_hash[key] = value
|
184
|
+
end
|
185
|
+
new_hash
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module CfDeployer
|
2
|
+
class ConfigValidation
|
3
|
+
|
4
|
+
class ValidationError < ApplicationError
|
5
|
+
end
|
6
|
+
|
7
|
+
CommonInputs = [:application, :environment, :component, :region]
|
8
|
+
EnvironmentOptions = [:settings, :inputs, :tags, :components]
|
9
|
+
ComponentOptions = [:settings, :inputs, :tags, :'depends-on', :'deployment-strategy', :'before-destroy', :'after-create', :'after-swap', :'defined_outputs', :'defined_parameters', :config_dir, :capabilities]
|
10
|
+
|
11
|
+
def validate config, validate_inputs = true
|
12
|
+
@config = config
|
13
|
+
@errors = []
|
14
|
+
@warnings = []
|
15
|
+
check_application_name
|
16
|
+
check_components validate_inputs
|
17
|
+
check_environments
|
18
|
+
@warnings.each { |message| puts "WARNNING:#{message}" }
|
19
|
+
raise ValidationError.new(@errors.join("\n")) if @errors.length > 0
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
|
25
|
+
def check_asg_name_output(component)
|
26
|
+
component[:settings][:'auto-scaling-group-name-output'] ||= []
|
27
|
+
outputs = component[:settings][:'auto-scaling-group-name-output'].map { |name| name.to_sym }
|
28
|
+
missing_output_keys = (outputs - component[:defined_outputs].keys)
|
29
|
+
@errors << "'#{missing_output_keys.map(&:to_s)}' is not a CF stack output" unless missing_output_keys.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_cname_swap_options(component)
|
33
|
+
@errors << "dns-fqdn is required when using cname-swap deployment-strategy" unless component[:settings][:'dns-fqdn']
|
34
|
+
@errors << "dns-zone is required when using cname-swap deployment-strategy" unless component[:settings][:'dns-zone']
|
35
|
+
@errors << "'#{component[:settings][:'elb-name-output']}' is not a CF stack output, which is required by cname-swap deployment" unless component[:defined_outputs].keys.include?(component[:settings][:'elb-name-output'].to_sym)
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_application_name
|
39
|
+
@config[:application] = "" unless @config[:application]
|
40
|
+
return @errors << "Application name is missing in config" unless @config[:application].length > 0
|
41
|
+
@errors << "Application name cannot be longer than 100 and can only contain letters, numbers, '-' and '.'" unless @config[:application] =~ /^[a-zA-Z0-9\.-]{1,100}$/
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_components validate_inputs
|
45
|
+
@config[:components] ||= {}
|
46
|
+
return @errors << "At least one component must be defined in config" unless @config[:components].length > 0
|
47
|
+
deployable_components = @config[:targets] || []
|
48
|
+
component_targets = @config[:components].select { |key, value| deployable_components.include?(key.to_s) }
|
49
|
+
invalid_names = deployable_components - component_targets.keys.map(&:to_s)
|
50
|
+
@errors << "Found invalid deployment components #{invalid_names}" if invalid_names.size > 0
|
51
|
+
component_targets.each do |component_name, component|
|
52
|
+
component[:settings] ||= {}
|
53
|
+
component[:inputs] ||= {}
|
54
|
+
component[:defined_outputs] ||= {}
|
55
|
+
@errors << "Component name cannot be longer than 100 and can only contain letters, numbers, '-' and '.': #{component_name}" unless component_name =~ /^[A-Za-z0-9\.-]{1,100}$/
|
56
|
+
check_parameters component_name, component if validate_inputs
|
57
|
+
check_cname_swap_options(component) if component[:'deployment-strategy'] == 'cname-swap'
|
58
|
+
check_asg_name_output(component)
|
59
|
+
check_hooks(component)
|
60
|
+
check_component_options(component_name, component)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_component_options(name, component)
|
65
|
+
component.keys.each do |option|
|
66
|
+
@errors << "The option '#{option}' of the component '#{name}' is not valid" unless ComponentOptions.include?(option)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def check_hooks(component)
|
71
|
+
hook_names = [:'before-destroy', :'after-create', :'after-swap']
|
72
|
+
hook_names.each do |hook_name|
|
73
|
+
next unless component[hook_name] && component[hook_name].is_a?(Hash)
|
74
|
+
@errors << "Invalid hook '#{hook_name}'" unless component[hook_name][:file] || component[hook_name][:code]
|
75
|
+
check_hook_file(component, hook_name)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def check_hook_file(component, hook_name)
|
80
|
+
file_name = component[hook_name][:file]
|
81
|
+
return unless file_name
|
82
|
+
path = File.join(component[:config_dir], file_name)
|
83
|
+
@errors << "File '#{path}' does not exist, which is required by hook '#{hook_name}'" unless File.exists?(path)
|
84
|
+
end
|
85
|
+
|
86
|
+
def check_environments
|
87
|
+
@config[:environments] ||= {}
|
88
|
+
@config[:environments].each do | name, environment |
|
89
|
+
@errors << "Environment name cannot be longer than 12 and can only contain letters, numbers, '-' and '.': #{name}" unless name =~ /^[a-zA-Z0-9\.-]{1,12}$/
|
90
|
+
check_environment_options(name, environment)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def check_environment_options(name, environment)
|
95
|
+
environment.keys.each do |option|
|
96
|
+
@errors << "The option '#{option}' of the environment '#{name}' is not valid" unless EnvironmentOptions.include?(option)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
def check_parameters(component_name, component)
|
102
|
+
component[:defined_parameters] ||= {}
|
103
|
+
component[:defined_outputs] ||= {}
|
104
|
+
component[:defined_parameters].each do | parameter_name, parameter |
|
105
|
+
if component[:inputs].keys.include?(parameter_name) || parameter[:Default]
|
106
|
+
check_output_reference(parameter_name, component_name)
|
107
|
+
else
|
108
|
+
@errors << "No input setting '#{parameter_name}' found for CF template parameter in component #{component_name}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
check_un_used_inputs(component_name, component)
|
112
|
+
end
|
113
|
+
|
114
|
+
def check_un_used_inputs(component_name, component)
|
115
|
+
component[:inputs].keys.each do |input|
|
116
|
+
unless component[:defined_parameters].keys.include?(input) || CommonInputs.include?(input)
|
117
|
+
message = "The input '#{input}' defined in the component '#{component_name}' is not used in the json template as a parameter"
|
118
|
+
if component[:settings][:'raise-error-for-unused-inputs']
|
119
|
+
@errors << message
|
120
|
+
else
|
121
|
+
@warnings << message
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
def check_output_reference(setting_name, component_name)
|
129
|
+
setting = @config[:components][component_name][:inputs][setting_name]
|
130
|
+
return unless setting.is_a?(Hash)
|
131
|
+
ref_component_name = setting[:component].to_sym
|
132
|
+
ref_component = @config[:components][ref_component_name]
|
133
|
+
output_key = setting[:'output-key'].to_sym
|
134
|
+
@errors << "No output '#{output_key}' found in CF template of component #{ref_component_name}, which is referenced by input setting '#{setting_name}' in component #{component_name}" unless ref_component[:defined_outputs].keys.include?(output_key)
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|