jelder_cloudfront-invalidator 0.2.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.
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ Usage
2
+ =====
3
+
4
+ invalidator = CloudfrontInvalidator.new(AWS_KEY, AWS_SECRET, CF_DISTRIBUTION_ID)
5
+ list = %w[
6
+ index.html
7
+ favicon.ico
8
+ ]
9
+ invalidator.invalidate(list) do |status,time| # Block is optional.
10
+ invalidator.list # Or invalidator.list_detail
11
+ puts "Complete after < #{time.to_f.ceil} seconds." if status == "Complete"
12
+ end
13
+
14
+ A command line utility is also included.
15
+
16
+ $ cloudfront-invalidator invalidate $AWS_KEY $AWS_SECRET $DISTRIBUTION_ID index.html favicon.ico
17
+ $ cloudfront-invalidator list $AWS_KEY $AWS_SECRET $DISTRIBUTION_ID
18
+ $ cloudfront-invalidator list_detail $AWS_KEY $AWS_SECRET $DISTRIBUTION_ID
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require '../lib/cloudfront-invalidator'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'cloudfront-invalidator'
8
+ end
9
+
10
+ unless ARGV.length >= 4
11
+ warn "Usage: cloudfront-invalidator (invalidate|list|list_detail) aws_key aws_secret distribution_id [path ...]"
12
+ exit 1
13
+ end
14
+
15
+ verb = ARGV.shift
16
+ raise "Verb must be invalidate, list, or list_detail" unless %w{invalidate list list_detail}.include? verb
17
+ aws_key = ARGV.shift
18
+ aws_secret = ARGV.shift
19
+ distribution_id = ARGV.shift
20
+ paths = ARGV
21
+
22
+ invalidator = CloudfrontInvalidator.new(aws_key, aws_secret, distribution_id)
23
+ case verb
24
+ when 'invalidate'
25
+ print "Invalidating #{paths.size} objects"
26
+ invalidator.invalidate(paths) do |status,time|
27
+ print status == "Complete" ? "\nComplete after < #{time.to_f.ceil} seconds.\n" : "."
28
+ end
29
+ when 'list'
30
+ invalidator.list
31
+ when 'list_detail'
32
+ invalidator.list_detail
33
+ end
@@ -0,0 +1,140 @@
1
+ require 'net/https'
2
+ require 'base64'
3
+ require 'rexml/document'
4
+ require 'hmac-sha1' # this is a gem
5
+
6
+ class CloudfrontInvalidator
7
+ API_VERSION = '2012-05-05'
8
+ BASE_URL = "https://cloudfront.amazonaws.com/#{API_VERSION}/distribution/"
9
+ DOC_URL = "http://cloudfront.amazonaws.com/doc/#{API_VERSION}/"
10
+ BACKOFF_LIMIT = 8192
11
+ BACKOFF_DELAY = 0.025
12
+
13
+ def initialize(aws_key, aws_secret, cf_dist_id)
14
+ @aws_key, @aws_secret, @cf_dist_id = aws_key, aws_secret, cf_dist_id
15
+ end
16
+
17
+ def invalidate(*keys)
18
+ keys = keys.flatten.map do |k|
19
+ k.start_with?('/') ? k : '/' + k
20
+ end
21
+
22
+ uri = URI.parse "#{BASE_URL}#{@cf_dist_id}/invalidation"
23
+ http = Net::HTTP.new(uri.host, uri.port)
24
+ http.use_ssl = true
25
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
26
+ body = xml_body(keys)
27
+
28
+ delay = 1
29
+ begin
30
+ resp = http.send_request 'POST', uri.path, body, headers
31
+ doc = REXML::Document.new resp.body
32
+
33
+ # Create and raise an exception for any error the API returns to us.
34
+ if resp.code.to_i != 201
35
+ error_code = doc.elements["ErrorResponse/Error/Code"].text
36
+ self.class.const_set(error_code,Class.new(StandardError)) unless self.class.const_defined?(error_code.to_sym)
37
+ raise self.class.const_get(error_code).new(doc.elements["ErrorResponse/Error/Message"].text)
38
+ end
39
+
40
+ # Handle the common case of too many in progress by waiting until the others finish.
41
+ rescue TooManyInvalidationsInProgress => e
42
+ sleep delay * BACKOFF_DELAY
43
+ delay *= 2 unless delay >= BACKOFF_LIMIT
44
+ STDERR.puts e.inspect
45
+ retry
46
+ end
47
+
48
+ # If we are passed a block, poll on the status of this invalidation with truncated exponential backoff.
49
+ if block_given?
50
+ invalidation_id = doc.elements["Invalidation/Id"].text
51
+ poll_invalidation(invalidation_id) do |status,time|
52
+ yield status, time
53
+ end
54
+ end
55
+ return resp
56
+ end
57
+
58
+ def poll_invalidation(invalidation_id)
59
+ start = Time.now
60
+ delay = 1
61
+ loop do
62
+ doc = REXML::Document.new get_invalidation_detail_xml(invalidation_id)
63
+ status = doc.elements["Invalidation/Status"].text
64
+ yield status, Time.now - start
65
+ break if status != "InProgress"
66
+ sleep delay * BACKOFF_DELAY
67
+ delay *= 2 unless delay >= BACKOFF_LIMIT
68
+ end
69
+ end
70
+
71
+ def list(show_detail = false)
72
+ uri = URI.parse "#{BASE_URL}#{@cf_dist_id}/invalidation"
73
+ http = Net::HTTP.new(uri.host, uri.port)
74
+ http.use_ssl = true
75
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
76
+ resp = http.send_request 'GET', uri.path, '', headers
77
+
78
+ doc = REXML::Document.new resp.body
79
+ puts "MaxItems " + doc.elements["InvalidationList/MaxItems"].text + "; " + (doc.elements["InvalidationList/MaxItems"].text == "true" ? "truncated" : "not truncated")
80
+
81
+ doc.each_element("/InvalidationList/InvalidationSummary") do |summary|
82
+ invalidation_id = summary.elements["Id"].text
83
+ summary_text = "ID " + invalidation_id + ": " + summary.elements["Status"].text
84
+
85
+ if show_detail
86
+ detail_doc = REXML::Document.new get_invalidation_detail_xml(invalidation_id)
87
+ puts summary_text +
88
+ "; Created at: " +
89
+ detail_doc.elements["Invalidation/CreateTime"].text +
90
+ '; Caller reference: "' +
91
+ detail_doc.elements["Invalidation/InvalidationBatch/CallerReference"].text +
92
+ '"'
93
+ puts ' Invalidated URL paths:'
94
+
95
+ puts " " + detail_doc.elements.to_a('Invalidation/InvalidationBatch/Path').map { |path| path.text }.join(" ")
96
+ else
97
+ puts summary_text
98
+ end
99
+ end
100
+ end
101
+
102
+ def list_detail
103
+ list(true)
104
+ end
105
+
106
+ def get_invalidation_detail_xml(invalidation_id)
107
+ uri = URI.parse "#{BASE_URL}#{@cf_dist_id}/invalidation/#{invalidation_id}"
108
+ http = Net::HTTP.new(uri.host, uri.port)
109
+ http.use_ssl = true
110
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
111
+ resp = http.send_request 'GET', uri.path, '', headers
112
+ return resp.body
113
+ end
114
+
115
+ def xml_body(keys)
116
+ xml = <<XML
117
+ <?xml version="1.0" encoding="UTF-8"?>
118
+ <InvalidationBatch xmlns="#{DOC_URL}">
119
+ <Paths>
120
+ <Quantity>#{keys.size}</Quantity>
121
+ <Items>
122
+ #{keys.map{|k| "<Path>#{k}</Path>" }.join("\n ")}
123
+ </Items>
124
+ </Paths>
125
+ <CallerReference>#{self.class.to_s} on #{Socket.gethostname} at #{Time.now.to_i}</CallerReference>"
126
+ </InvalidationBatch>
127
+ XML
128
+ end
129
+
130
+ def headers
131
+ date = Time.now.strftime('%a, %d %b %Y %H:%M:%S %Z')
132
+ digest = HMAC::SHA1.new(@aws_secret)
133
+ digest << date
134
+ signature = Base64.encode64(digest.digest)
135
+ {'Date' => date, 'Authorization' => "AWS #{@aws_key}:#{signature}"}
136
+ end
137
+
138
+ class TooManyInvalidationsInProgress < StandardError ; end
139
+
140
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jelder_cloudfront-invalidator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jacob Elder
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ruby-hmac
16
+ requirement: &70328224035140 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70328224035140
25
+ description: Ruby library and command line utility included.
26
+ email: jacob.elder@gmail.com
27
+ executables:
28
+ - cloudfront-invalidator
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - lib/cloudfront-invalidator.rb
33
+ - README.md
34
+ - bin/cloudfront-invalidator
35
+ homepage: http://github.com/jelder/cloudfront-invalidator
36
+ licenses: []
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project: jelder_cloudfront-invalidator
55
+ rubygems_version: 1.8.11
56
+ signing_key:
57
+ specification_version: 3
58
+ summary: Manage Amazon CloudFront invalidation requests
59
+ test_files: []