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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +29 -0
  3. data/ChangeLog.md +16 -0
  4. data/DETAILS.md +268 -0
  5. data/FAQ.md +61 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.lock +51 -0
  8. data/LICENSE +22 -0
  9. data/QUICKSTART.md +96 -0
  10. data/README.md +36 -0
  11. data/Rakefile +32 -0
  12. data/bin/cf_deploy +10 -0
  13. data/cf_deployer.gemspec +23 -0
  14. data/lib/cf_deployer/application.rb +74 -0
  15. data/lib/cf_deployer/application_error.rb +4 -0
  16. data/lib/cf_deployer/aws_constants.rb +3 -0
  17. data/lib/cf_deployer/cli.rb +111 -0
  18. data/lib/cf_deployer/component.rb +103 -0
  19. data/lib/cf_deployer/config_loader.rb +189 -0
  20. data/lib/cf_deployer/config_validation.rb +138 -0
  21. data/lib/cf_deployer/defaults.rb +10 -0
  22. data/lib/cf_deployer/deployment_strategy/auto_scaling_group_swap.rb +102 -0
  23. data/lib/cf_deployer/deployment_strategy/base.rb +88 -0
  24. data/lib/cf_deployer/deployment_strategy/blue_green.rb +70 -0
  25. data/lib/cf_deployer/deployment_strategy/cname_swap.rb +108 -0
  26. data/lib/cf_deployer/deployment_strategy/create_or_update.rb +57 -0
  27. data/lib/cf_deployer/driver/auto_scaling_group.rb +86 -0
  28. data/lib/cf_deployer/driver/cloud_formation_driver.rb +85 -0
  29. data/lib/cf_deployer/driver/dry_run.rb +27 -0
  30. data/lib/cf_deployer/driver/elb_driver.rb +17 -0
  31. data/lib/cf_deployer/driver/instance.rb +29 -0
  32. data/lib/cf_deployer/driver/route53_driver.rb +79 -0
  33. data/lib/cf_deployer/driver/verisign_driver.rb +21 -0
  34. data/lib/cf_deployer/hook.rb +32 -0
  35. data/lib/cf_deployer/logger.rb +34 -0
  36. data/lib/cf_deployer/stack.rb +154 -0
  37. data/lib/cf_deployer/status_presenter.rb +195 -0
  38. data/lib/cf_deployer/version.rb +3 -0
  39. data/lib/cf_deployer.rb +97 -0
  40. data/spec/fakes/instance.rb +32 -0
  41. data/spec/fakes/route53_client.rb +23 -0
  42. data/spec/fakes/stack.rb +65 -0
  43. data/spec/functional/deploy_spec.rb +73 -0
  44. data/spec/functional/kill_inactive_spec.rb +57 -0
  45. data/spec/functional_spec_helper.rb +3 -0
  46. data/spec/spec_helper.rb +8 -0
  47. data/spec/unit/application_spec.rb +191 -0
  48. data/spec/unit/component_spec.rb +142 -0
  49. data/spec/unit/config_loader_spec.rb +356 -0
  50. data/spec/unit/config_validation_spec.rb +480 -0
  51. data/spec/unit/deployment_strategy/auto_scaling_group_swap_spec.rb +435 -0
  52. data/spec/unit/deployment_strategy/base_spec.rb +44 -0
  53. data/spec/unit/deployment_strategy/cname_swap_spec.rb +294 -0
  54. data/spec/unit/deployment_strategy/create_or_update_spec.rb +113 -0
  55. data/spec/unit/deployment_strategy/deployment_strategy_spec.rb +29 -0
  56. data/spec/unit/driver/auto_scaling_group_spec.rb +127 -0
  57. data/spec/unit/driver/cloud_formation_spec.rb +32 -0
  58. data/spec/unit/driver/elb_spec.rb +11 -0
  59. data/spec/unit/driver/instance_spec.rb +30 -0
  60. data/spec/unit/driver/route53_spec.rb +85 -0
  61. data/spec/unit/driver/verisign_spec.rb +18 -0
  62. data/spec/unit/hook_spec.rb +64 -0
  63. data/spec/unit/stack_spec.rb +150 -0
  64. data/spec/unit/status_presenter_spec.rb +108 -0
  65. 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,4 @@
1
+ module CfDeployer
2
+ class ApplicationError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module AwsConstants
2
+ ELB_RESOURCE_TYPE = 'AWS::ElasticLoadBalancing::LoadBalancer'
3
+ 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
@@ -0,0 +1,10 @@
1
+ module CfDeployer
2
+ module Defaults
3
+ ELBName = 'ELBName'
4
+ AutoScalingGroupName = 'AutoScalingGroupName'
5
+ Timeout = 900
6
+ DNSDriver = 'CfDeployer::Driver::Route53'
7
+ RaiseErrorForUnusedInputs = false
8
+ KeepPreviousStack = true
9
+ end
10
+ end