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 +4 -4
- data/README.md +13 -4
- data/data/sumomo/custom_resources/TempS3Bucket.js +34 -1
- data/exe/sumomo +68 -57
- data/lib/sumomo/version.rb +1 -1
- data/lib/sumomo.rb +204 -12
- data/sumomo.gemspec +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f4f67797a004db69b413bc1abd72dd8d26a289847a1ec6943d4b84d3ec2e034
|
4
|
+
data.tar.gz: b28799dfccbebbcb03f3a96af34f383bd94d2ec979c405e354ea36164f11ab88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
43
|
+
$ sumomo create mystack
|
44
44
|
|
45
45
|
To view a stack's outputs
|
46
46
|
|
47
|
-
|
47
|
+
$ sumomo outputs mystack
|
48
48
|
|
49
49
|
To update a stack
|
50
50
|
|
51
|
-
|
51
|
+
$ sumomo update mystack
|
52
52
|
|
53
53
|
To delete a stack
|
54
54
|
|
55
|
-
|
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
|
-
|
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 '
|
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 =
|
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]
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
92
|
+
unless %w[show diff].include?(cmd)
|
93
|
+
Sumomo.wait_for_stack(name: ARGV[0], region: global_opts[:region])
|
94
|
+
end
|
data/lib/sumomo/version.rb
CHANGED
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.
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
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
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.
|
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-
|
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:
|
154
|
+
name: optimist
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - ">="
|