1
0
Fork 0
mirror of https://github.com/fog/fog.git synced 2022-11-09 13:51:43 -05:00

Merge pull request #1173 from masterzen/feature/aws-cdn-models

[aws|cdn] small fixes and aws cdn models
This commit is contained in:
Wesley Beary 2012-09-26 12:27:33 -07:00
commit 772909377c
18 changed files with 782 additions and 24 deletions

View file

@ -37,21 +37,72 @@ Now you'll need to create a 'distribution' which represents a mapping from the C
# parse the response for stuff you'll need later
distribution_id = data.body['Id']
caller_reference = data.body['CallerReference']
caller_reference = data.body['DistributionConfig']['CallerReference']
etag = data.headers['ETag']
cdn_domain_name = data.body['DomainName']
# wait for the updates to propogate
Fog.wait_for {
cdn.get_distribution(distribution_id).body['Status'] ## 'Deployed'
cdn.get_distribution(distribution_id).body['Status'] == 'Deployed'
}
Fog also supports models for the AWS CDN. The above code can also be written like this:
distribution = cdn.distributions.create( :custom_origin => {
'DNSName' => 'www.example.com',
'OriginProtocolPolicy' => 'match-viewer'
}, :enabled => true
})
distribution.wait_for { ready? }
Like other collections supported by Fog, it is also possible to browse the list of distributions:
cdn.distributions.all
Or get access to a distinct distribution by its identity:
cdn.distributions.get(distribution_id)
## Getting Served
With the domain name from the distribution in hand you should now be ready to serve content from the edge. All you need to do is start replacing urls like `http://www.example.com/stylesheets/foo.css` with `#{cdn_domain_name}/stylesheets/foo.css`. Just because you can do something doesn't always mean you should though. Dynamic pages are not really well suited to CDN storage, since CDN content will be the same for every user. Fortunately some of your most used content is a great fit. By just switching over your images, javascripts and stylesheets you can have an impact for each and every one of your users.
Congrats, your site is faster! By default the urls aren't very pretty, something like `http://d1xdx2sah5udd0.cloudfront.net/stylesheets/foo.css`. Thankfully you can use CNAME config options to utilize something like `http://assets.example.com/stylesheets/foo.css`, if you are interested in learning more about this let me know in the comments.
## Invalidating the CDN caches
Sometimes, some part of the CDN cache needs to be invalidated because the origin changed and we need a faster propagation than waiting for the objects to expire by themselves. To do this, CloudFront supports creating <a href="http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/Actions_Invalidations.html">distributions invalidation</a>.
An invalidation can be created with the following code:
# let's invalidate /test.html and /path/to/file.html
data = cdn.post_invalidation(distribution_id, [ "/test.html", "/path/to/file.html" ])
invalidation_id = data.body['Id']
Fog.wait_for { cdn.get_invalidation(distribution_id, invalidation_id).body['Status'] == 'Completed' }
It is also possible to list past and current invalidation for a given distribution:
cdn.get_invalidation_list(distribution_id)
The same can be written with Fog CDN model abstraction:
distribution = cdn.distributions.get(distribution_id)
invalidation = distribution.invalidations.create(:paths => [ "/test.html", "/path/to/file.html" ])
invalidation.wait_for { ready? }
Listing invalidations is as simple as:
distribution.invalidations.all
# this returns only summarized invalidation
# to get access to the invalidations path:
distribution.invalidations.get(invalidation_id)
## Cleaning Up
But, just in case you need to update things I'll run through how you can make changes. In my case I just want to clean up after myself, so I'll use the distribution_id and ETag from before to disable the distribution. We need to use the ETag as well because it provides a way to refer to different versions of the same distribution and ensures we are updating the version that we think we are.
@ -75,8 +126,23 @@ But, just in case you need to update things I'll run through how you can make ch
Now you just need to wait for the update to happen like before and once its disabled we can delete it:
Fog.wait_for {
cdn.get_distribution(distribution_id).body['Status'] ## 'Deployed'
cdn.get_distribution(distribution_id).body['Status'] == 'Deployed'
}
cdn.delete_distribution(distribution_id, etag)
This can also be written with CDN models as:
distribution = cdn.distributions.get(distribution_id)
# make sure the distribution is deployed otherwise it can't be disabled
distribution.wait_for { ready? }
distribution.disable
# Disabling a distribution is a lengthy operation
distribution.wait_for { ready? }
# and finally let's get rid of it
distribution.destroy
Thats it, now go forth and speed up some load times!

View file

@ -9,7 +9,11 @@ module Fog
requires :aws_access_key_id, :aws_secret_access_key
recognizes :host, :path, :port, :scheme, :version, :persistent, :use_iam_profile, :aws_session_token, :aws_credentials_expire_at
model_path 'fog/aws/cdn/models'
model_path 'fog/aws/models/cdn'
model :distribution
collection :distributions
model :streaming_distribution
collection :streaming_distributions
request_path 'fog/aws/requests/cdn'
request 'delete_distribution'

View file

@ -0,0 +1,93 @@
require 'fog/core/model'
require 'fog/aws/models/cdn/invalidations'
require 'fog/aws/models/cdn/distribution_helper'
module Fog
module CDN
class AWS
class Distribution < Fog::Model
include Fog::CDN::AWS::DistributionHelper
identity :id, :aliases => 'Id'
attribute :caller_reference, :aliases => 'CallerReference'
attribute :last_modified_time, :aliases => 'LastModifiedTime'
attribute :status, :aliases => 'Status'
attribute :s3_origin, :aliases => 'S3Origin'
attribute :custom_origin, :aliases => 'CustomOrigin'
attribute :cname, :aliases => 'CNAME'
attribute :comment, :aliases => 'Comment'
attribute :enabled, :aliases => 'Enabled'
attribute :in_progress_invalidation_batches, :aliases => 'InProgressInvalidationBatches'
attribute :logging, :aliases => 'Logging'
attribute :trusted_signers, :aliases => 'TrustedSigners'
attribute :default_root_object,:aliases => 'DefaultRootObject'
attribute :domain, :aliases => 'DomainName'
attribute :etag, :aliases => ['Etag', 'ETag']
# items part of DistributionConfig
CONFIG = [ :caller_reference, :origin, :cname, :comment, :enabled, :logging, :trusted_signers, :default_root_object ]
def initialize(new_attributes = {})
super(distribution_config_to_attributes(new_attributes))
end
def invalidations
@invalidations ||= begin
Fog::CDN::AWS::Invalidations.new(
:distribution => self,
:connection => connection
)
end
end
def save
requires_one :s3_origin, :custom_origin
options = attributes_to_options
response = identity ? put_distribution_config(identity, etag, options) : post_distribution(options)
etag = response.headers['ETag']
merge_attributes(response.body)
true
end
private
def delete_distribution(identity, etag)
connection.delete_distribution(identity, etag)
end
def put_distribution_config(identity, etag, options)
connection.put_distribution_config(identity, etag, options)
end
def post_distribution(options = {})
connection.post_distribution(options)
end
def attributes_to_options
options = {
'CallerReference' => caller_reference,
'S3Origin' => s3_origin,
'CustomOrigin' => custom_origin,
'CNAME' => cname,
'Comment' => comment,
'Enabled' => enabled,
'Logging' => logging,
'TrustedSigners' => trusted_signers,
'DefaultRootObject' => default_root_object
}
options.reject! { |k,v| v.nil? }
options.reject! { |k,v| v.respond_to?(:empty?) && v.empty? }
options
end
def distribution_config_to_attributes(new_attributes = {})
new_attributes.merge(new_attributes.delete('DistributionConfig') || {})
end
end
end
end
end

View file

@ -0,0 +1,64 @@
require 'fog/core/collection'
module Fog
module CDN
class AWS
module DistributionHelper
def destroy
requires :identity, :etag, :caller_reference
raise "Distribution must be disabled to be deleted" unless disabled?
delete_distribution(identity, etag)
true
end
def enabled?
requires :identity
!!enabled and ready?
end
def disabled?
requires :identity
not enabled? and ready?
end
def custom_origin?
requires :identity
not custom_origin.nil?
end
def ready?
requires :identity
status == 'Deployed'
end
def enable
requires :identity
reload if etag.nil? or caller_reference.nil?
unless enabled?
self.enabled = true
response = put_distribution_config(identity, etag, attributes_to_options)
etag = response.headers['ETag']
merge_attributes(response.body)
end
true
end
def disable
requires :identity
reload if etag.nil? or caller_reference.nil?
if enabled?
self.enabled = false
response = put_distribution_config(identity, etag, attributes_to_options)
etag = response.headers['ETag']
merge_attributes(response.body)
end
true
end
end
end
end
end

View file

@ -0,0 +1,32 @@
require 'fog/core/collection'
require 'fog/aws/models/cdn/distribution'
require 'fog/aws/models/cdn/distributions_helper'
module Fog
module CDN
class AWS
class Distributions < Fog::Collection
include Fog::CDN::AWS::DistributionsHelper
model Fog::CDN::AWS::Distribution
attribute :marker, :aliases => 'Marker'
attribute :max_items, :aliases => 'MaxItems'
attribute :is_truncated, :aliases => 'IsTruncated'
def get_distribution(dist_id)
connection.get_distribution(dist_id)
end
def list_distributions(options = {})
connection.get_distribution_list(options)
end
alias :each_distribution_this_page :each
end
end
end
end

View file

@ -0,0 +1,47 @@
require 'fog/core/collection'
module Fog
module CDN
class AWS
module DistributionsHelper
def all(options = {})
merge_attributes(options)
data = list_distributions(options).body
merge_attributes('IsTruncated' => data['IsTruncated'], 'Marker' => data['Marker'])
if summary = data['DistributionSummary']
load(summary.map { |a| { 'DistributionConfig' => a } })
else
load((data['StreamingDistributionSummary'] || {}).map { |a| { 'StreamingDistributionConfig' => a }})
end
end
def get(dist_id)
response = get_distribution(dist_id)
data = response.body.merge({'ETag' => response.headers['ETag']})
new(data)
rescue Excon::Errors::NotFound
nil
end
def each
if !block_given?
self
else
subset = dup.all
subset.each_distribution_this_page {|f| yield f}
while subset.is_truncated
subset = subset.all('Marker' => subset.marker, 'MaxItems' => 1000)
subset.each_distribution_this_page {|f| yield f}
end
self
end
end
end
end
end
end

View file

@ -0,0 +1,59 @@
require 'fog/core/model'
module Fog
module CDN
class AWS
class Invalidation < Fog::Model
identity :id, :aliases => 'Id'
attribute :status, :aliases => 'Status'
attribute :create_time, :aliases => 'CreateTime'
attribute :caller_reference, :aliases => 'CallerReference'
attribute :paths, :aliases => 'Paths'
def initialize(new_attributes={})
super(invalidation_to_attributes(new_attributes))
end
def distribution
@distribution
end
def ready?
requires :id, :status
status == 'Completed'
end
def save
requires :paths, :caller_reference
raise "Submitted invalidation cannot be submitted again" if identity
response = connection.post_invalidation(distribution.identity, paths, caller_reference || Time.now.to_i.to_s)
merge_attributes(invalidation_to_attributes(response.body))
true
end
def destroy
# invalidations can't be removed, but tests are requiring they do :)
true
end
private
def distribution=(dist)
@distribution = dist
end
def invalidation_to_attributes(new_attributes={})
invalidation_batch = new_attributes.delete('InvalidationBatch') || {}
new_attributes['Paths'] = invalidation_batch['Path']
new_attributes['CallerReference'] = invalidation_batch['CallerReference']
new_attributes
end
end
end
end
end

View file

@ -0,0 +1,54 @@
require 'fog/core/collection'
require 'fog/aws/models/cdn/invalidation'
module Fog
module CDN
class AWS
class Invalidations < Fog::Collection
attribute :is_truncated, :aliases => ['IsTruncated']
attribute :max_items, :aliases => ['MaxItems']
attribute :next_marker, :aliases => ['NextMarker']
attribute :marker, :aliases => ['Marker']
attribute :distribution
model Fog::CDN::AWS::Invalidation
def all(options = {})
requires :distribution
options[:max_items] ||= max_items
options.delete_if {|key, value| value.nil?}
data = connection.get_invalidation_list(distribution.identity, options).body
merge_attributes(data.reject {|key, value| !['IsTruncated', 'MaxItems', 'NextMarker', 'Marker'].include?(key)})
load(data['InvalidationSummary'])
end
def get(invalidation_id)
requires :distribution
data = connection.get_invalidation(distribution.identity, invalidation_id).body
if data
invalidation = new(data)
else
nil
end
rescue Excon::Errors::NotFound
nil
end
def new(attributes = {})
requires :distribution
super({ :distribution => distribution }.merge!(attributes))
end
end
end
end
end

View file

@ -0,0 +1,77 @@
require 'fog/core/model'
require 'fog/aws/models/cdn/invalidations'
require 'fog/aws/models/cdn/distribution_helper'
module Fog
module CDN
class AWS
class StreamingDistribution < Fog::Model
include Fog::CDN::AWS::DistributionHelper
identity :id, :aliases => 'Id'
attribute :caller_reference, :aliases => 'CallerReference'
attribute :last_modified_time, :aliases => 'LastModifiedTime'
attribute :status, :aliases => 'Status'
attribute :s3_origin, :aliases => 'S3Origin'
attribute :cname, :aliases => 'CNAME'
attribute :comment, :aliases => 'Comment'
attribute :enabled, :aliases => 'Enabled'
attribute :logging, :aliases => 'Logging'
attribute :domain, :aliases => 'DomainName'
attribute :etag, :aliases => ['Etag', 'ETag']
# items part of DistributionConfig
CONFIG = [ :caller_reference, :cname, :comment, :enabled, :logging ]
def initialize(new_attributes = {})
super(distribution_config_to_attributes(new_attributes))
end
def save
requires_one :s3_origin
options = attributes_to_options
response = identity ? put_distribution_config(identity, etag, options) : post_distribution(options)
etag = response.headers['ETag']
merge_attributes(response.body)
true
end
private
def delete_distribution(identity, etag)
connection.delete_streaming_distribution(identity, etag)
end
def put_distribution_config(identity, etag, options)
connection.put_streaming_distribution_config(identity, etag, options)
end
def post_distribution(options = {})
connection.post_streaming_distribution(options)
end
def attributes_to_options
options = {
'CallerReference' => caller_reference,
'S3Origin' => s3_origin,
'CNAME' => cname,
'Comment' => comment,
'Enabled' => enabled,
'Logging' => logging,
}
options.reject! { |k,v| v.nil? }
options.reject! { |k,v| v.respond_to?(:empty?) && v.empty? }
options
end
def distribution_config_to_attributes(new_attributes = {})
new_attributes.merge(new_attributes.delete('StreamingDistributionConfig') || {})
end
end
end
end
end

View file

@ -0,0 +1,32 @@
require 'fog/core/collection'
require 'fog/aws/models/cdn/streaming_distribution'
require 'fog/aws/models/cdn/distributions_helper'
module Fog
module CDN
class AWS
class StreamingDistributions < Fog::Collection
include Fog::CDN::AWS::DistributionsHelper
model Fog::CDN::AWS::StreamingDistribution
attribute :marker, :aliases => 'Marker'
attribute :max_items, :aliases => 'MaxItems'
attribute :is_truncated, :aliases => 'IsTruncated'
def get_distribution(dist_id)
connection.get_streaming_distribution(dist_id)
end
def list_distributions(options = {})
connection.get_streaming_distribution_list(options)
end
alias :each_distribution_this_page :each
end
end
end
end

View file

@ -6,7 +6,7 @@ module Fog
class GetInvalidation < Fog::Parsers::Base
def reset
@response = { 'InvalidationBatch' => [] }
@response = { 'InvalidationBatch' => { 'Path' => [] } }
end
def start_element(name, attrs = [])
@ -16,9 +16,11 @@ module Fog
def end_element(name)
case name
when 'Path'
@response['InvalidationBatch'] << @value
@response['InvalidationBatch'][name] << value
when 'Id', 'Status', 'CreateTime'
@response[name] = @value
@response[name] = value
when 'CallerReference'
@response['InvalidationBatch'][name] = value
end
end

View file

@ -0,0 +1,19 @@
Shindo.tests("Fog::CDN[:aws] | distribution", ['aws', 'cdn']) do
params = { :s3_origin => { 'DNSName' => 'fog_test_cdn.s3.amazonaws.com'}, :enabled => true }
model_tests(Fog::CDN[:aws].distributions, params, false) do
# distribution needs to be ready before being disabled
tests("#ready? - may take 15 minutes to complete...").succeeds do
pending if Fog.mocking?
@instance.wait_for { ready? }
end
# and disabled before being distroyed
tests("#disable - may take 15 minutes to complete...").succeeds do
pending if Fog.mocking?
@instance.disable
@instance.wait_for { ready? }
end
end
end

View file

@ -0,0 +1,19 @@
Shindo.tests("Fog::CDN[:aws] | distributions", ['aws', 'cdn']) do
params = { :s3_origin => { 'DNSName' => 'fog_test_cdn.s3.amazonaws.com'}, :enabled => true}
collection_tests(Fog::CDN[:aws].distributions, params, false) do
# distribution needs to be ready before being disabled
tests("#ready? - may take 15 minutes to complete...").succeeds do
pending if Fog.mocking?
@instance.wait_for { ready? }
end
# and disabled before being distroyed
tests("#disable - may take 15 minutes to complete...").succeeds do
pending if Fog.mocking?
@instance.disable
@instance.wait_for { ready? }
end
end
end

View file

@ -0,0 +1,34 @@
Shindo.tests("Fog::CDN[:aws] | invalidation", ['aws', 'cdn']) do
pending if Fog.mocking?
tests("distributions#create").succeeds do
@distribution = Fog::CDN[:aws].distributions.create(:s3_origin => {'DNSName' => 'fog_test.s3.amazonaws.com'}, :enabled => true)
end
params = { :paths => [ '/index.html', '/path/to/index.html' ] }
model_tests(@distribution.invalidations, params, false) do
tests("#id") do
returns(true) { @instance.identity != nil }
end
tests("#paths") do
returns([ '/index.html', '/path/to/index.html' ].sort) { @instance.paths.sort }
end
tests("#ready? - may take 15 minutes to complete...").succeeds do
@instance.wait_for { ready? }
end
end
tests("distribution#destroy - may take around 15/20 minutes to complete...").succeeds do
@distribution.wait_for { ready? }
@distribution.disable
@distribution.wait_for { ready? }
@distribution.destroy
end
end

View file

@ -0,0 +1,17 @@
Shindo.tests("Fog::CDN[:aws] | invalidations", ['aws', 'cdn']) do
pending if Fog.mocking?
tests("distributions#create").succeeds do
@distribution = Fog::CDN[:aws].distributions.create(:s3_origin => {'DNSName' => 'fog_test.s3.amazonaws.com'}, :enabled => true)
end
collection_tests(@distribution.invalidations, { :paths => [ '/index.html' ]}, false)
tests("distribution#destroy - may take 15/20 minutes to complete").succeeds do
@distribution.wait_for { ready? }
@distribution.disable
@distribution.wait_for { ready? }
@distribution.destroy
end
end

View file

@ -0,0 +1,19 @@
Shindo.tests("Fog::CDN[:aws] | streaming_distribution", ['aws', 'cdn']) do
params = { :s3_origin => { 'DNSName' => 'fog_test_cdn.s3.amazonaws.com'}, :enabled => true }
model_tests(Fog::CDN[:aws].streaming_distributions, params, false) do
# distribution needs to be ready before being disabled
tests("#ready? - may take 15 minutes to complete...").succeeds do
pending if Fog.mocking?
@instance.wait_for { ready? }
end
# and disabled before being distroyed
tests("#disable - may take 15 minutes to complete...").succeeds do
pending if Fog.mocking?
@instance.disable
@instance.wait_for { ready? }
end
end
end

View file

@ -0,0 +1,19 @@
Shindo.tests("Fog::CDN[:aws] | streaming_distributions", ['aws', 'cdn']) do
params = { :s3_origin => { 'DNSName' => 'fog_test_cdn.s3.amazonaws.com'}, :enabled => true}
collection_tests(Fog::CDN[:aws].streaming_distributions, params, false) do
# distribution needs to be ready before being disabled
tests("#ready? - may take 15 minutes to complete...").succeeds do
pending if Fog.mocking?
@instance.wait_for { ready? }
end
# and disabled before being distroyed
tests("#disable - may take 15 minutes to complete...").succeeds do
pending if Fog.mocking?
@instance.disable
@instance.wait_for { ready? }
end
end
end

View file

@ -2,7 +2,7 @@ Shindo.tests('Fog::CDN[:aws] | CDN requests', ['aws', 'cdn']) do
@cf_connection = Fog::CDN[:aws]
tests('success') do
tests('distributions success') do
test('get current ditribution list count') do
pending if Fog.mocking?
@ -108,7 +108,7 @@ Shindo.tests('Fog::CDN[:aws] | CDN requests', ['aws', 'cdn']) do
response = @cf_connection.get_invalidation(@dist_id, @invalidation_id)
if response.status == 200
paths = response.body['InvalidationBatch'].sort
paths = response.body['InvalidationBatch']['Path'].sort
status = response.body['Status']
if status.length > 0 and paths == [ '/test.html', '/path/to/file.html' ].sort
result = true
@ -118,7 +118,7 @@ Shindo.tests('Fog::CDN[:aws] | CDN requests', ['aws', 'cdn']) do
result
}
test("disable distribution #{@dist_id}") {
test("disable distribution #{@dist_id} - can take 15 minutes to complete...") {
pending if Fog.mocking?
result = false
@ -140,20 +140,11 @@ Shindo.tests('Fog::CDN[:aws] | CDN requests', ['aws', 'cdn']) do
result = true
# unfortunately you can delete only after a distribution becomes Deployed
first = Time.now
catch(:deployed) do
loop do
Fog.wait_for {
response = @cf_connection.get_distribution(@dist_id)
return false if response.status != 200
return false if (Time.now - first) > 20 * 60 # abort after 20 minutes
if response.status == 200 and response.body['Status'] == 'Deployed'
@etag = response.headers['ETag']
throw :deployed
end
sleep 15
end
end
response.status == 200 and response.body['Status'] == 'Deployed'
}
response = @cf_connection.delete_distribution(@dist_id, @etag)
if response.status != 204
@ -163,4 +154,114 @@ Shindo.tests('Fog::CDN[:aws] | CDN requests', ['aws', 'cdn']) do
result
}
end
tests('streaming distributions success') do
test('get current streaming ditribution list count') do
pending if Fog.mocking?
@count= 0
response = @cf_connection.get_streaming_distribution_list
if response.status == 200
@distributions = response.body['StreamingDistributionSummary']
@count = @distributions.count
end
response.status == 200
end
test('create distribution') {
pending if Fog.mocking?
result = false
response = @cf_connection.post_streaming_distribution('S3Origin' => { 'DNSName' => 'test_cdn.s3.amazonaws.com'}, 'Enabled' => true)
if response.status == 201
@dist_id = response.body['Id']
@etag = response.headers['ETag']
@caller_reference = response.body['StreamingDistributionConfig']['CallerReference']
if (@dist_id.length > 0)
result = true
end
end
result
}
test("get info on distribution #{@dist_id}") {
pending if Fog.mocking?
result = false
response = @cf_connection.get_streaming_distribution(@dist_id)
if response.status == 200
@etag = response.headers['ETag']
status = response.body['Status']
if ((status == 'Deployed') or (status == 'InProgress')) and not @etag.nil?
result = true
end
end
result
}
test('list streaming distributions') do
pending if Fog.mocking?
result = false
response = @cf_connection.get_streaming_distribution_list
if response.status == 200
distributions = response.body['StreamingDistributionSummary']
if (distributions.count > 0)
dist = distributions[0]
dist_id = dist['Id']
end
max_items = response.body['MaxItems']
if (dist_id.length > 0) and (max_items > 0)
result = true
end
end
result
end
test("disable distribution #{@dist_id} - can take 15 minutes to complete...") {
pending if Fog.mocking?
result = false
response = @cf_connection.put_streaming_distribution_config(@dist_id, @etag, 'S3Origin' => { 'DNSName' => 'test_cdn.s3.amazonaws.com'}, 'Enabled' => false, 'CallerReference' => @caller_reference)
if response.status == 200
@etag = response.headers['ETag']
unless @etag.nil?
result = true
end
end
result
}
test("remove distribution #{@dist_id}") {
pending if Fog.mocking?
result = true
# unfortunately you can delete only after a distribution becomes Deployed
Fog.wait_for {
response = @cf_connection.get_distribution(@dist_id)
@etag = response.headers['ETag']
response.status == 200 and response.body['Status'] == 'Deployed'
}
response = @cf_connection.delete_streaming_distribution(@dist_id, @etag)
if response.status != 204
result = false
end
result
}
end
end