sumomo 0.9.0 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '073782fdfbe69977c7593b76415797b035764772302b35c50ccf69533324c875'
4
- data.tar.gz: 1efbe5244de1bf4cd3c8ff6faf18a3b3bbda9b4137297c1ba6d1349553ea60a9
3
+ metadata.gz: 3f4f67797a004db69b413bc1abd72dd8d26a289847a1ec6943d4b84d3ec2e034
4
+ data.tar.gz: b28799dfccbebbcb03f3a96af34f383bd94d2ec979c405e354ea36164f11ab88
5
5
  SHA512:
6
- metadata.gz: 9ee43afb24863f75a9d421d116e696e410ad90c2cdb15e1bd353c05bcde3d6ef08a69055ede03b3ad62f87c0483196a643029839afe255266292d7a5371957b1
7
- data.tar.gz: 92f385cc371f99e5201540d9fecc75f6a66ced34b75d3c397bbc85706c1ab00d72e25651d56d30402549994ad4fb6f3b6d4f03df9edcfb0f4c794af036d9f5f9
6
+ metadata.gz: e4ca377095ae8386d059a35f32efc6c7f57696dd1457301beeee36e6734d437c1cb1b09f1ff4263f9d3d01d517d6bc0aa42e9377c9c0a653e02325e2c7bf6e7c
7
+ data.tar.gz: 131bf090f8161b6ebe07ddd0eefb304e34426a7aca4c19eb4215111d9b61641977a73ba27baac471f81c6abc6fa6a908bb3438b5c06ca9230a209a3918ae5bd4
data/README.md CHANGED
@@ -40,19 +40,28 @@ output "Haha", x
40
40
 
41
41
  To create a stack that acquires an IP Address and outputs it
42
42
 
43
- $ sumomo create mystack
43
+ $ sumomo create mystack
44
44
 
45
45
  To view a stack's outputs
46
46
 
47
- $ sumomo outputs mystack
47
+ $ sumomo outputs mystack
48
48
 
49
49
  To update a stack
50
50
 
51
- $ sumomo update mystack
51
+ $ sumomo update mystack
52
52
 
53
53
  To delete a stack
54
54
 
55
- $ sumomo delete mystack
55
+ $ sumomo delete mystack
56
+
57
+ To view what changes your stuff will cause to your stack
58
+
59
+ $ sumomo diff mystack
60
+
61
+ To see the raw cloudformation template
62
+
63
+ $ sumomo show mystack
64
+
56
65
 
57
66
  ## Library Usage
58
67
 
@@ -2,6 +2,7 @@ var s3 = new aws.S3();
2
2
  var name = request.ResourceProperties.BucketName;
3
3
  var copy_from_dir = request.ResourceProperties.CopyFromDirectory;
4
4
  var copy_from_bucket = request.ResourceProperties.CopyFromBucket;
5
+ var disable_acl = request.ResourceProperties.DisableACL;
5
6
  var success_obj = {
6
7
  Arn: "arn:aws:s3:::" + name,
7
8
  DomainName: name + ".s3-" + request.ResourceProperties.Region + ".amazonaws.com"
@@ -72,6 +73,38 @@ function copy_files(success, fail)
72
73
  );
73
74
  }
74
75
 
76
+ function apply_ownership_policy(name, success, fail)
77
+ {
78
+ if (disable_acl)
79
+ {
80
+ // ACL is disabled by default, so we simply skip this step.
81
+ return copy_files(success, fail);
82
+ }
83
+
84
+ var params = {
85
+ Bucket: name,
86
+ OwnershipControls: {
87
+ Rules: [
88
+ {
89
+ ObjectOwnership: "BucketOwnerPreferred"
90
+ }
91
+ ]
92
+ }
93
+ };
94
+
95
+ s3.putBucketOwnershipControls(params, function(err, data)
96
+ {
97
+ if (err)
98
+ {
99
+ fail(err);
100
+ }
101
+ else
102
+ {
103
+ copy_files(success, fail);
104
+ }
105
+ });
106
+ }
107
+
75
108
  function create_bucket(name, success, fail)
76
109
  {
77
110
  var create_params = {
@@ -89,7 +122,7 @@ function create_bucket(name, success, fail)
89
122
  }
90
123
  else
91
124
  {
92
- copy_files(success, fail);
125
+ apply_ownership_policy(name, success, fail);
93
126
  }
94
127
  });
95
128
  }
data/exe/sumomo CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'trollop'
4
+ require 'optimist'
5
5
  require 'sumomo'
6
6
  require 'yaml'
7
7
 
8
- SUB_COMMANDS = %w[delete create update outputs testapi].freeze
9
- global_opts = Trollop.options do
8
+ SUB_COMMANDS = %w[delete create update outputs show diff testapi].freeze
9
+ global_opts = Optimist.options do
10
10
  banner <<-USAGE
11
11
  Sumomo v#{Sumomo::VERSION}
12
12
 
13
- Usage: sumomo [options] <create|update|delete|outputs> <stackname>
13
+ Usage: sumomo [options] <#{SUB_COMMANDS.join('|')}> <stackname>
14
14
  USAGE
15
15
 
16
16
  opt :region, 'AWS region to use', type: :string, default: 'ap-northeast-1'
@@ -26,58 +26,69 @@ p ENV['AWS_PROFILE']
26
26
 
27
27
  cmd = ARGV.shift # get the subcommand
28
28
 
29
- cmd_opts = case cmd
30
- when 'delete'
31
- Sumomo.delete_stack(name: ARGV[0], region: global_opts[:region])
32
-
33
- when 'create'
34
- local_opts = Trollop.options do
35
- opt :filename, 'File that describes the stack', type: :string, default: 'Sumomofile'
36
- end
37
- Sumomo.create_stack(name: ARGV[0], region: global_opts[:region]) do
38
- proc = proc {}
39
- eval File.read(local_opts[:filename]), proc.binding, local_opts[:filename]
40
- end
41
-
42
- when 'update'
43
- local_opts = Trollop.options do
44
- opt :filename, 'File that describes the stack', type: :string, default: 'Sumomofile'
45
- opt :changeset, 'Create a changeset instead of directly update', type: :boolean, default: false
46
- end
47
- Sumomo.update_stack(name: ARGV[0], changeset: !!local_opts[:changeset], region: global_opts[:region]) do
48
- proc = proc {}
49
- eval File.read(local_opts[:filename]), proc.binding, local_opts[:filename]
50
- end
51
-
52
- when 'outputs'
53
- puts "Outputs for stack #{ARGV[0]}"
54
- puts Sumomo.get_stack_outputs(name: ARGV[0], region: global_opts[:region]).to_yaml
55
-
56
- when 'login'
57
- puts "Login to stack #{ARGV[0]} instance at #{ARGV[1]}"
58
- `aws s3 cp s3://#{ARGV[0]}/cloudformation/#{ARGV[0]}_master_key.pem x.txt`
59
- key = JSON.parse(File.read('x.txt'))['value'].
60
- gsub('-----BEGIN RSA PRIVATE KEY----- ', "-----BEGIN RSA PRIVATE KEY-----\n").
61
- gsub(' -----END RSA PRIVATE KEY-----', "\n-----END RSA PRIVATE KEY-----").
62
- gsub(/(.{64}) /, "\\1\n")
63
- File.write('key.pem', key)
64
- `chmod 0600 key.pem`
65
- exec "ssh -i 'key.pem' ec2-user@#{ARGV[1]} #{ARGV[2]}"
66
-
67
- when 'testapi'
68
- local_opts = Trollop.options do
69
- opt :filename, 'File that describes the stack', type: :string, default: 'Sumomofile'
70
- opt :apiname, 'Name of the API you want to test', type: :string
71
- opt :prettyprint, 'Test API outputs JSON with nice indentation', type: :boolean, default: true
72
- end
73
- puts 'API Test Mode'
74
- Sumomo.test_api(local_opts[:apiname], local_opts[:prettyprint]) do
75
- proc = proc {}
76
- eval File.read(local_opts[:filename]), proc.binding, local_opts[:filename]
77
- end
78
- exit(0)
79
- else
80
- Trollop.die "Unknown subcommand #{cmd.inspect}"
29
+ case cmd
30
+ when 'delete'
31
+ Sumomo.delete_stack(name: ARGV[0], region: global_opts[:region])
32
+
33
+ when 'rollback'
34
+ Sumomo.rollback_stack(name: ARGV[0], region: global_opts[:region])
35
+
36
+ when 'create', 'update', 'show', 'diff'
37
+ local_opts = Optimist.options do
38
+ opt :filename, 'File that describes the stack', type: :string, default: 'Sumomofile'
39
+ opt :changeset, 'Create a changeset instead of directly update', type: :boolean, default: false
40
+ opt :rollback, 'Specify whether or not to rollback. Allowed values are [enable, disable]', type: :string, default: 'unspecified'
41
+ end
42
+
43
+ changeset = !!local_opts[:changeset]
44
+ rollback = local_opts[:rollback].to_sym
45
+
46
+ if rollback == :disable && local_opts[:changeset] == false
47
+ Optimist.die "Rollback cannot be set to disable when changeset is set to false, because we will end up in an update-failed state"
48
+ end
49
+
50
+ Sumomo.manage_stack(
51
+ name: ARGV[0],
52
+ cmd: cmd,
53
+ changeset: changeset,
54
+ region: global_opts[:region],
55
+ rollback: rollback) do
56
+
57
+ proc = proc {}
58
+ eval File.read(local_opts[:filename]), proc.binding, local_opts[:filename]
59
+ end
60
+
61
+ when 'outputs'
62
+ puts "Outputs for stack #{ARGV[0]}"
63
+ puts Sumomo.get_stack_outputs(name: ARGV[0], region: global_opts[:region]).to_yaml
64
+
65
+ when 'login'
66
+ puts "Login to stack #{ARGV[0]} instance at #{ARGV[1]}"
67
+ `aws s3 cp s3://#{ARGV[0]}/cloudformation/#{ARGV[0]}_master_key.pem x.txt`
68
+ key = JSON.parse(File.read('x.txt'))['value'].
69
+ gsub('-----BEGIN RSA PRIVATE KEY----- ', "-----BEGIN RSA PRIVATE KEY-----\n").
70
+ gsub(' -----END RSA PRIVATE KEY-----', "\n-----END RSA PRIVATE KEY-----").
71
+ gsub(/(.{64}) /, "\\1\n")
72
+ File.write('key.pem', key)
73
+ `chmod 0600 key.pem`
74
+ exec "ssh -i 'key.pem' ec2-user@#{ARGV[1]} #{ARGV[2]}"
75
+
76
+ when 'testapi'
77
+ local_opts = Optimist.options do
78
+ opt :filename, 'File that describes the stack', type: :string, default: 'Sumomofile'
79
+ opt :apiname, 'Name of the API you want to test', type: :string
80
+ opt :prettyprint, 'Test API outputs JSON with nice indentation', type: :boolean, default: true
81
+ end
82
+ puts 'API Test Mode'
83
+ Sumomo.test_api(local_opts[:apiname], local_opts[:prettyprint]) do
84
+ proc = proc {}
85
+ eval File.read(local_opts[:filename]), proc.binding, local_opts[:filename]
86
+ end
87
+ exit(0)
88
+ else
89
+ Optimist.die "Unknown subcommand #{cmd.inspect}"
81
90
  end
82
91
 
83
- Sumomo.wait_for_stack(name: ARGV[0], region: global_opts[:region])
92
+ unless %w[show diff].include?(cmd)
93
+ Sumomo.wait_for_stack(name: ARGV[0], region: global_opts[:region])
94
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sumomo
4
- VERSION = '0.9.0'
4
+ VERSION = '0.10.1'
5
5
  end
data/lib/sumomo.rb CHANGED
@@ -26,17 +26,14 @@ module Sumomo
26
26
  "cloudformation/#{make_master_key_name(name: name)}.pem"
27
27
  end
28
28
 
29
- def self.create_stack(name:, region:, sns_arn: nil, &block)
30
- cf = Aws::CloudFormation::Client.new(region: region)
31
- begin
32
- cf.describe_stacks(stack_name: name)
33
- raise "There is already a stack named '#{name}'"
34
- rescue Aws::CloudFormation::Errors::ValidationError
35
- update_stack(name: name, region: region, sns_arn: sns_arn, &block)
36
- end
37
- end
29
+ def self.manage_stack(
30
+ name:,
31
+ region:,
32
+ cmd:'update',
33
+ sns_arn: nil,
34
+ rollback: :unspecified,
35
+ changeset: false, &block)
38
36
 
39
- def self.update_stack(name:, region:, sns_arn: nil, changeset: false, &block)
40
37
  cf = Aws::CloudFormation::Client.new(region: region)
41
38
  s3 = Aws::S3::Client.new(region: region)
42
39
  ec2 = Aws::EC2::Client.new(region: region)
@@ -107,9 +104,66 @@ module Sumomo
107
104
  hidden_values = @hidden_values
108
105
  end.templatize
109
106
 
110
- # TODO: if the template is too big, split it into nested templates
107
+ templatedata = JSON.parse(template)
108
+
109
+ if cmd == 'show'
110
+ puts templatedata.to_yaml
111
+ return
112
+ end
113
+
114
+ if cmd == 'diff'
115
+ store.set_raw('cloudformation/temporary_template', template)
116
+ update_options = {
117
+ stack_name: name,
118
+ template_url: store.url('cloudformation/temporary_template'),
119
+ parameters: hidden_values,
120
+ capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM']
121
+ }
122
+
123
+ change_set_name = "CanaryChange#{curtimestr}"
124
+
125
+ puts "Create change set"
126
+
127
+ cf.create_change_set(
128
+ **update_options,
129
+ change_set_name: change_set_name
130
+ )
131
+
132
+ resp = nil
133
+
134
+ puts "Wait for change set"
135
+
136
+ loop do
137
+ sleep 5
138
+ resp = cf.describe_change_set({
139
+ change_set_name: change_set_name,
140
+ stack_name: name
141
+ })
142
+ puts "#{change_set_name} #{resp.status}"
143
+
144
+ break unless resp.status.end_with? 'IN_PROGRESS'
145
+ end
146
+
147
+ puts "Cleaning up..."
148
+
149
+ cf.delete_change_set({
150
+ change_set_name: change_set_name,
151
+ stack_name: name,
152
+ })
153
+
154
+ puts 'Change list'
155
+
156
+ clist = process_changeset_response(resp)
157
+ drifts = get_drifts(templatedata, cf, name)
158
+
159
+ # puts clist.to_yaml
160
+ # puts drifts.to_yaml
161
+ puts humanize_changeset_response(clist, drifts, templatedata).to_yaml
111
162
 
112
- # puts JSON.parse(template).to_yaml
163
+ return
164
+ end
165
+
166
+ # TODO: if the template is too big, split it into nested templates
113
167
 
114
168
  store.set_raw('cloudformation/template', template)
115
169
 
@@ -117,6 +171,7 @@ module Sumomo
117
171
  stack_name: name,
118
172
  template_url: store.url('cloudformation/template'),
119
173
  parameters: hidden_values,
174
+ disable_rollback: rollback == :disable,
120
175
  capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM']
121
176
  }
122
177
 
@@ -144,6 +199,137 @@ module Sumomo
144
199
  end
145
200
  end
146
201
 
202
+ def self.get_drifts(templatedata, cf, stack_name)
203
+ drifts = {}
204
+
205
+ templatedata['Resources'].each do |k,info|
206
+ puts "detect drift for #{k}"
207
+
208
+ rd = cf.detect_stack_resource_drift({
209
+ stack_name: stack_name, # required
210
+ logical_resource_id: k, # required
211
+ })
212
+
213
+ diffinfo = {}
214
+ differences = []
215
+
216
+ rd.stack_resource_drift.property_differences.each do |pd|
217
+ res = {}
218
+
219
+ %w[property_path expected_value actual_value difference_type].each do |att|
220
+ res[att] = pd.send(att.to_sym)
221
+ end
222
+
223
+ differences << res
224
+ end
225
+
226
+ %w[resource_type expected_properties actual_properties stack_resource_drift_status].each do |key|
227
+ diffinfo[key] = rd.stack_resource_drift.send(key.to_sym)
228
+ end
229
+
230
+ diffinfo['differences'] = differences
231
+
232
+ drifts[k] = diffinfo
233
+
234
+ rescue Aws::CloudFormation::Errors::ValidationError => e
235
+ drifts[k] = {
236
+ "exception" => e.message
237
+ }
238
+
239
+ end
240
+
241
+ drifts
242
+ end
243
+
244
+ def self.process_changeset_response(resp)
245
+ result = {}
246
+
247
+ resp.changes.each do |change|
248
+ info = {}
249
+
250
+ %w[action physical_resource_id resource_type replacement].each do |k|
251
+ info[k] = change.resource_change.send(k.to_sym)
252
+ end
253
+
254
+ details = []
255
+
256
+ change.resource_change.details.each do |detail|
257
+
258
+ detailinfo = {}
259
+
260
+ %w[attribute name requires_recreation].each do |k|
261
+ detailinfo[k] = detail.target.send(k.to_sym)
262
+ end
263
+
264
+ %w[evaluation change_source causing_entity].each do |k|
265
+ detailinfo[k] = detail.send(k.to_sym)
266
+ end
267
+
268
+ details << detailinfo
269
+ end
270
+
271
+ info['details'] = details
272
+
273
+ result[change.resource_change.logical_resource_id] = info
274
+ end
275
+
276
+ result
277
+ end
278
+
279
+ def self.humanize_changeset_response(clist, drifts, templatedata)
280
+ # refs
281
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CloudFormation/Types/ResourceChange.html
282
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CloudFormation/Types/ResourceTargetDefinition.html
283
+
284
+ result = {}
285
+
286
+ clist.each do |k,info|
287
+
288
+ if info['action'] == 'Modify'
289
+
290
+ z = if info['replacement'] != 'False'
291
+ result['Replace'] ||= {}
292
+ else
293
+ result['Modify'] ||= {}
294
+ end
295
+
296
+ z[k] = {}
297
+
298
+ info['details'].each do |detail|
299
+
300
+ propname = detail['attribute']
301
+ if detail['attribute'] == 'Properties'
302
+ propname = detail['name']
303
+ end
304
+
305
+ fr = templatedata['Resources'][k]
306
+ tr = drifts[k]
307
+
308
+ fromprops = fr['Properties']
309
+ toprops = {}
310
+
311
+ if tr['actual_properties']
312
+ toprops = JSON.parse(tr['actual_properties'])
313
+ end
314
+
315
+
316
+ z[k][propname] = {
317
+ 'From' => toprops[propname],
318
+ 'To' => fromprops[propname]
319
+ }
320
+ end
321
+ else
322
+ z = result[info['action']] ||= {}
323
+
324
+ z[k] = {}
325
+ end
326
+
327
+ end
328
+
329
+ result
330
+
331
+ end
332
+
147
333
  def self.curtimestr
148
334
  Time.now.strftime('%Y%m%d%H%M%S')
149
335
  end
@@ -270,6 +456,12 @@ module Sumomo
270
456
  end
271
457
  end
272
458
 
459
+ def self.rollback_stack(name:, region:)
460
+ cf = Aws::CloudFormation::Client.new(region: region)
461
+
462
+ cf.rollback_stack(stack_name: name)
463
+ end
464
+
273
465
  def self.get_stack_outputs(name:, region:)
274
466
  cf = Aws::CloudFormation::Client.new(region: region)
275
467
 
data/sumomo.gemspec CHANGED
@@ -38,6 +38,6 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency 'momo', '0.4.1'
39
39
  spec.add_dependency 'rubyzip'
40
40
  spec.add_dependency 's3cabinet'
41
- spec.add_dependency 'trollop'
41
+ spec.add_dependency 'optimist'
42
42
  spec.add_dependency 'webrick'
43
43
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sumomo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Siaw
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-27 00:00:00.000000000 Z
11
+ date: 2023-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -151,7 +151,7 @@ dependencies:
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
- name: trollop
154
+ name: optimist
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - ">="