middleman-cdn 0.1.9 → 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f2489e5e48befb67ac4262785c5d50eafc4f38bc
4
- data.tar.gz: 06de2636504f543675da89ffccf1f9e8b364704b
3
+ metadata.gz: f1126fd1f909909f010c506e2239341214a4ef80
4
+ data.tar.gz: 6ee82a56f22971322621659548ff8430e9259f8a
5
5
  SHA512:
6
- metadata.gz: 04ceb24a4ed31df414973cf66396c8add45475c27984ae9e80b8ebc51df7718e9f24183f263cd87c7510d980461f281a6da2161ec55ada2f1eb9ef94c3ea675b
7
- data.tar.gz: b65fa9621dcfcece07031d290b8a4a1794026c9123da238da053a47237ed69b1c9c0f7e797d432f285166a495812643ef7e08695115319d559618202beca269b
6
+ metadata.gz: 9dbf7a85e93f6ed5caba2d3264379114fd318d04449c2b2487127095ee8ab0534ff767e39a7a6d5c7c745341a684093b3ea4b4daa4a96d33dd4f9be947b2f2cf
7
+ data.tar.gz: 2540c95e40446f4ac123da08e4929df6e2926481ff287c402824a4f8177256b7df42241f68f5ae29a82e1f8bc788d45a4164e6273a39f790f95ab01e4b57850e
data/README.md CHANGED
@@ -1,21 +1,21 @@
1
1
  # Middleman CDN
2
- [![Gem Version](https://badge.fury.io/rb/middleman-cdn.svg)](http://badge.fury.io/rb/middleman-cdn) [![Build Status](https://travis-ci.org/leighmcculloch/middleman-cdn.svg)](https://travis-ci.org/leighmcculloch/middleman-cdn) [![Dependency Status](https://gemnasium.com/leighmcculloch/middleman-cdn.png)](https://gemnasium.com/leighmcculloch/middleman-cdn)
2
+ [![Gem Version](https://badge.fury.io/rb/middleman-cdn.svg)](http://badge.fury.io/rb/middleman-cdn) [![Build Status](https://travis-ci.org/leighmcculloch/middleman-cdn.svg)](https://travis-ci.org/leighmcculloch/middleman-cdn) [![Coverage Status](https://img.shields.io/coveralls/leighmcculloch/middleman-cdn.svg)](https://coveralls.io/r/leighmcculloch/middleman-cdn) [![Dependency Status](https://gemnasium.com/leighmcculloch/middleman-cdn.png)](https://gemnasium.com/leighmcculloch/middleman-cdn)
3
3
 
4
4
  A [middleman](http://middlemanapp.com/) deploy tool for invalidating resources cached
5
5
  on common Content Delivery Networks (CDNs).
6
6
 
7
7
  * Cache invalidation of files on:
8
8
  * [CloudFlare](https://cloudflare.com)
9
- * [Fastly](https://fastly.com)
10
9
  * [MaxCDN](https://maxcdn.com)
10
+ * [Fastly](https://fastly.com)
11
11
  * [Amazon CloudFront](https://aws.amazon.com/cloudfront/)
12
+ * [Rackspace CloudFiles](http://www.rackspace.com/cloud/files/)
12
13
  * Select files for invalidation with regex.
13
14
  * Automatically invalidate after build.
14
- * Manually trigger invalidation with single command.
15
+ * Manually trigger invalidation with a single command.
15
16
 
16
17
  What's next?
17
-
18
- * Add support for RackspaceCDN (Akamai).
18
+ * Invalidating files only when they've changed.
19
19
  * [Open an issue](../../issues/new) if you'd like your CDN provider added.
20
20
 
21
21
  # Usage
@@ -46,6 +46,12 @@ activate :cdn do |cdn|
46
46
  'https://example.com',
47
47
  ]
48
48
  }
49
+ cdn.maxcdn = {
50
+ alias: "...", # default ENV['MAXCDN_ALIAS']
51
+ consumer_key: "...", # default ENV['MAXCDN_CONSUMER_KEY']
52
+ consumer_secret: "...", # default ENV['MAXCDN_CONSUMER_SECRET']
53
+ zone_id: "...",
54
+ }
49
55
  cdn.fastly = {
50
56
  api_key: '...', # default ENV['FASTLY_API_KEY']
51
57
  base_urls: [
@@ -53,17 +59,18 @@ activate :cdn do |cdn|
53
59
  'https://www.example.com'
54
60
  ],
55
61
  }
56
- cdn.maxcdn = {
57
- alias: "...", # default ENV['MAXCDN_ALIAS']
58
- consumer_key: "...", # default ENV['MAXCDN_CONSUMER_KEY']
59
- consumer_secret: "...", # default ENV['MAXCDN_CONSUMER_SECRET']
60
- zone_id: "...",
61
- }
62
62
  cdn.cloudfront = {
63
63
  access_key_id: '...', # default ENV['AWS_ACCESS_KEY_ID']
64
64
  secret_access_key: '...', # default ENV['AWS_SECRET_ACCESS_KEY']
65
65
  distribution_id: '...'
66
66
  }
67
+ cdn.rackspace = {
68
+ username: "...", # default ENV['RACKSPACE_USERNAME']
69
+ api_key: "...", # default ENV['RACKSPACE_API_KEY']
70
+ region: "DFW", # DFW, SYD, IAD, ORD, HKG, etc
71
+ container: "...",
72
+ notification_email: "you@example.com" # optional
73
+ }
67
74
  cdn.filter = /\.html/i # default /.*/
68
75
  cdn.after_build = true # default is false
69
76
  end
@@ -107,6 +114,23 @@ at.
107
114
 
108
115
  CloudFlare invalidations often take a few seconds.
109
116
 
117
+ ### Configuration: MaxCDN
118
+
119
+ The `maxcdn` parameter contains the information specific to your MaxCDN
120
+ account. Your `alias` can be found on the API tab of your MaxCDN account page,
121
+ and you'll need to create an `application` in your MaxCDN account which
122
+ will provide you with API keys. The extension works by invalidating files
123
+ in pull zones. Make sure you add your website as a pull zone.
124
+
125
+ | Parameter | Description |
126
+ |:--------- |:----------- |
127
+ | `alias` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and it will be down the bottom right. |
128
+ | `consumer_key` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and creating an application which will give you a key and secret. |
129
+ | `secret_key` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and creating an application which will give you a key and secret. |
130
+ | `zone_id` | Each pull zone has a zone_id, you'll find this in your account. |
131
+
132
+ MaxCDN invalidations often take a few seconds.
133
+
110
134
  ### Configuration: Fastly
111
135
 
112
136
  The `fastly` parameter contains the information specific to your Fastly
@@ -121,23 +145,6 @@ at.
121
145
 
122
146
  Fastly invalidations often take a few seconds.
123
147
 
124
- ### Configuration: MaxCDN
125
-
126
- The `maxcdn` parameter contains the information specific to your MaxCDN
127
- account. You'll need to create an `application` in your MaxCDN account which
128
- will provide you with API keys, and your alias can be found on the API tab of
129
- your account page. The extension works by invalidating files in pull zones.
130
- Make sure you add your website as a pull zone.
131
-
132
- | Parameter | Description |
133
- |:--------- |:----------- |
134
- | `alias` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and it will be down the bottom right. |
135
- | `consumer_key` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and creating an application which will give you a key and secret. |
136
- | `secret_key` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and creating an application which will give you a key and secret. |
137
- | `zone_id` | Each pull zone has a zone_id, you'll find this in your account. |
138
-
139
- MaxCDN invalidations often take a few seconds.
140
-
141
148
  ### Configuration: CloudFront
142
149
 
143
150
  The `cloudfront` parameter contains the information specific to your AWS CloudFront
@@ -152,16 +159,40 @@ account and which distribution files should be invalidated for.
152
159
  CloudFront invalidations take up to 15 minutes. You can monitor the progress of
153
160
  the invalidation in your AWS Console.
154
161
 
162
+ ### Configuration: Rackspace CloudFiles
163
+
164
+ The `rackspace` parameter contains the information specific to your Rackspace
165
+ account. The extension works by invalidating files stored at the edge on
166
+ Rackspace's CDN (Akaimi) that mirrors Rackspace's CloudFiles. If you specify a
167
+ notification email, you will receive an email from Akaimi when the invalidation
168
+ has been completed.
169
+
170
+ | Parameter | Description |
171
+ |:--------- |:----------- |
172
+ | `username` | Your Rackspace username. |
173
+ | `api_key` | Your Rackspace API key. |
174
+ | `region` | The region the CloudFiles container is stored in. Typically this is `DFW` unless you specified an alternative region for your container. Examples: `DFW`, `SYD`, `IAD`, `ORD`, `HKG`. |
175
+ | `container` | The CloudFiles container. |
176
+ | `notification_email` | An email will be sent to this address after the invalidation has completed. |
177
+
178
+ Note:
179
+ 1. At the time of writing Rackspace allows 25 files to be invalidated per
180
+ day.
181
+ 2. This extension will only invalidate files on the CDN and will not synchronise
182
+ or upload files to CloudFiles. Use [middleman-sync](https://github.com/karlfreeman/middleman-sync) to do that.
183
+
155
184
  ### Credentials via Environment Variables
156
185
 
157
186
  Instead of storing your CDN credentials in config.rb where they may be public
158
187
  on github, store them in environment variables, or execute on the
159
- commandline as:
188
+ commandline as. Any parameters with a default of `ENV[...]` (see above) can be stored
189
+ as environment variables or provided on the commandline like this example.
160
190
 
161
191
  ```bash
162
- CLOUDFLARE_CLIENT_API_KEY= CLOUDFLARE_EMAIL= FASTLY_API_KEY= AWS_ACCESS_KEY= AWS_SECRET= bundle exec middleman invalidate
192
+ CLOUDFLARE_CLIENT_API_KEY= CLOUDFLARE_EMAIL= bundle exec middleman invalidate
163
193
  ```
164
194
 
195
+
165
196
  ## Invalidating
166
197
 
167
198
  Set `after_build` to `true` and the cache will be invalidated after build:
@@ -181,17 +212,21 @@ bundle exec middleman cdn
181
212
 
182
213
  ## Example Usage
183
214
 
184
- I'm using middleman-cdn on my personal website [leighmcculloch.com](http://leighmcculloch.com) which is on [github](https://github.com/leighmcculloch/leighmcculloch.com) if you want to checkout how I deploy. Unlike CloudFront, CloudFlare doesn't default to caching HTML. Configure a PageRule that looks like this to tell CloudFlare's edge to cache everything.
215
+ I'm using middleman-cdn on my personal website [leighmcculloch.com](http://leighmcculloch.com) which is on [github](https://github.com/leighmcculloch/leighmcculloch.com) if you want to checkout how I deploy. It's configuration has all of the above CDNs in use for demonstration. I primarily use CloudFlare, and unlike the other CDNs, CloudFlare doesn't default to caching HTML. To make the most of CloudFlare, configure a PageRule that looks like this to tell CloudFlare to cache everything.
185
216
  ![CloudFlare PageRule Example](README-cloudflare-pagerule-example.png)
186
217
 
187
218
  ## Thanks
188
219
 
189
- Middleman CDN is a fork off [Middleman CloudFront](https://github.com/andrusha/middleman-cloudfront) and I used it as the base for building this extension. The code was well structured and easy to understand. It was easy to break out the CloudFront specific logic and to add support for CloudFlare. My gratitude goes to @andrusha and his work on Middleman CloudFront.
220
+ Middleman CDN is a fork off [Middleman CloudFront](https://github.com/andrusha/middleman-cloudfront) and I used it as the base for building this extension. The code was well structured and easy to understand. It was easy to break out the CloudFront specific logic and to add support for CloudFlare and the other CDNs. My gratitude goes to @andrusha and @manuelmeurer for their work on Middleman CloudFront.
190
221
 
191
222
  Thanks to @b4k3r for the [Cloudflare gem](https://github.com/b4k3r/cloudflare) that made invalidating CloudFlare files a breeze.
192
223
 
224
+ Thanks to @geemus and the many contributors to the [fog gem](https://github.com/fog/fog) that made invalidating CloudFront easy.
225
+
226
+ Official gems from [Fastly](https://github.com/fastly/fastly-ruby) and [MaxCDN](https://github.com/MaxCDN/ruby-maxcdn) are used for interacting with their services.
227
+
193
228
  ## Why Middleman CDN
194
229
 
195
- Middleman CloudFront is a great extension for Middleman and perfect if you're using CloudFront. I needed a similar extension for CloudFlare, however it's becoming increasingly common for static websites to be hosted across multiple CDNs. [jsDelivr](http://jsdelivr.com/) is a well known promoter of this strategy.
230
+ It's becoming increasingly common for static websites to be hosted across multiple CDNs. [jsDelivr](http://jsdelivr.com/) is a well known promoter of this strategy and it's a strategy I want my toolset (middleman) to support for my next side project.
196
231
 
197
- In light of the new trends in how we are using CDNs, I decided it would be more worthwhile to create an extension that can grow to support all the popular CDNs.
232
+ I've created this extension so that it can grow to support the CDNs we (you and me) are using.
@@ -14,8 +14,8 @@ module Middleman
14
14
  TEXT
15
15
  end
16
16
 
17
- def say_status(status, newline: true, header: true)
18
- ::Middleman::Cli::CDN.say_status(self.class.key, status, newline: newline, header: header)
17
+ def say_status(status, newline: true, header: true, wait_enter: false)
18
+ ::Middleman::Cli::CDN.say_status(self.class.key, status, newline: newline, header: header, wait_enter: wait_enter)
19
19
  end
20
20
  end
21
21
 
@@ -0,0 +1,64 @@
1
+ #Encoding: UTF-8
2
+ require "httparty"
3
+ require "active_support/core_ext/string"
4
+ require "middleman-cdn/clients/rackspace.rb"
5
+
6
+ module Middleman
7
+ module Cli
8
+ class RackspaceCDN < BaseCDN
9
+ DAILY_LIMIT = 25
10
+
11
+ def self.key
12
+ "rackspace"
13
+ end
14
+
15
+ def self.example_configuration_elements
16
+ {
17
+ username: ['"..."', "# default ENV['RACKSPACE_USERNAME']"],
18
+ api_key: ['"..."', "# default ENV['RACKSPACE_API_KEY']"],
19
+ region: ['"DFW"', "# DFW, SYD, IAD, ORD, HKG, etc"],
20
+ container: ['"..."', ""],
21
+ notification_email: ['"..."', "# optional"],
22
+ }
23
+ end
24
+
25
+ def invalidate(options, files)
26
+ options[:username] ||= ENV['RACKSPACE_USERNAME']
27
+ options[:api_key] ||= ENV['RACKSPACE_API_KEY']
28
+
29
+ [:username, :api_key, :region, :container].each do |key|
30
+ if options[key].blank?
31
+ say_status("Error: Configuration key rackspace[:#{key}] is missing.".light_red)
32
+ raise
33
+ end
34
+ end
35
+
36
+ files = files.reject { |file| file.end_with?("/") }
37
+
38
+ if files.count > DAILY_LIMIT
39
+ say_status("Warning: You are invalidating more files than Rackspace's daily limit (25).")
40
+ say_status("Press ENTER to continue, or CTRL-C to exit.", wait_enter: true)
41
+ end
42
+
43
+ rackspace_client = RackspaceClient.new(options[:username], options[:api_key])
44
+
45
+ files.each do |file|
46
+ invalidate_file(rackspace_client, options[:region], options[:container], file, notification_email: options[:notification_email])
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def invalidate_file(rackspace_client, region, container, file, notification_email: nil)
53
+ begin
54
+ say_status("Invalidating #{file}...", newline: false)
55
+ rackspace_client.invalidate(region, container, file, notification_email: notification_email)
56
+ rescue => e
57
+ say_status(" error: #{e.message}".light_red, header: false)
58
+ else
59
+ say_status("✔".light_green, header: false)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,82 @@
1
+ #Encoding: UTF-8
2
+ require "httparty"
3
+ require "active_support/core_ext/string"
4
+
5
+ module Middleman
6
+ module Cli
7
+ class RackspaceClient
8
+ def initialize(username, api_key)
9
+ @username = username
10
+ @api_key = api_key
11
+ @auth_data = nil
12
+ end
13
+
14
+ def invalidate(region, container, file, notification_email: nil)
15
+ headers = { "x-auth-token" => get_auth_token }
16
+ headers.merge!({ "x-purge-email" => notification_email }) if notification_email.present?
17
+ response = HTTParty.delete("#{get_cdn_endpoint(region)}/#{container}#{URI.escape(file)}", { :headers => headers })
18
+ case response.header.code
19
+ when "204"
20
+ # success
21
+ when "400"
22
+ error_message = response.headers["x-purge-failed-reason"]
23
+ raise "400, #{error_message}" if error_message.present?
24
+ raise "400, an error occurred."
25
+ when "403"
26
+ raise "403, the server refused to respond to the request. Check your credentials."
27
+ when "404"
28
+ raise "404, the requested resource could not be found."
29
+ else
30
+ error_message = response.body
31
+ raise "#{response.header.code}, an error occurred. #{error_message}".rstrip
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def perform_auth
38
+ return if @auth_data.present?
39
+ response = HTTParty.post("https://identity.api.rackspacecloud.com/v2.0/tokens", {
40
+ :body => {
41
+ "auth" => {
42
+ "RAX-KSKEY:apiKeyCredentials" => {
43
+ "username" => @username,
44
+ "apiKey" => @api_key
45
+ }
46
+ }
47
+ }.to_json,
48
+ :headers => {
49
+ "Content-Type" => "application/json"
50
+ }
51
+ })
52
+ case response.header.code
53
+ when "200"
54
+ @auth_data = JSON.parse(response.body)
55
+ else
56
+ error_message = response.body
57
+ raise "#{response.header.code}, an error occurred. #{error_message}"
58
+ end
59
+ end
60
+
61
+ def get_auth_token
62
+ perform_auth
63
+ @auth_data["access"]["token"]["id"]
64
+ end
65
+
66
+ def get_service_endpoint(service_type, region)
67
+ perform_auth
68
+ access = @auth_data["access"] if @auth_data
69
+ serviceCatalog = access["serviceCatalog"] if access
70
+ service = serviceCatalog.find { |service| service["type"] == service_type } if serviceCatalog
71
+ endpoints = service["endpoints"] if service
72
+ endpoint = endpoints.find { |endpoint| endpoint["region"] == region } if endpoints
73
+ return public_url = endpoint["publicURL"] if endpoint
74
+ nil
75
+ end
76
+
77
+ def get_cdn_endpoint(region)
78
+ get_service_endpoint("rax:object-cdn", region)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -5,6 +5,7 @@ require "middleman-cdn/cdns/cloudflare.rb"
5
5
  require "middleman-cdn/cdns/cloudfront.rb"
6
6
  require "middleman-cdn/cdns/fastly.rb"
7
7
  require "middleman-cdn/cdns/maxcdn.rb"
8
+ require "middleman-cdn/cdns/rackspace.rb"
8
9
  require "colorize"
9
10
 
10
11
  module Middleman
@@ -23,42 +24,44 @@ module Middleman
23
24
 
24
25
  desc "cdn:cdn_invalidate", "Invalidate your CloudFlare or CloudFront cache"
25
26
  def cdn_invalidate(options = nil)
26
- if options.nil?
27
- app_instance = ::Middleman::Application.server.inst
28
- unless app_instance.respond_to?(:cdn_options)
29
- self.class.say_status(nil, "Error: You need to activate the cdn extension in config.rb.\n#{example_configuration}".light_red)
30
- raise
27
+ begin
28
+ if options.nil?
29
+ app_instance = ::Middleman::Application.server.inst
30
+ unless app_instance.respond_to?(:cdn_options)
31
+ self.class.say_status(nil, "Error: You need to activate the cdn extension in config.rb.\n#{example_configuration}".light_red)
32
+ raise
33
+ end
34
+ options = app_instance.cdn_options
31
35
  end
32
- options = app_instance.cdn_options
33
- end
34
- options.filter ||= /.*/
36
+ options.filter ||= /.*/
35
37
 
36
- if cdns.all? { |cdn| options.public_send(cdn.key.to_sym).nil? }
37
- self.class.say_status(nil, "Error: You must specify a config for one of the supported CDNs.\n#{example_configuration}".light_red)
38
- raise
39
- end
38
+ if cdns.all? { |cdn| options.public_send(cdn.key.to_sym).nil? }
39
+ self.class.say_status(nil, "Error: You must specify a config for one of the supported CDNs.\n#{example_configuration}".light_red)
40
+ raise
41
+ end
40
42
 
41
- files = list_files(options.filter)
42
- self.class.say_status(nil, "Invalidating #{files.count} files with filter: " + "#{options.filter.source}".magenta.bold)
43
- files.each { |file| self.class.say_status(nil, " • #{file}") }
44
- return if files.empty?
43
+ files = list_files(options.filter)
44
+ self.class.say_status(nil, "Invalidating #{files.count} files with filter: " + "#{options.filter.source}".magenta.bold)
45
+ files.each { |file| self.class.say_status(nil, " • #{file}") }
46
+ return if files.empty?
45
47
 
46
- cdns_keyed.each do |cdn_key, cdn|
47
- cdn_options = options.public_send(cdn_key.to_sym)
48
- cdn.new.invalidate(cdn_options, files) unless cdn_options.nil?
48
+ cdns_keyed.each do |cdn_key, cdn|
49
+ cdn_options = options.public_send(cdn_key.to_sym)
50
+ cdn.new.invalidate(cdn_options, files) unless cdn_options.nil?
51
+ end
52
+ rescue SystemExit, Interrupt
53
+ self.class.say_status(nil, nil, header: false)
49
54
  end
50
55
  end
51
56
 
52
- def self.say_status(cdn, status, newline: true, header: true)
57
+ def self.say_status(cdn, status, newline: true, header: true, wait_enter: false)
53
58
  message = ""
54
59
  message << "#{:cdn.to_s.rjust(12).light_green.bold} #{cdn.try(:yellow).try(:bold)}" if header
55
60
  message << " " if header && cdn
56
- message << status
57
- if newline
58
- puts message
59
- else
60
- print message
61
- end
61
+ message << status if status
62
+ print message
63
+ STDIN.noecho(&:gets) if wait_enter
64
+ puts "" if newline
62
65
  end
63
66
 
64
67
  private
@@ -68,7 +71,8 @@ module Middleman
68
71
  CloudFlareCDN,
69
72
  CloudFrontCDN,
70
73
  FastlyCDN,
71
- MaxCDN
74
+ MaxCDN,
75
+ RackspaceCDN
72
76
  ]
73
77
  end
74
78
 
@@ -13,6 +13,7 @@ module Middleman
13
13
  option :cloudfront, nil, 'CloudFront options'
14
14
  option :fastly, nil, 'Fastly options'
15
15
  option :maxcdn, nil, 'MaxCDN options'
16
+ option :rackspace, nil, 'Rackspace options'
16
17
  option :filter, nil, 'Cloudflare options'
17
18
  option :after_build, false, 'Cloudflare options'
18
19
 
@@ -1,5 +1,5 @@
1
1
  module Middleman
2
2
  module CDN
3
- VERSION = '0.1.9'
3
+ VERSION = '0.1.10'
4
4
  end
5
5
  end
@@ -23,9 +23,11 @@ Gem::Specification.new do |s|
23
23
  s.add_dependency 'maxcdn', '~> 0.1'
24
24
  s.add_dependency 'colorize', '~> 0.7'
25
25
  s.add_dependency 'activesupport', '~> 4.1'
26
+ s.add_dependency 'httparty', '~> 0.13'
26
27
 
27
28
  s.add_development_dependency 'rake', '~> 0.9'
28
29
  s.add_development_dependency 'rspec', '~> 3.0'
30
+ s.add_development_dependency 'coveralls', '~> 0.7'
29
31
 
30
32
  s.add_dependency 'middleman-core', '~> 3.0'
31
33
  end
@@ -40,12 +40,12 @@ TEXT
40
40
 
41
41
  describe "#say_status" do
42
42
  it "should use the Cli CDN class to say status" do
43
- expect(::Middleman::Cli::CDN).to receive(:say_status).with(described_class.key, "status text", newline: false, header: false)
43
+ expect(::Middleman::Cli::CDN).to receive(:say_status).with(described_class.key, "status text", newline: false, header: false, wait_enter: false)
44
44
  subject.say_status("status text", newline: false, header: false)
45
45
  end
46
46
 
47
47
  it "should use the Cli CDN class to say status with defaults" do
48
- expect(::Middleman::Cli::CDN).to receive(:say_status).with(described_class.key, "status text", newline: true, header: true)
48
+ expect(::Middleman::Cli::CDN).to receive(:say_status).with(described_class.key, "status text", newline: true, header: true, wait_enter: false)
49
49
  subject.say_status("status text")
50
50
  end
51
51
  end
@@ -0,0 +1,216 @@
1
+ #Encoding: UTF-8
2
+ require 'spec_helper'
3
+ require 'lib/middleman-cdn/cdns/base_protocol'
4
+
5
+ describe Middleman::Cli::RackspaceCDN do
6
+ it_behaves_like "BaseCDN"
7
+
8
+ describe '.key' do
9
+ it "should be 'rackspace'" do
10
+ expect(described_class.key).to eq("rackspace")
11
+ end
12
+ end
13
+
14
+ describe '.example_configuration_elements' do
15
+ it "should contain these keys" do
16
+ required_keys = [:username, :api_key, :region, :container, :notification_email]
17
+ expect(described_class.example_configuration_elements.keys).to eq(required_keys)
18
+ end
19
+ end
20
+
21
+ describe '#invalidate' do
22
+ let(:double_rackspace) { double("RackspaceClient") }
23
+
24
+ before do
25
+ allow(double_rackspace).to receive(:invalidate)
26
+ allow(Middleman::Cli::RackspaceClient).to receive(:new).and_return(double_rackspace)
27
+ end
28
+
29
+ let(:files) { [ "/index.html", "/", "/test/index.html", "/test/image.png" ] }
30
+
31
+ let(:files_no_dirs) { files.reject { |file| file.end_with?("/") } }
32
+
33
+ context "all options provided" do
34
+ let(:options) do
35
+ {
36
+ username: "00000000000000000000",
37
+ api_key: "11111111111111111111",
38
+ region: "222222",
39
+ container: "333333",
40
+ notification_email: "test@example.com",
41
+ }
42
+ end
43
+
44
+ it "should instantiate rackspace client with credentails" do
45
+ expect(Middleman::Cli::RackspaceClient).to receive(:new).with("00000000000000000000", "11111111111111111111")
46
+ subject.invalidate(options, files)
47
+ end
48
+
49
+ it "should not raise errors" do
50
+ subject.invalidate(options, files)
51
+ end
52
+
53
+ it "should invalidate each files one at a time" do
54
+ expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/index.html", notification_email: "test@example.com")
55
+ expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/test/index.html", notification_email: "test@example.com")
56
+ expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/test/image.png", notification_email: "test@example.com")
57
+ expect(double_rackspace).to_not receive(:invalidate).with(anything, anything, "/", anything)
58
+ subject.invalidate(options, files)
59
+ end
60
+
61
+ it "should output saying invalidating each file" do
62
+ files_escaped = files_no_dirs.map { |file| Regexp.escape(file) }
63
+ expect { subject.invalidate(options, files) }.to output(/#{files_escaped.join(".+")}/m).to_stdout
64
+ end
65
+
66
+ it "should output saying success checkmarks" do
67
+ expect { subject.invalidate(options, files) }.to output(/✔/).to_stdout
68
+ end
69
+
70
+ context "and errors occurs when purging" do
71
+ before do
72
+ allow(double_rackspace).to receive(:invalidate).and_raise(StandardError)
73
+ end
74
+
75
+ it "should output saying error information" do
76
+ expect { subject.invalidate(options, files) }.to output(/error: StandardError/).to_stdout
77
+ end
78
+ end
79
+ end
80
+
81
+ context "environment variables used for credentials" do
82
+ before do
83
+ allow(ENV).to receive(:[])
84
+ allow(ENV).to receive(:[]).with("RACKSPACE_USERNAME").and_return("00000000000000000000")
85
+ allow(ENV).to receive(:[]).with("RACKSPACE_API_KEY").and_return("11111111111111111111")
86
+ end
87
+
88
+ let(:options) do
89
+ {
90
+ region: "222222",
91
+ container: "333333",
92
+ notification_email: "test@example.com",
93
+ }
94
+ end
95
+
96
+ it "should instantiate with environment variable credentails" do
97
+ expect(Middleman::Cli::RackspaceClient).to receive(:new).with("00000000000000000000", "11111111111111111111")
98
+ subject.invalidate(options, files)
99
+ end
100
+
101
+ it "should not raise errors" do
102
+ subject.invalidate(options, files)
103
+ end
104
+ end
105
+
106
+ context "if username not provided" do
107
+ before do
108
+ allow(ENV).to receive(:[])
109
+ allow(ENV).to receive(:[]).with("RACKSPACE_USERNAME").and_return(nil)
110
+ end
111
+
112
+ let(:options) do
113
+ {
114
+ api_key: "11111111111111111111",
115
+ region: "222222",
116
+ container: "333333",
117
+ notification_email: "test@example.com",
118
+ }
119
+ end
120
+
121
+ it "should raise error" do
122
+ expect { subject.invalidate(options, files) }.to raise_error(RuntimeError)
123
+ end
124
+
125
+ it "should output saying error" do
126
+ expect { subject.invalidate(options, files) rescue nil }.to output(/Error: Configuration key rackspace\[:username\] is missing\./).to_stdout
127
+ end
128
+ end
129
+
130
+ context "if api key not provided" do
131
+ before do
132
+ allow(ENV).to receive(:[])
133
+ allow(ENV).to receive(:[]).with("RACKSPACE_API_KEY").and_return(nil)
134
+ end
135
+
136
+ let(:options) do
137
+ {
138
+ username: "00000000000000000000",
139
+ region: "222222",
140
+ container: "333333",
141
+ notification_email: "test@example.com",
142
+ }
143
+ end
144
+
145
+ it "should raise error" do
146
+ expect { subject.invalidate(options, files) }.to raise_error(RuntimeError)
147
+ end
148
+
149
+ it "should output saying error" do
150
+ expect { subject.invalidate(options, files) rescue nil }.to output(/Error: Configuration key rackspace\[:api_key\] is missing\./).to_stdout
151
+ end
152
+ end
153
+
154
+ context "if region not provided" do
155
+ let(:options) do
156
+ {
157
+ username: "00000000000000000000",
158
+ api_key: "11111111111111111111",
159
+ container: "333333",
160
+ notification_email: "test@example.com",
161
+ }
162
+ end
163
+
164
+ it "should raise error" do
165
+ expect { subject.invalidate(options, files) }.to raise_error(RuntimeError)
166
+ end
167
+
168
+ it "should output saying error" do
169
+ expect { subject.invalidate(options, files) rescue nil }.to output(/Error: Configuration key rackspace\[:region\] is missing\./).to_stdout
170
+ end
171
+ end
172
+
173
+ context "if container not provided" do
174
+ let(:options) do
175
+ {
176
+ username: "00000000000000000000",
177
+ api_key: "11111111111111111111",
178
+ region: "222222",
179
+ notification_email: "test@example.com",
180
+ }
181
+ end
182
+
183
+ it "should raise error" do
184
+ expect { subject.invalidate(options, files) }.to raise_error(RuntimeError)
185
+ end
186
+
187
+ it "should output saying error" do
188
+ expect { subject.invalidate(options, files) rescue nil }.to output(/Error: Configuration key rackspace\[:container\] is missing\./).to_stdout
189
+ end
190
+ end
191
+
192
+ context "if notification email not provided" do
193
+ let(:options) do
194
+ {
195
+ username: "00000000000000000000",
196
+ api_key: "11111111111111111111",
197
+ region: "222222",
198
+ container: "333333"
199
+ }
200
+ end
201
+
202
+ it "should not raise error" do
203
+ expect { subject.invalidate(options, files) }.to_not raise_error
204
+ end
205
+
206
+ it "should invalidate each file with a nil notification email" do
207
+ expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/index.html", notification_email: nil)
208
+ expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/test/index.html", notification_email: nil)
209
+ expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/test/image.png", notification_email: nil)
210
+ expect(double_rackspace).to_not receive(:invalidate).with(anything, anything, "/", notification_email: nil)
211
+ subject.invalidate(options, files)
212
+ end
213
+ end
214
+
215
+ end
216
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ module RackspaceResponseDoubles
4
+ def double_for_response_auth_success
5
+ double({
6
+ :header => double({
7
+ :code => "200"
8
+ }),
9
+ :headers => {},
10
+ :body => {
11
+ "access" => { "token" => { "id" => "<auth-token>" } },
12
+ "serviceCatalog" => [
13
+ {
14
+ "type" => "rax:object-cdn",
15
+ "endpoints" => [
16
+ {
17
+ "region" => "DFW",
18
+ "publicURL" => "http://example.com/"
19
+ }
20
+ ]
21
+ }
22
+ ]
23
+ }.to_json
24
+ })
25
+ end
26
+
27
+ def double_for_response_auth_fail
28
+ double({
29
+ :header => double({
30
+ :code => "401"
31
+ }),
32
+ :headers => {},
33
+ :body => {
34
+ "unauthorized" => {
35
+ "code" => "401",
36
+ "message" => "Username or api key is invalid."
37
+ }
38
+ }.to_json
39
+ })
40
+ end
41
+
42
+ def double_for_response_delete(status_code: 204, fail_message: nil)
43
+ d = double({
44
+ :header => double({
45
+ :code => "#{status_code}"
46
+ }),
47
+ :headers => {},
48
+ :body => ""
49
+ })
50
+ if fail_message
51
+ case status_code
52
+ when 204, 403, 404
53
+ when 400
54
+ allow(d).to receive(:headers).and_return({ "x-purge-failed-reason" => fail_message })
55
+ else
56
+ allow(d).to receive(:body).and_return(fail_message) if status_code
57
+ end
58
+ end
59
+ d
60
+ end
61
+ end
62
+
63
+ RSpec.configure do |c|
64
+ c.include RackspaceResponseDoubles, :include_rackspace_response_doubles
65
+ end
@@ -0,0 +1,122 @@
1
+ #Encoding: UTF-8
2
+ require 'spec_helper'
3
+ require 'lib/middleman-cdn/clients/rackspace_response_doubles.rb'
4
+
5
+ describe Middleman::Cli::RackspaceClient, :include_rackspace_response_doubles do
6
+
7
+ describe '#invalidate', :vcr do
8
+
9
+ let(:username) { "111111" }
10
+ let(:api_key) { "000000" }
11
+ let(:region) { "DFW" }
12
+ let(:container) { "container" }
13
+ let(:notification_email) { "test@example.com" }
14
+
15
+ let(:subject) { described_class.new(username, api_key) }
16
+
17
+ before do
18
+ allow(HTTParty).to receive(:post).and_return(double_for_response_auth_success)
19
+ allow(HTTParty).to receive(:delete).and_return(double_for_response_delete)
20
+ end
21
+
22
+ context "authentication" do
23
+ context "valid credentials" do
24
+ before do
25
+ expect(HTTParty).to receive(:post).with("https://identity.api.rackspacecloud.com/v2.0/tokens", {
26
+ :body => {
27
+ "auth" => {
28
+ "RAX-KSKEY:apiKeyCredentials" => {
29
+ "username" => username,
30
+ "apiKey" => api_key
31
+ }
32
+ }
33
+ }.to_json,
34
+ :headers => {
35
+ "Content-Type" => "application/json"
36
+ }
37
+ }).once.and_return(double_for_response_auth_success)
38
+ end
39
+
40
+ it "should authenticate using the given credentials" do
41
+ subject.invalidate(region, container, "/index.html")
42
+ end
43
+
44
+ it "should authenticate once only" do
45
+ subject.invalidate(region, container, "/index.html")
46
+ subject.invalidate(region, container, "/dir/index.html")
47
+ end
48
+ end
49
+
50
+ context "invalid credentials" do
51
+ it "should raise error" do
52
+ expect(HTTParty).to receive(:post).and_return(double_for_response_auth_fail)
53
+ expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError)
54
+ end
55
+
56
+ it "should retry authentication when needed" do
57
+ expect(HTTParty).to receive(:post).with("https://identity.api.rackspacecloud.com/v2.0/tokens", anything).once.and_return(double_for_response_auth_fail)
58
+ expect(HTTParty).to receive(:post).with("https://identity.api.rackspacecloud.com/v2.0/tokens", anything).once.and_return(double_for_response_auth_success)
59
+ subject.invalidate(region, container, "/index.html") rescue nil
60
+ subject.invalidate(region, container, "/index.html")
61
+ end
62
+ end
63
+ end
64
+
65
+ context "successful invalidate (204)" do
66
+ it "should not raise error" do
67
+ expect{ subject.invalidate(region, container, "/index.html") }.to_not raise_error
68
+ end
69
+
70
+ context "with notification email" do
71
+ it "should include notification email in invalidation request" do
72
+ expect(HTTParty).to receive(:delete).with(anything, hash_including({
73
+ :headers => hash_including({
74
+ "x-purge-email" => "test@example.com"
75
+ })
76
+ })).once.and_return(double_for_response_delete)
77
+ subject.invalidate(region, container, "/index.html", notification_email: "test@example.com")
78
+ end
79
+ end
80
+
81
+ context "with file names that contain characters not allowed in a URL" do
82
+ it "should escape the file path" do
83
+ expect(HTTParty).to receive(:delete).with(match(/\/dir\/index%20file.html$/), anything).once.and_return(double_for_response_delete)
84
+ subject.invalidate(region, container, "/dir/index file.html")
85
+ end
86
+ end
87
+ end
88
+
89
+ context "unsuccessful invalidation" do
90
+ it "should raise error on 400 with fail message" do
91
+ expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 400, fail_message: "the fail message"))
92
+ expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "400, the fail message")
93
+ end
94
+
95
+ it "should raise error on 400 without fail message" do
96
+ expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 400))
97
+ expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "400, an error occurred.")
98
+ end
99
+
100
+ it "should raise error on 403" do
101
+ expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 403))
102
+ expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "403, the server refused to respond to the request. Check your credentials.")
103
+ end
104
+
105
+ it "should raise error on 404" do
106
+ expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 404))
107
+ expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "404, the requested resource could not be found.")
108
+ end
109
+
110
+ it "should raise error on any unspecified response codes" do
111
+ expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 999))
112
+ expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "999, an error occurred.")
113
+ end
114
+
115
+ it "should raise error on any unspecified response codes with fail message in body" do
116
+ expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 999, fail_message: "the error has arrived"))
117
+ expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "999, an error occurred. the error has arrived")
118
+ end
119
+ end
120
+
121
+ end
122
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
1
4
  require 'middleman-cdn'
2
5
 
3
6
  RSpec.configure do |config|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: middleman-cdn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leigh McCulloch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-17 00:00:00.000000000 Z
11
+ date: 2014-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fog
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '4.1'
97
+ - !ruby/object:Gem::Dependency
98
+ name: httparty
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.13'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.13'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rake
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,20 @@ dependencies:
122
136
  - - "~>"
123
137
  - !ruby/object:Gem::Version
124
138
  version: '3.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: coveralls
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.7'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.7'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: middleman-core
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -155,6 +183,8 @@ files:
155
183
  - lib/middleman-cdn/cdns/cloudfront.rb
156
184
  - lib/middleman-cdn/cdns/fastly.rb
157
185
  - lib/middleman-cdn/cdns/maxcdn.rb
186
+ - lib/middleman-cdn/cdns/rackspace.rb
187
+ - lib/middleman-cdn/clients/rackspace.rb
158
188
  - lib/middleman-cdn/commands.rb
159
189
  - lib/middleman-cdn/extension.rb
160
190
  - lib/middleman-cdn/version.rb
@@ -165,6 +195,9 @@ files:
165
195
  - spec/lib/middleman-cdn/cdns/cloudfront_spec.rb
166
196
  - spec/lib/middleman-cdn/cdns/fastly_spec.rb
167
197
  - spec/lib/middleman-cdn/cdns/maxcdn_spec.rb
198
+ - spec/lib/middleman-cdn/cdns/rackspace_spec.rb
199
+ - spec/lib/middleman-cdn/clients/rackspace_response_doubles.rb
200
+ - spec/lib/middleman-cdn/clients/rackspace_spec.rb
168
201
  - spec/lib/middleman-cdn/commands_spec.rb
169
202
  - spec/spec_helper.rb
170
203
  homepage: https://github.com/leighmcculloch/middleman-cdn