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.
@@ -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,11 @@
1
+ module CloudFormationTool
2
+ module Errors
3
+
4
+ Autoloaded.module { }
5
+
6
+ class AppError < StandardError; end
7
+
8
+ class StackDoesNotExistError < StandardError; end
9
+
10
+ end
11
+ 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
@@ -0,0 +1,3 @@
1
+ module CloudFormationTool
2
+ VERSION = '0.1.1'
3
+ 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: []