sumomo 0.9.0 → 0.10.1

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 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
  - - ">="