cfndk 0.0.7 → 0.1.2

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 (78) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +79 -0
  3. data/.gitignore +1 -1
  4. data/.rspec +2 -0
  5. data/.rspec_parallel +6 -0
  6. data/.simplecov +9 -0
  7. data/Gemfile +11 -1
  8. data/Gemfile.lock +815 -0
  9. data/README.md +269 -76
  10. data/bin/cfndk +3 -18
  11. data/cfndk.gemspec +15 -6
  12. data/docker/Dockerfile +8 -0
  13. data/docker/build.sh +3 -0
  14. data/docker/cfndk.sh +14 -0
  15. data/lib/cfndk.rb +36 -0
  16. data/lib/cfndk/change_set_command.rb +103 -0
  17. data/lib/cfndk/command.rb +125 -119
  18. data/lib/cfndk/config_file_loadable.rb +13 -0
  19. data/lib/cfndk/credential_provider_chain.rb +12 -42
  20. data/lib/cfndk/credential_resolvable.rb +10 -0
  21. data/lib/cfndk/diff.rb +38 -0
  22. data/lib/cfndk/global_config.rb +46 -0
  23. data/lib/cfndk/key_pair.rb +66 -14
  24. data/lib/cfndk/key_pair_command.rb +60 -0
  25. data/lib/cfndk/key_pairs.rb +22 -5
  26. data/lib/cfndk/logger.rb +12 -3
  27. data/lib/cfndk/stack.rb +427 -126
  28. data/lib/cfndk/stack_command.rb +128 -0
  29. data/lib/cfndk/stacks.rb +48 -22
  30. data/lib/cfndk/subcommand_help_returnable.rb +16 -0
  31. data/lib/cfndk/template_packager.rb +210 -0
  32. data/lib/cfndk/uuid.rb +10 -0
  33. data/lib/cfndk/version.rb +1 -1
  34. data/skel/cfndk.yml +4 -0
  35. data/spec/.gitignore +1 -0
  36. data/spec/cfndk_change_set_create_spec.rb +436 -0
  37. data/spec/cfndk_change_set_destroy_spec.rb +160 -0
  38. data/spec/cfndk_change_set_execute_spec.rb +179 -0
  39. data/spec/cfndk_change_set_report_spec.rb +107 -0
  40. data/spec/cfndk_change_set_spec.rb +37 -0
  41. data/spec/cfndk_create_spec.rb +504 -0
  42. data/spec/cfndk_destroy_spec.rb +148 -0
  43. data/spec/cfndk_keypiar_spec.rb +397 -0
  44. data/spec/cfndk_report_spec.rb +164 -0
  45. data/spec/cfndk_spec.rb +103 -0
  46. data/spec/cfndk_stack_create_spec.rb +814 -0
  47. data/spec/cfndk_stack_destroy_spec.rb +225 -0
  48. data/spec/cfndk_stack_report_spec.rb +181 -0
  49. data/spec/cfndk_stack_spec.rb +133 -0
  50. data/spec/cfndk_stack_update_spec.rb +553 -0
  51. data/spec/fixtures/big_vpc.yaml +533 -0
  52. data/spec/fixtures/empty_resource.yaml +2 -0
  53. data/spec/fixtures/iam.json +8 -0
  54. data/spec/fixtures/iam.yaml +38 -0
  55. data/spec/fixtures/iam_different.json +8 -0
  56. data/spec/fixtures/invalid_vpc.yaml +21 -0
  57. data/spec/fixtures/lambda_function/index.js +4 -0
  58. data/spec/fixtures/lambda_function/lambda_function.json +4 -0
  59. data/spec/fixtures/lambda_function/lambda_function.yaml +28 -0
  60. data/spec/fixtures/nested_stack.json +35 -0
  61. data/spec/fixtures/nested_stack.yaml +20 -0
  62. data/spec/fixtures/serverless_function/index.js +4 -0
  63. data/spec/fixtures/serverless_function/serverless_function.json +4 -0
  64. data/spec/fixtures/serverless_function/serverless_function.yaml +21 -0
  65. data/spec/fixtures/sg.json +8 -0
  66. data/spec/fixtures/sg.yaml +27 -0
  67. data/spec/fixtures/sg_different.yaml +22 -0
  68. data/spec/fixtures/stack.json +8 -0
  69. data/spec/fixtures/stack.template.json +39 -0
  70. data/spec/fixtures/stack.yaml +22 -0
  71. data/spec/fixtures/vpc.json +8 -0
  72. data/spec/fixtures/vpc.template.json +40 -0
  73. data/spec/fixtures/vpc.yaml +21 -0
  74. data/spec/fixtures/vpc_different.yaml +21 -0
  75. data/spec/spec_helper.rb +14 -0
  76. data/spec/support/aruba.rb +6 -0
  77. data/vagrant/Vagrantfile +89 -0
  78. metadata +259 -31
@@ -0,0 +1,128 @@
1
+ module CFnDK
2
+ class StackCommand < Thor
3
+ include SubcommandHelpReturnable
4
+ include ConfigFileLoadable
5
+ include CredentialResolvable
6
+
7
+ class_option :verbose, type: :boolean, aliases: 'v', desc: 'More verbose output.'
8
+ class_option :color, type: :boolean, default: true, desc: 'Use colored output'
9
+ class_option :config_path, type: :string, aliases: 'c', default: "#{Dir.getwd}/cfndk.yml", desc: 'The configuration file to use'
10
+ class_option :stack_names, type: :array, aliases: 's', desc: 'Target stack names'
11
+
12
+ desc 'create', 'Create stack'
13
+ option :uuid, type: :string, aliases: 'u', default: ENV['CFNDK_UUID'] || nil, desc: 'Use UUID'
14
+ option :properties, type: :hash, aliases: 'p', default: {}, desc: 'Set property'
15
+ def create
16
+ CFnDK.logger.info 'create...'.color(:green)
17
+ data = load_config_data(options)
18
+ credentials = resolve_credential(data, options)
19
+ global_config = CFnDK::GlobalConfig.new(data, options)
20
+ stacks = CFnDK::Stacks.new(data, options, credentials)
21
+
22
+ global_config.pre_command_execute
23
+ stacks.pre_command_execute
24
+ stacks.validate
25
+ stacks.create
26
+ stacks.post_command_execute
27
+ global_config.post_command_execute
28
+ return 0
29
+ rescue => e
30
+ CFnDK.logger.error "#{e.class}: #{e.message}".color(:red)
31
+ e.backtrace_locations.each do |line|
32
+ CFnDK.logger.debug line
33
+ end
34
+ return 1
35
+ end
36
+
37
+ desc 'update', 'Update stack'
38
+ option :uuid, type: :string, aliases: 'u', default: ENV['CFNDK_UUID'] || nil, desc: 'Use UUID'
39
+ option :properties, type: :hash, aliases: 'p', default: {}, desc: 'Set property'
40
+ def update
41
+ CFnDK.logger.info 'update...'.color(:green)
42
+ data = load_config_data(options)
43
+ credentials = resolve_credential(data, options)
44
+ global_config = CFnDK::GlobalConfig.new(data, options)
45
+ stacks = CFnDK::Stacks.new(data, options, credentials)
46
+
47
+ global_config.pre_command_execute
48
+ stacks.pre_command_execute
49
+ stacks.validate
50
+ stacks.update
51
+ stacks.post_command_execute
52
+ global_config.post_command_execute
53
+ return 0
54
+ rescue => e
55
+ CFnDK.logger.error "#{e.class}: #{e.message}".color(:red)
56
+ e.backtrace_locations.each do |line|
57
+ CFnDK.logger.debug line
58
+ end
59
+ return 1
60
+ end
61
+
62
+ desc 'destroy', 'Destroy stack'
63
+ option :force, type: :boolean, aliases: 'f', default: false, desc: 'Say yes to all prompts for confirmation'
64
+ option :uuid, type: :string, aliases: 'u', default: ENV['CFNDK_UUID'] || nil, desc: 'Use UUID'
65
+ def destroy
66
+ CFnDK.logger.info 'destroy...'.color(:green)
67
+ data = load_config_data(options)
68
+ credentials = resolve_credential(data, options)
69
+
70
+ stacks = CFnDK::Stacks.new(data, options, credentials)
71
+
72
+ if options[:force] || yes?('Are you sure you want to destroy? (y/n)', :yellow)
73
+ stacks.destroy
74
+ return 0
75
+ else
76
+ CFnDK.logger.info 'destroy command was canceled'.color(:green)
77
+ return 2
78
+ end
79
+ rescue => e
80
+ CFnDK.logger.error "#{e.class}: #{e.message}".color(:red)
81
+ e.backtrace_locations.each do |line|
82
+ CFnDK.logger.debug line
83
+ end
84
+ return 1
85
+ end
86
+
87
+ desc 'validate', 'Validate stack'
88
+ def validate
89
+ CFnDK.logger.info 'validate...'.color(:green)
90
+ data = load_config_data(options)
91
+ credentials = resolve_credential(data, options)
92
+ global_config = CFnDK::GlobalConfig.new(data, options)
93
+ stacks = CFnDK::Stacks.new(data, options, credentials)
94
+
95
+ global_config.pre_command_execute
96
+ stacks.pre_command_execute
97
+ stacks.validate
98
+ stacks.post_command_execute
99
+ global_config.post_command_execute
100
+ return 0
101
+ rescue => e
102
+ CFnDK.logger.error "#{e.class}: #{e.message}".color(:red)
103
+ e.backtrace_locations.each do |line|
104
+ CFnDK.logger.debug line
105
+ end
106
+ return 1
107
+ end
108
+
109
+ desc 'report', 'Report stack'
110
+ option :uuid, type: :string, aliases: 'u', default: ENV['CFNDK_UUID'] || nil, desc: 'Use UUID'
111
+ option :types, type: :array, default: %w(tag output parameter resource event), desc: 'Report type'
112
+ def report
113
+ CFnDK.logger.info 'report...'.color(:green)
114
+ data = load_config_data(options)
115
+ credentials = resolve_credential(data, options)
116
+
117
+ stacks = CFnDK::Stacks.new(data, options, credentials)
118
+ stacks.report
119
+ return 0
120
+ rescue => e
121
+ CFnDK.logger.error "#{e.class}: #{e.message}".color(:red)
122
+ e.backtrace_locations.each do |line|
123
+ CFnDK.logger.debug line
124
+ end
125
+ return 1
126
+ end
127
+ end
128
+ end
@@ -3,8 +3,7 @@ module CFnDK
3
3
  def initialize(data, option, credentials)
4
4
  @option = option
5
5
  @credentials = credentials
6
- @logger = CFnDK::Logger.new(option)
7
-
6
+ @global_config = CFnDK::GlobalConfig.new(data, option)
8
7
  prepare_stack(data)
9
8
  prepare_sequence
10
9
  end
@@ -52,48 +51,74 @@ module CFnDK
52
51
  end
53
52
  end
54
53
 
55
- def create_or_changeset
54
+ def create_change_set
56
55
  @sequence.each do |stacks|
57
- create_stacks = []
58
- changeset_stacks = []
56
+ wait_until_stacks = []
59
57
  stacks.each do |name|
60
- if @stacks[name].exits?
61
- @stacks[name].create_change_set
62
- changeset_stacks.push name
58
+ wait_until_stacks.push(@stacks[name].create_change_set)
59
+ end
60
+ wait_until_stacks.compact!
61
+ wait_until_stacks.each do |name|
62
+ @stacks[name].wait_until_create_change_set
63
+ end
64
+ end
65
+ end
66
+
67
+ def execute_change_set
68
+ @sequence.each do |stacks|
69
+ created_stacks = []
70
+ wait_until_stacks = []
71
+ stacks.each do |name|
72
+ created_stacks.push(name) if @stacks[name].created?
73
+ wait_until_stacks.push(@stacks[name].execute_change_set)
74
+ end
75
+ wait_until_stacks.compact!
76
+ wait_until_stacks.each do |name|
77
+ if created_stacks.include?(name)
78
+ @stacks[name].wait_until_update
63
79
  else
64
- @stacks[name].create
65
- create_stacks.push name
80
+ @stacks[name].wait_until_create
66
81
  end
67
82
  end
68
- create_stacks.each do |name|
69
- @stacks[name].wait_until_create
83
+ end
84
+ end
85
+
86
+ def delete_change_set
87
+ @sequence.reverse_each do |stacks|
88
+ stacks.each do |name|
89
+ @stacks[name].delete_change_set
70
90
  end
71
- changeset_stacks.each do |name|
72
- @stacks[name].wait_until_create_change_set
91
+ end
92
+ end
93
+
94
+ def report_change_set
95
+ @sequence.each do |stacks|
96
+ stacks.each do |name|
97
+ @stacks[name].report_change_set
73
98
  end
74
99
  end
75
100
  end
76
101
 
77
- def report_stack
102
+ def report
78
103
  @sequence.each do |stacks|
79
104
  stacks.each do |name|
80
- @stacks[name].report_stack
105
+ @stacks[name].report
81
106
  end
82
107
  end
83
108
  end
84
109
 
85
- def report_stack_resource
110
+ def pre_command_execute
86
111
  @sequence.each do |stacks|
87
112
  stacks.each do |name|
88
- @stacks[name].report_stack_resource
113
+ @stacks[name].pre_command_execute
89
114
  end
90
115
  end
91
116
  end
92
117
 
93
- def report_event
118
+ def post_command_execute
94
119
  @sequence.each do |stacks|
95
120
  stacks.each do |name|
96
- @stacks[name].report_event
121
+ @stacks[name].post_command_execute
97
122
  end
98
123
  end
99
124
  end
@@ -102,8 +127,9 @@ module CFnDK
102
127
 
103
128
  def prepare_stack(data)
104
129
  @stacks = {}
130
+ return unless data['stacks'].is_a?(Hash)
105
131
  data['stacks'].each do |name, properties|
106
- @stacks[name] = Stack.new(name, properties, @option, @credentials)
132
+ @stacks[name] = Stack.new(name, properties, @option, @global_config, @credentials)
107
133
  end
108
134
  end
109
135
 
@@ -117,7 +143,7 @@ module CFnDK
117
143
  names_of_processed_stack.include? depend_name
118
144
  end
119
145
  end
120
- raise 'There are cyclic dependency or stack is not exit. unprocessed_stack: ' + names_of_upprocessed_stack.join(',') if names.empty?
146
+ raise "There are cyclic dependency or stack doesn't exist. unprocessed_stack: " + names_of_upprocessed_stack.join(',') if names.empty?
121
147
  names_of_processed_stack += names
122
148
  names_of_upprocessed_stack -= names
123
149
  @sequence.push names
@@ -0,0 +1,16 @@
1
+ module CFnDK
2
+ module SubcommandHelpReturnable
3
+ module ClassMethods
4
+ def subcommand_help(cmd)
5
+ desc 'help [COMMAND]', 'Describe subcommands or one specific subcommand'
6
+ class_eval "
7
+ def help(command = nil, subcommand = true); super; return 2; end
8
+ "
9
+ end
10
+ end
11
+ extend ClassMethods
12
+ def self.included(klass)
13
+ klass.extend ClassMethods
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,210 @@
1
+ using Polyfill(
2
+ String: %w[#delete_prefix]
3
+ )
4
+
5
+ module CFnDK
6
+ class TemplatePackager
7
+ def initialize(template_file, region, package, global_config, s3_client, sts_client)
8
+ @template_file = template_file
9
+ @region = region
10
+ @package = package
11
+ @global_config = global_config
12
+ @s3_client = s3_client
13
+ @sts_client = sts_client
14
+ @template_body = nil
15
+ @is_uploaded = false
16
+ end
17
+
18
+ def large_template?
19
+ template_body.size > 51200
20
+ end
21
+
22
+ def template_body
23
+ package_templte
24
+ end
25
+
26
+ def upload_template_file
27
+ key = [@global_config.s3_template_hash, @template_file].compact.join('/')
28
+ url = "https://s3.amazonaws.com/#{bucket_name}/#{key}"
29
+
30
+ unless @is_uploaded
31
+ create_bucket
32
+ @s3_client.put_object(
33
+ body: template_body,
34
+ bucket: bucket_name,
35
+ key: key
36
+ )
37
+ @is_uploaded = true
38
+ CFnDK.logger.info('Put S3 object: ' + url + ' Size: ' + template_body.size.to_s)
39
+ end
40
+ url
41
+ end
42
+
43
+ def package_templte
44
+ if !@template_body
45
+ if !@package
46
+ @template_body = File.open(@template_file, 'r').read
47
+ return @template_body
48
+ end
49
+ orgTemplate = File.open(@template_file, 'r').read
50
+ CFnDK.logger.debug('Original Template:' + orgTemplate)
51
+ if is_json?(orgTemplate)
52
+ data = JSON.parse(orgTemplate)
53
+ else
54
+ data = YAML.load(orgTemplate.gsub(/!/, '____CFNDK!____'))
55
+ end
56
+
57
+ if data['Resources']
58
+ data['Resources'].each do |k, v|
59
+ next unless v.key?('Type')
60
+ t = v['Type']
61
+ properties = v['Properties'] || {}
62
+ case t
63
+ when 'AWS::CloudFormation::Stack' then
64
+ if properties['TemplateURL'] =~ /^\s*./
65
+ tp = TemplatePackager.new(File.dirname(@template_file) + '/' + properties['TemplateURL'].sub(/^\s*.\//, ''), @region, @package, @global_config, @s3_client, @sts_client)
66
+ v['Properties']['TemplateURL'] = tp.upload_template_file
67
+ end
68
+ when 'AWS::Lambda::Function' then
69
+ if properties['Code'].kind_of?(String)
70
+ result = upload_zip_file(File.dirname(@template_file) + '/' + properties['Code'].sub(/^\s*.\//, ''))
71
+ v['Properties']['Code'] = {
72
+ 'S3Bucket' => result['bucket'],
73
+ 'S3Key' => result['key']
74
+ }
75
+ end
76
+ when 'AWS::Serverless::Function' then
77
+ if properties['CodeUri'].kind_of?(String)
78
+ result = upload_zip_file(File.dirname(@template_file) + '/' + properties['CodeUri'].sub(/^\s*.\//, ''))
79
+ v['Properties']['CodeUri'] = {
80
+ 'Bucket' => result['bucket'],
81
+ 'Key' => result['key']
82
+ }
83
+ end
84
+ when 'AWS::Serverless::Api' then
85
+ if properties['DefinitionUri'].kind_of?(String)
86
+ result = upload_file(File.dirname(@template_file) + '/' + properties['DefinitionUri'].sub(/^\s*.\//, ''))
87
+ v['Properties']['DefinitionUri'] = {
88
+ 'Bucket' => result['bucket'],
89
+ 'Key' => result['key']
90
+ }
91
+ end
92
+ when 'AWS::ApiGateway::RestApi' then
93
+ if properties['BodyS3Location'].kind_of?(String)
94
+ result = upload_file(File.dirname(@template_file) + '/' + properties['BodyS3Location'].sub(/^\s*.\//, ''))
95
+ v['Properties']['BodyS3Location'] = {
96
+ 'Bucket' => result['bucket'],
97
+ 'Key' => result['key']
98
+ }
99
+ end
100
+ end
101
+ ## TODO support resources
102
+ # * AWS::AppSync::GraphQLSchema DefinitionS3Location
103
+ # * AWS::AppSync::Resolver RequestMappingTemplateS3Location
104
+ # * AWS::AppSync::Resolver ResponseMappingTemplateS3Location
105
+ # * AWS::ElasticBeanstalk::ApplicationVersion SourceBundle
106
+ # * AWS::Glue::Job Command ScriptLocation
107
+ # * AWS::Include Location
108
+ end
109
+ end
110
+
111
+ if is_json?(orgTemplate)
112
+ @template_body = JSON.dump(data)
113
+ else
114
+ @template_body = YAML.dump_stream(data).gsub(/____CFNDK!____/, '!')
115
+ end
116
+ CFnDK.logger.info('Template Packager diff: ' + @template_file)
117
+ CFnDK.logger.info(CFnDK.diff(orgTemplate, @template_body).to_s)
118
+ CFnDK.logger.debug('Package Template size: ' + @template_body.size.to_s)
119
+ CFnDK.logger.debug('Package Template:' + @template_body)
120
+ end
121
+ @template_body
122
+ end
123
+
124
+ private
125
+
126
+ def upload_zip_file(path)
127
+ create_bucket
128
+ key = [@global_config.s3_template_hash, path.sub(/^.\//, '') + ".zip"].compact.join('/')
129
+
130
+
131
+ buffer = Zip::OutputStream.write_buffer do |out|
132
+ Dir.glob(path + '/**/*') do |file|
133
+ if (!File.directory?(file))
134
+ out.put_next_entry(file.delete_prefix(path + '/'))
135
+ out.write(File.open(file, 'r').read)
136
+ end
137
+ end
138
+ end
139
+
140
+ @s3_client.put_object(
141
+ body: buffer.string,
142
+ bucket: bucket_name,
143
+ key: key
144
+ )
145
+ url = "https://s3.amazonaws.com/#{bucket_name}/#{key}"
146
+ CFnDK.logger.info('Put S3 object: ' + url)
147
+ {
148
+ 'bucket' => bucket_name,
149
+ 'key' => key
150
+ }
151
+ end
152
+
153
+ def upload_file(path)
154
+ create_bucket
155
+ key = [@global_config.s3_template_hash, path.sub(/^.\//, '')].compact.join('/')
156
+
157
+ @s3_client.put_object(
158
+ body: File.open(path, 'r').read,
159
+ bucket: bucket_name,
160
+ key: key
161
+ )
162
+ url = "https://s3.amazonaws.com/#{bucket_name}/#{key}"
163
+ CFnDK.logger.info('Put S3 object: ' + url)
164
+ {
165
+ 'bucket' => bucket_name,
166
+ 'key' => key
167
+ }
168
+ end
169
+
170
+ def create_bucket
171
+ begin
172
+ @s3_client.head_bucket(bucket: bucket_name)
173
+ rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::Forbidden
174
+ @s3_client.create_bucket(bucket: bucket_name)
175
+ CFnDK.logger.info('Creatt S3 bucket: ' + bucket_name)
176
+ @s3_client.put_bucket_lifecycle_configuration(
177
+ bucket: bucket_name,
178
+ lifecycle_configuration: {
179
+ rules: [
180
+ {
181
+ expiration: {
182
+ days: 1,
183
+ },
184
+ status: 'Enabled',
185
+ id: 'Delete Old Files',
186
+ prefix: '',
187
+ abort_incomplete_multipart_upload: {
188
+ days_after_initiation: 1,
189
+ },
190
+ },
191
+ ],
192
+ }
193
+ )
194
+ end
195
+ end
196
+
197
+ def bucket_name
198
+ resp = @sts_client.get_caller_identity({})
199
+ resp.account + '-' + @region + '-' + @global_config.s3_template_bucket
200
+ end
201
+
202
+ def is_json?(str)
203
+ begin
204
+ !!JSON.parse(str)
205
+ rescue
206
+ false
207
+ end
208
+ end
209
+ end
210
+ end