cf_deployer 1.2.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|