cloudformation-tool 0.1.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 +7 -0
- data/LICENSE +339 -0
- data/README.md +204 -0
- data/bin/cftool +9 -0
- data/lib/cloud_formation_tool.rb +94 -0
- data/lib/cloud_formation_tool/cli.rb +5 -0
- data/lib/cloud_formation_tool/cli/compile.rb +19 -0
- data/lib/cloud_formation_tool/cli/create.rb +50 -0
- data/lib/cloud_formation_tool/cli/delete.rb +18 -0
- data/lib/cloud_formation_tool/cli/list_stacks.rb +15 -0
- data/lib/cloud_formation_tool/cli/main.rb +31 -0
- data/lib/cloud_formation_tool/cli/monitor.rb +28 -0
- data/lib/cloud_formation_tool/cli/parameters.rb +20 -0
- data/lib/cloud_formation_tool/cli/servers.rb +30 -0
- data/lib/cloud_formation_tool/cli/status.rb +18 -0
- data/lib/cloud_formation_tool/cloud_formation.rb +153 -0
- data/lib/cloud_formation_tool/cloud_formation/lambda_code.rb +41 -0
- data/lib/cloud_formation_tool/cloud_formation/stack.rb +152 -0
- data/lib/cloud_formation_tool/cloud_init.rb +86 -0
- data/lib/cloud_formation_tool/errors.rb +11 -0
- data/lib/cloud_formation_tool/storable.rb +41 -0
- data/lib/cloud_formation_tool/version.rb +3 -0
- metadata +109 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module CloudFormationTool
|
4
|
+
class CloudFormation
|
5
|
+
|
6
|
+
class LambdaCode
|
7
|
+
include Storable
|
8
|
+
|
9
|
+
def initialize(url)
|
10
|
+
log "Downloading Lambda code from #{url}"
|
11
|
+
res = fetch(url)
|
12
|
+
|
13
|
+
@s3_url = URI(upload(make_filename(url.split('.').last), res.body, res['content-type']))
|
14
|
+
log "uploaded Lambda function to #{@s3_url}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch(uri_str, limit = 10)
|
18
|
+
raise ArgumentError, 'too many HTTP redirects' if limit == 0
|
19
|
+
response = Net::HTTP.get_response(URI(uri_str))
|
20
|
+
case response
|
21
|
+
when Net::HTTPSuccess then
|
22
|
+
response
|
23
|
+
when Net::HTTPRedirection then
|
24
|
+
location = response['location']
|
25
|
+
log "redirected to #{location}"
|
26
|
+
fetch(location, limit - 1)
|
27
|
+
else
|
28
|
+
raise AppError, "Error downloading #{url}: #{response.value}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_cloudformation
|
33
|
+
{
|
34
|
+
'S3Bucket' => @s3_url.hostname.split('.').first,
|
35
|
+
'S3Key' => @s3_url.path[1..-1]
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module CloudFormationTool
|
2
|
+
class CloudFormation
|
3
|
+
|
4
|
+
class Stack
|
5
|
+
include Enumerable
|
6
|
+
include Storable
|
7
|
+
include CloudFormationTool
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@seenev = Set.new
|
14
|
+
@watch_timeouts = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete
|
18
|
+
awscf.delete_stack stack_name: @name
|
19
|
+
end
|
20
|
+
|
21
|
+
def exist?
|
22
|
+
begin
|
23
|
+
awscf.describe_stacks stack_name: name
|
24
|
+
true
|
25
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(url, filepath, params = {})
|
31
|
+
log "Updating existing stack '#{name}' from '#{filepath}' params #{params.inspect}"
|
32
|
+
resp = awscf.update_stack({
|
33
|
+
stack_name: @name,
|
34
|
+
template_url: url,
|
35
|
+
capabilities: %w(CAPABILITY_IAM CAPABILITY_NAMED_IAM),
|
36
|
+
parameters: params.collect do |k,v|
|
37
|
+
{
|
38
|
+
parameter_key: k.to_s,
|
39
|
+
parameter_value: v.to_s,
|
40
|
+
use_previous_value: false,
|
41
|
+
}
|
42
|
+
end
|
43
|
+
})
|
44
|
+
resp.stack_id
|
45
|
+
end
|
46
|
+
|
47
|
+
def create(template, params = {})
|
48
|
+
tmpl = CloudFormation.parse(template).to_yaml
|
49
|
+
url = upload(make_filename('yaml'), tmpl)
|
50
|
+
return update(url, template, params) if exist?
|
51
|
+
log "Creating stack '#{name}' from '#{template}' params #{params.inspect}"
|
52
|
+
resp = awscf.create_stack({
|
53
|
+
stack_name: @name,
|
54
|
+
template_url: url,
|
55
|
+
capabilities: %w(CAPABILITY_IAM CAPABILITY_NAMED_IAM),
|
56
|
+
on_failure: "DO_NOTHING", ##"ROLLBACK",
|
57
|
+
parameters: params.collect do |k,v|
|
58
|
+
{
|
59
|
+
parameter_key: k.to_s,
|
60
|
+
parameter_value: v.to_s,
|
61
|
+
use_previous_value: false,
|
62
|
+
}
|
63
|
+
end
|
64
|
+
})
|
65
|
+
resp.stack_id
|
66
|
+
end
|
67
|
+
|
68
|
+
def resources
|
69
|
+
begin
|
70
|
+
resp = awscf.describe_stack_resources stack_name: @name
|
71
|
+
resp.stack_resources
|
72
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
73
|
+
raise Errors::AppError, "Failed to get resources: #{e.message}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def asgroups
|
78
|
+
resources.select do |res|
|
79
|
+
res.resource_type == 'AWS::AutoScaling::AutoScalingGroup'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def see_events
|
84
|
+
each { |e| @seenev << e.event_id }
|
85
|
+
end
|
86
|
+
|
87
|
+
def monitor(start_time = nil)
|
88
|
+
done = false
|
89
|
+
begin
|
90
|
+
until done
|
91
|
+
reverse_each do |ev|
|
92
|
+
next if @seenev.add?(ev.event_id).nil?
|
93
|
+
text = "#{ev.timestamp.strftime "%Y-%m-%d %H:%M:%S"}| " + %w(
|
94
|
+
resource_type:40
|
95
|
+
logical_resource_id:38
|
96
|
+
resource_status
|
97
|
+
).collect { |field|
|
98
|
+
(name,size) = field.split(":")
|
99
|
+
size ||= 1
|
100
|
+
ev.send(name.to_sym).ljust(size.to_i, ' ')
|
101
|
+
}.join(" ")
|
102
|
+
text += " " + ev.resource_status_reason if ev.resource_status =~ /_FAILED/
|
103
|
+
if start_time.nil? or start_time < ev.timestamp
|
104
|
+
puts text
|
105
|
+
end
|
106
|
+
done = (ev.resource_type == "AWS::CloudFormation::Stack" and ev.resource_status =~ /(_COMPLETE|_FAILED)$/)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
rescue CloudFormationTool::Errors::StackDoesNotExistError => e
|
110
|
+
puts "Stack #{name} does not exist"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def each
|
115
|
+
token = nil
|
116
|
+
sleep(if @_last_poll_time.nil?
|
117
|
+
0
|
118
|
+
else
|
119
|
+
diff = Time.now - @_last_poll_time
|
120
|
+
if diff < 1
|
121
|
+
diff
|
122
|
+
else
|
123
|
+
0
|
124
|
+
end
|
125
|
+
end)
|
126
|
+
begin
|
127
|
+
resp = awscf.describe_stack_events stack_name: name, next_token: token
|
128
|
+
@watch_timeouts = 0
|
129
|
+
resp.stack_events.each do |ev|
|
130
|
+
yield ev
|
131
|
+
end
|
132
|
+
rescue Aws::CloudFormation::Errors::Throttling => e
|
133
|
+
sleep 1
|
134
|
+
retry
|
135
|
+
rescue Seahorse::Client::NetworkingError => e # we get this when there's a timeout
|
136
|
+
if (@watch_timeouts += 1) > 5
|
137
|
+
raise AppError, "Too many timeouts!"
|
138
|
+
else
|
139
|
+
retry
|
140
|
+
end
|
141
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
142
|
+
if e.message =~ /does not exist/
|
143
|
+
raise CloudFormationTool::Errors::StackDoesNotExistError, "Stack does not exist"
|
144
|
+
else
|
145
|
+
raise e
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
module CloudFormationTool
|
4
|
+
class CloudInit
|
5
|
+
include Storable
|
6
|
+
|
7
|
+
attr_reader :path
|
8
|
+
|
9
|
+
def initialize(path)
|
10
|
+
@path = path
|
11
|
+
log "Loading #{path}"
|
12
|
+
begin
|
13
|
+
@initfile = YAML.load(File.read(path)).to_h
|
14
|
+
rescue Errno::ENOENT => e
|
15
|
+
raise AppError.new("Error reading #{@path}: " + e.message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def compile
|
20
|
+
basepath = File.dirname(path)
|
21
|
+
basepath = "." if basepath.empty?
|
22
|
+
@initfile['write_files'] = (@initfile['write_files'] || []).collect do |file|
|
23
|
+
if file['file']
|
24
|
+
begin
|
25
|
+
read_file_content(basepath + "/" + file.delete('file'), file)
|
26
|
+
rescue Errno::EISDIR => e
|
27
|
+
raise AppError, "#{path} - error loading embedded file for #{file['path']}: " + e.message
|
28
|
+
rescue Errno::ENOENT => e
|
29
|
+
raise AppError, "#{path} - error loading embedded file for #{file['path']}: " + e.message
|
30
|
+
end
|
31
|
+
else
|
32
|
+
file
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@initfile['write_files'] += (@initfile.delete('write_directories') || []).collect do |directory|
|
36
|
+
realdir = "#{basepath}/#{directory['source']}"
|
37
|
+
raise AppError.new("Cloud-init file #{path} references missing directory #{realdir}") unless File.exist? realdir
|
38
|
+
read_dir_files(realdir, directory['target'])
|
39
|
+
end.flatten
|
40
|
+
"#cloud-config\n" + @initfile.to_yaml
|
41
|
+
end
|
42
|
+
|
43
|
+
def encode
|
44
|
+
yamlout = compile
|
45
|
+
if yamlout.size > 16384 # max AWS EC2 user data size - try compressing it
|
46
|
+
yamlout = Zlib::Deflate.new(nil, 31).deflate(yamlout, Zlib::FINISH) # 31 is the magic word to have deflate create a gzip compatible header
|
47
|
+
end
|
48
|
+
if yamlout.size > 16384 # still to big, we should upload to S3 and create an include file
|
49
|
+
url = upload make_filename('init'),
|
50
|
+
yamlout, 'text/cloud-config'
|
51
|
+
log "Wrote cloud config to #{url}"
|
52
|
+
[ "#include", url ].join "\n"
|
53
|
+
else
|
54
|
+
yamlout
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_file_content(filepath, spec)
|
59
|
+
spec['encoding'] = 'base64'
|
60
|
+
spec['content'] = Base64.strict_encode64(File.read(filepath))
|
61
|
+
spec
|
62
|
+
end
|
63
|
+
|
64
|
+
def read_dir_files(source, target)
|
65
|
+
Dir.entries(source).select do |entry|
|
66
|
+
entry !~ /^\.{1,2}$/
|
67
|
+
end.collect do |entry|
|
68
|
+
path = source + "/" + entry
|
69
|
+
targetpath = target + "/" + entry
|
70
|
+
if File.directory? path
|
71
|
+
read_dir_files(path, targetpath)
|
72
|
+
else
|
73
|
+
[ read_file_content(path, {
|
74
|
+
'path' => targetpath,
|
75
|
+
'permissions' => (("%o" % File.stat(path).mode)[-4..-1])
|
76
|
+
}) ]
|
77
|
+
end
|
78
|
+
end.flatten
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_base64
|
82
|
+
Base64.encode64(encode)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module CloudFormationTool
|
4
|
+
module Storable
|
5
|
+
include CloudFormationTool
|
6
|
+
|
7
|
+
def make_filename(ext = '')
|
8
|
+
base = "#{File.basename(Dir.pwd)}-#{Time.now.strftime("%Y%m%d%H%M%S")}"
|
9
|
+
if ext.empty?
|
10
|
+
base
|
11
|
+
else
|
12
|
+
"#{base}.#{ext}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def upload(path, content, mime_type = 'text/yaml')
|
17
|
+
md5 = Digest::MD5.hexdigest content
|
18
|
+
prefix = "#{md5[0]}/#{md5[1..2]}/#{md5}"
|
19
|
+
b = Aws::S3::Bucket.new(s3_bucket_name(region), client: awss3(region))
|
20
|
+
# return early if we already have a copy of this object stored.
|
21
|
+
# if this object was previously uploaded, we use its URLs (and not, for example,
|
22
|
+
# do a local copy to the requested path) because this way cloudformation can see
|
23
|
+
# that the updated template is exactly the same as the old one and will not force
|
24
|
+
# an unneeded update.
|
25
|
+
o = b.objects(prefix: "cf-compiled/#{prefix}/").first
|
26
|
+
if o.nil?
|
27
|
+
# no such luck, we need to actually upload the file
|
28
|
+
o = b.object("cf-compiled/#{prefix}/#{path}")
|
29
|
+
o.put acl: 'public-read',
|
30
|
+
body: content,
|
31
|
+
content_disposition: 'attachment',
|
32
|
+
content_encoding: 'gzip',
|
33
|
+
content_type: mime_type,
|
34
|
+
storage_class: 'REDUCED_REDUNDANCY'
|
35
|
+
else
|
36
|
+
log "re-using cached object"
|
37
|
+
end
|
38
|
+
o.public_url
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloudformation-tool
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oded Arbel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2011-03-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: clamp
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aws-sdk
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: autoloaded
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2'
|
55
|
+
description:
|
56
|
+
email: oded.arbel@greenfieldtech.net
|
57
|
+
executables:
|
58
|
+
- cftool
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files:
|
61
|
+
- LICENSE
|
62
|
+
- README.md
|
63
|
+
files:
|
64
|
+
- LICENSE
|
65
|
+
- README.md
|
66
|
+
- bin/cftool
|
67
|
+
- lib/cloud_formation_tool.rb
|
68
|
+
- lib/cloud_formation_tool/cli.rb
|
69
|
+
- lib/cloud_formation_tool/cli/compile.rb
|
70
|
+
- lib/cloud_formation_tool/cli/create.rb
|
71
|
+
- lib/cloud_formation_tool/cli/delete.rb
|
72
|
+
- lib/cloud_formation_tool/cli/list_stacks.rb
|
73
|
+
- lib/cloud_formation_tool/cli/main.rb
|
74
|
+
- lib/cloud_formation_tool/cli/monitor.rb
|
75
|
+
- lib/cloud_formation_tool/cli/parameters.rb
|
76
|
+
- lib/cloud_formation_tool/cli/servers.rb
|
77
|
+
- lib/cloud_formation_tool/cli/status.rb
|
78
|
+
- lib/cloud_formation_tool/cloud_formation.rb
|
79
|
+
- lib/cloud_formation_tool/cloud_formation/lambda_code.rb
|
80
|
+
- lib/cloud_formation_tool/cloud_formation/stack.rb
|
81
|
+
- lib/cloud_formation_tool/cloud_init.rb
|
82
|
+
- lib/cloud_formation_tool/errors.rb
|
83
|
+
- lib/cloud_formation_tool/storable.rb
|
84
|
+
- lib/cloud_formation_tool/version.rb
|
85
|
+
homepage: http://github.com/GreenfieldTech/cloudformation-tool
|
86
|
+
licenses:
|
87
|
+
- GPL-2.0
|
88
|
+
metadata: {}
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 2.4.5
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: A pre-compiler tool for CloudFormation YAML templates
|
109
|
+
test_files: []
|