sumomo 0.9.0 → 0.10.0
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 +7 -14
- data/lib/sumomo/version.rb +1 -1
- data/lib/sumomo.rb +191 -13
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 346a647def575daedb5dc3e3eba04a73024c441be3ae92740cf9edb668992e44
|
|
4
|
+
data.tar.gz: 379cbf589f7ace6b8da717054ab71754604ed6b863808aadf3b6996a18df292f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0cd59b9b12bf2ec4e1b968f37962c505e9c9e6b7e47ed8846ad7af3737c162f5947bd4e01bfa1cdddfd4d347c378803db10c25ceadf56c5d7d31e0cecbe6b26
|
|
7
|
+
data.tar.gz: ed8757d5ca20a22791c1ded35a453ffd00f51260e6f5a96fb38adc9cc55df4617d30dbfa4a83bad353d2758bdc1232f51183e1bdbf09591c6142f6e92d98ff84
|
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
|
@@ -5,12 +5,12 @@ require 'trollop'
|
|
|
5
5
|
require 'sumomo'
|
|
6
6
|
require 'yaml'
|
|
7
7
|
|
|
8
|
-
SUB_COMMANDS = %w[delete create update outputs testapi].freeze
|
|
8
|
+
SUB_COMMANDS = %w[delete create update outputs show diff testapi].freeze
|
|
9
9
|
global_opts = Trollop.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'
|
|
@@ -30,21 +30,12 @@ cmd_opts = case cmd
|
|
|
30
30
|
when 'delete'
|
|
31
31
|
Sumomo.delete_stack(name: ARGV[0], region: global_opts[:region])
|
|
32
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'
|
|
33
|
+
when 'create', 'update', 'show', 'diff'
|
|
43
34
|
local_opts = Trollop.options do
|
|
44
35
|
opt :filename, 'File that describes the stack', type: :string, default: 'Sumomofile'
|
|
45
36
|
opt :changeset, 'Create a changeset instead of directly update', type: :boolean, default: false
|
|
46
37
|
end
|
|
47
|
-
Sumomo.
|
|
38
|
+
Sumomo.manage_stack(name: ARGV[0], cmd: cmd, changeset: !!local_opts[:changeset], region: global_opts[:region]) do
|
|
48
39
|
proc = proc {}
|
|
49
40
|
eval File.read(local_opts[:filename]), proc.binding, local_opts[:filename]
|
|
50
41
|
end
|
|
@@ -80,4 +71,6 @@ cmd_opts = case cmd
|
|
|
80
71
|
Trollop.die "Unknown subcommand #{cmd.inspect}"
|
|
81
72
|
end
|
|
82
73
|
|
|
83
|
-
|
|
74
|
+
unless %w[show diff].include?(cmd)
|
|
75
|
+
Sumomo.wait_for_stack(name: ARGV[0], region: global_opts[:region])
|
|
76
|
+
end
|
data/lib/sumomo/version.rb
CHANGED
data/lib/sumomo.rb
CHANGED
|
@@ -26,17 +26,7 @@ module Sumomo
|
|
|
26
26
|
"cloudformation/#{make_master_key_name(name: name)}.pem"
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def self.
|
|
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
|
|
38
|
-
|
|
39
|
-
def self.update_stack(name:, region:, sns_arn: nil, changeset: false, &block)
|
|
29
|
+
def self.manage_stack(name:, region:, cmd:'update', sns_arn: nil, changeset: false, &block)
|
|
40
30
|
cf = Aws::CloudFormation::Client.new(region: region)
|
|
41
31
|
s3 = Aws::S3::Client.new(region: region)
|
|
42
32
|
ec2 = Aws::EC2::Client.new(region: region)
|
|
@@ -107,9 +97,66 @@ module Sumomo
|
|
|
107
97
|
hidden_values = @hidden_values
|
|
108
98
|
end.templatize
|
|
109
99
|
|
|
110
|
-
|
|
100
|
+
templatedata = JSON.parse(template)
|
|
101
|
+
|
|
102
|
+
if cmd == 'show'
|
|
103
|
+
puts templatedata.to_yaml
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if cmd == 'diff'
|
|
108
|
+
store.set_raw('cloudformation/temporary_template', template)
|
|
109
|
+
update_options = {
|
|
110
|
+
stack_name: name,
|
|
111
|
+
template_url: store.url('cloudformation/temporary_template'),
|
|
112
|
+
parameters: hidden_values,
|
|
113
|
+
capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM']
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
change_set_name = "CanaryChange#{curtimestr}"
|
|
117
|
+
|
|
118
|
+
puts "Create change set"
|
|
119
|
+
|
|
120
|
+
cf.create_change_set(
|
|
121
|
+
**update_options,
|
|
122
|
+
change_set_name: change_set_name
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
resp = nil
|
|
126
|
+
|
|
127
|
+
puts "Wait for change set"
|
|
128
|
+
|
|
129
|
+
loop do
|
|
130
|
+
sleep 5
|
|
131
|
+
resp = cf.describe_change_set({
|
|
132
|
+
change_set_name: change_set_name,
|
|
133
|
+
stack_name: name
|
|
134
|
+
})
|
|
135
|
+
puts "#{change_set_name} #{resp.status}"
|
|
136
|
+
|
|
137
|
+
break unless resp.status.end_with? 'IN_PROGRESS'
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
puts "Cleaning up..."
|
|
141
|
+
|
|
142
|
+
cf.delete_change_set({
|
|
143
|
+
change_set_name: change_set_name,
|
|
144
|
+
stack_name: name,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
puts 'Change list'
|
|
148
|
+
|
|
149
|
+
clist = process_changeset_response(resp)
|
|
150
|
+
drifts = get_drifts(templatedata, cf, name)
|
|
111
151
|
|
|
112
|
-
|
|
152
|
+
# puts clist.to_yaml
|
|
153
|
+
# puts drifts.to_yaml
|
|
154
|
+
puts humanize_changeset_response(clist, drifts, templatedata).to_yaml
|
|
155
|
+
|
|
156
|
+
return
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# TODO: if the template is too big, split it into nested templates
|
|
113
160
|
|
|
114
161
|
store.set_raw('cloudformation/template', template)
|
|
115
162
|
|
|
@@ -144,6 +191,137 @@ module Sumomo
|
|
|
144
191
|
end
|
|
145
192
|
end
|
|
146
193
|
|
|
194
|
+
def self.get_drifts(templatedata, cf, stack_name)
|
|
195
|
+
drifts = {}
|
|
196
|
+
|
|
197
|
+
templatedata['Resources'].each do |k,info|
|
|
198
|
+
puts "detect drift for #{k}"
|
|
199
|
+
|
|
200
|
+
rd = cf.detect_stack_resource_drift({
|
|
201
|
+
stack_name: stack_name, # required
|
|
202
|
+
logical_resource_id: k, # required
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
diffinfo = {}
|
|
206
|
+
differences = []
|
|
207
|
+
|
|
208
|
+
rd.stack_resource_drift.property_differences.each do |pd|
|
|
209
|
+
res = {}
|
|
210
|
+
|
|
211
|
+
%w[property_path expected_value actual_value difference_type].each do |att|
|
|
212
|
+
res[att] = pd.send(att.to_sym)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
differences << res
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
%w[resource_type expected_properties actual_properties stack_resource_drift_status].each do |key|
|
|
219
|
+
diffinfo[key] = rd.stack_resource_drift.send(key.to_sym)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
diffinfo['differences'] = differences
|
|
223
|
+
|
|
224
|
+
drifts[k] = diffinfo
|
|
225
|
+
|
|
226
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
|
227
|
+
drifts[k] = {
|
|
228
|
+
"exception" => e.message
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
drifts
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def self.process_changeset_response(resp)
|
|
237
|
+
result = {}
|
|
238
|
+
|
|
239
|
+
resp.changes.each do |change|
|
|
240
|
+
info = {}
|
|
241
|
+
|
|
242
|
+
%w[action physical_resource_id resource_type replacement].each do |k|
|
|
243
|
+
info[k] = change.resource_change.send(k.to_sym)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
details = []
|
|
247
|
+
|
|
248
|
+
change.resource_change.details.each do |detail|
|
|
249
|
+
|
|
250
|
+
detailinfo = {}
|
|
251
|
+
|
|
252
|
+
%w[attribute name requires_recreation].each do |k|
|
|
253
|
+
detailinfo[k] = detail.target.send(k.to_sym)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
%w[evaluation change_source causing_entity].each do |k|
|
|
257
|
+
detailinfo[k] = detail.send(k.to_sym)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
details << detailinfo
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
info['details'] = details
|
|
264
|
+
|
|
265
|
+
result[change.resource_change.logical_resource_id] = info
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
result
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def self.humanize_changeset_response(clist, drifts, templatedata)
|
|
272
|
+
# refs
|
|
273
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CloudFormation/Types/ResourceChange.html
|
|
274
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CloudFormation/Types/ResourceTargetDefinition.html
|
|
275
|
+
|
|
276
|
+
result = {}
|
|
277
|
+
|
|
278
|
+
clist.each do |k,info|
|
|
279
|
+
|
|
280
|
+
if info['action'] == 'Modify'
|
|
281
|
+
|
|
282
|
+
z = if info['replacement'] != 'False'
|
|
283
|
+
result['Replace'] ||= {}
|
|
284
|
+
else
|
|
285
|
+
result['Modify'] ||= {}
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
z[k] = {}
|
|
289
|
+
|
|
290
|
+
info['details'].each do |detail|
|
|
291
|
+
|
|
292
|
+
propname = detail['attribute']
|
|
293
|
+
if detail['attribute'] == 'Properties'
|
|
294
|
+
propname = detail['name']
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
fr = templatedata['Resources'][k]
|
|
298
|
+
tr = drifts[k]
|
|
299
|
+
|
|
300
|
+
fromprops = fr['Properties']
|
|
301
|
+
toprops = {}
|
|
302
|
+
|
|
303
|
+
if tr['actual_properties']
|
|
304
|
+
toprops = JSON.parse(tr['actual_properties'])
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
z[k][propname] = {
|
|
309
|
+
'From' => toprops[propname],
|
|
310
|
+
'To' => fromprops[propname]
|
|
311
|
+
}
|
|
312
|
+
end
|
|
313
|
+
else
|
|
314
|
+
z = result[info['action']] ||= {}
|
|
315
|
+
|
|
316
|
+
z[k] = {}
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
result
|
|
322
|
+
|
|
323
|
+
end
|
|
324
|
+
|
|
147
325
|
def self.curtimestr
|
|
148
326
|
Time.now.strftime('%Y%m%d%H%M%S')
|
|
149
327
|
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.
|
|
4
|
+
version: 0.10.0
|
|
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-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|