branchable_cdn_assets 0.5.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.
- checksums.yaml +7 -0
- data/lib/branchable_cdn_assets.rb +19 -0
- data/lib/branchable_cdn_assets/check_before.rb +49 -0
- data/lib/branchable_cdn_assets/cloudfront.rb +28 -0
- data/lib/branchable_cdn_assets/config.rb +80 -0
- data/lib/branchable_cdn_assets/config/environment_attribute_reader.rb +17 -0
- data/lib/branchable_cdn_assets/file_manager.rb +222 -0
- data/lib/branchable_cdn_assets/file_manager/checks.rb +41 -0
- data/lib/branchable_cdn_assets/manifest.rb +63 -0
- data/lib/branchable_cdn_assets/rake_tasks.rb +69 -0
- data/lib/branchable_cdn_assets/shell.rb +17 -0
- data/lib/branchable_cdn_assets/version.rb +3 -0
- data/spec/lib/branchable_cdn_assets/check_before_spec.rb +61 -0
- data/spec/lib/branchable_cdn_assets/config_spec.rb +152 -0
- data/spec/lib/branchable_cdn_assets/file_manager/find_spec.rb +61 -0
- data/spec/lib/branchable_cdn_assets/file_manager_spec.rb +256 -0
- data/spec/lib/branchable_cdn_assets/manifest_spec.rb +138 -0
- data/spec/lib/branchable_cdn_assets/rake_tasks_spec.rb +64 -0
- data/spec/lib/branchable_cdn_assets_spec.rb +9 -0
- data/spec/lib/cloudfront_spec.rb +67 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/given.rb +28 -0
- data/spec/support/hash.rb +8 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6ea31a15245850a8679508ce6256859f872e8e22
|
4
|
+
data.tar.gz: 34645624f7e366189c37c46c2ccd1841e032652c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d724f92695a224e3fc88dca3ca8075b18cdb420a6a0817205ceeceb9a3b3cc789907b4fcf9c0247221e9a913a2977fc6ea987c3547171865874f72435e68174c
|
7
|
+
data.tar.gz: fd962e24f6d4b906f7507d6636d6cd11f82a782112e418dc0421a67e66801a4ac9c11ed66a7d8ff9e697746f6129ae26f0fa1560470f6cfab6f44372155954bc
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'asgit'
|
5
|
+
require 'here_or_there'
|
6
|
+
require 'colorize'
|
7
|
+
require 'fog'
|
8
|
+
|
9
|
+
require_relative 'branchable_cdn_assets/version'
|
10
|
+
require_relative 'branchable_cdn_assets/check_before'
|
11
|
+
require_relative 'branchable_cdn_assets/shell'
|
12
|
+
|
13
|
+
require_relative 'branchable_cdn_assets/file_manager'
|
14
|
+
require_relative 'branchable_cdn_assets/manifest'
|
15
|
+
require_relative 'branchable_cdn_assets/config'
|
16
|
+
require_relative 'branchable_cdn_assets/cloudfront'
|
17
|
+
|
18
|
+
module BranchableCDNAssets
|
19
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module BranchableCDNAssets
|
2
|
+
|
3
|
+
# this abstracts creating filter methods
|
4
|
+
# not offering inheritance currently
|
5
|
+
#
|
6
|
+
# check_before :check, params={}
|
7
|
+
#
|
8
|
+
# to only check for specific methods,
|
9
|
+
# pass an array of methods to the :methods key
|
10
|
+
#
|
11
|
+
# check_before :check, methods: ["foo", "bar"]
|
12
|
+
#
|
13
|
+
module CheckBefore
|
14
|
+
|
15
|
+
def before_checks
|
16
|
+
@_before_checks ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
# define a before check for a specific method
|
20
|
+
#
|
21
|
+
# @param method [Symbol] the method to check on
|
22
|
+
# @param check [Symbol] the check to run
|
23
|
+
# @param params [Hash] additional parameters for the check
|
24
|
+
# @return [Void]
|
25
|
+
def check_before check, params={}
|
26
|
+
before_checks << params.merge( check: check )
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def self.extended base
|
31
|
+
base.class_eval do
|
32
|
+
|
33
|
+
# @param action [Symbol] the method to execute
|
34
|
+
# @param *args [Array] arguments to pass to the check method
|
35
|
+
def with_check action, *args
|
36
|
+
self.class.before_checks.select do |c|
|
37
|
+
c[:methods].nil? || c[:methods].include?(action)
|
38
|
+
end.each do |c|
|
39
|
+
send c[:check]
|
40
|
+
end
|
41
|
+
|
42
|
+
public_send(action, *args)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module BranchableCDNAssets
|
2
|
+
class Cloudfront
|
3
|
+
|
4
|
+
attr_reader :access_key, :secret_key, :distribution_id,
|
5
|
+
:cdn
|
6
|
+
|
7
|
+
# handles communication with cloudfront
|
8
|
+
def initialize keys
|
9
|
+
@distribution_id = keys.fetch :distribution_id
|
10
|
+
@access_key = keys.fetch :access_key
|
11
|
+
@secret_key = keys.fetch :secret_key
|
12
|
+
|
13
|
+
@cdn = ::Fog::CDN.new provider: 'AWS',
|
14
|
+
aws_access_key_id: access_key,
|
15
|
+
aws_secret_access_key: secret_key
|
16
|
+
end
|
17
|
+
|
18
|
+
# invalidate a batch of files on fog
|
19
|
+
# @param files [Array]
|
20
|
+
def invalidate_files files
|
21
|
+
resp = cdn.post_invalidation( distribution_id, Array(files) )
|
22
|
+
resp.body["InvalidationBatch"]["Path"].each do |file|
|
23
|
+
puts "Posted an invalidation for #{file}".colorize( :green )
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require_relative 'config/environment_attribute_reader'
|
2
|
+
|
3
|
+
module BranchableCDNAssets
|
4
|
+
class Config
|
5
|
+
extend EnvironmentAttributeReader
|
6
|
+
|
7
|
+
attr_reader :raw_data, :branch,
|
8
|
+
:production_branch, :cloudfront, :cdn_dir,
|
9
|
+
:env, :environments
|
10
|
+
env_attr_reader :host, :root, :url
|
11
|
+
|
12
|
+
def initialize data, branch=Asgit.current_branch
|
13
|
+
@raw_data = normalize_data data
|
14
|
+
@branch = branch
|
15
|
+
|
16
|
+
@production_branch = raw_data.fetch :production_branch, 'master'
|
17
|
+
@cloudfront = raw_data.fetch :cloudfront, {}
|
18
|
+
@cdn_dir = raw_data.fetch :dir, 'cdn'
|
19
|
+
|
20
|
+
@env = env_for_branch
|
21
|
+
@environments = add_env_path_modifications_to_env_data raw_data.fetch(:environments, {})
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def add_env_path_modifications_to_env_data environments
|
27
|
+
Hash[*environments.map do |env,data|
|
28
|
+
data = data.dup
|
29
|
+
unless env == :production
|
30
|
+
data[:root] = File.join( data[:root], branch, '/' )
|
31
|
+
data[:url] = File.join( data[:url], branch )
|
32
|
+
end
|
33
|
+
|
34
|
+
[env,data]
|
35
|
+
end.flatten]
|
36
|
+
end
|
37
|
+
|
38
|
+
def env_for_branch
|
39
|
+
if branch == production_branch
|
40
|
+
return :production
|
41
|
+
elsif raw_data.fetch(:environments).has_key?( branch.to_sym )
|
42
|
+
return branch.to_sym
|
43
|
+
else
|
44
|
+
return raw_data.fetch(:default_env, 'staging').to_sym
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def normalize_data data
|
49
|
+
if data.respond_to?(:to_hash)
|
50
|
+
symbolize_keys data.to_hash
|
51
|
+
else
|
52
|
+
symbolize_keys read_config_file(data)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def read_config_file path
|
57
|
+
if File.exists?(path)
|
58
|
+
YAML.load IO.read(path)
|
59
|
+
else
|
60
|
+
raise "config file not found at #{path}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def symbolize_keys(hash)
|
65
|
+
hash.inject({}) do |out, (key, value)|
|
66
|
+
k = case key
|
67
|
+
when String then key.to_sym
|
68
|
+
else key
|
69
|
+
end
|
70
|
+
v = case value
|
71
|
+
when Hash then symbolize_keys(value)
|
72
|
+
else value
|
73
|
+
end
|
74
|
+
out[k] = v
|
75
|
+
out
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module BranchableCDNAssets
|
2
|
+
class Config
|
3
|
+
|
4
|
+
module EnvironmentAttributeReader
|
5
|
+
def env_attr_reader *keys
|
6
|
+
keys.each do |key|
|
7
|
+
define_method key do
|
8
|
+
environments[env.to_sym].fetch(key) {
|
9
|
+
raise ArgumentError, "No key '#{key.to_s}' exists for '#{env}'"
|
10
|
+
}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require_relative 'file_manager/checks'
|
2
|
+
|
3
|
+
module BranchableCDNAssets
|
4
|
+
class FileManager
|
5
|
+
extend CheckBefore
|
6
|
+
include Checks
|
7
|
+
|
8
|
+
check_before :manifest_list_clean_for_master, methods: [ :push!, :pull!, :prune! ]
|
9
|
+
check_before :local_file_conflict, methods: [ :push!, :pull! ]
|
10
|
+
check_before :ready_for_production, methods: [ :move_to_production ]
|
11
|
+
|
12
|
+
attr_reader :config
|
13
|
+
attr_reader :root
|
14
|
+
attr_reader :branch
|
15
|
+
attr_reader :manifest
|
16
|
+
|
17
|
+
# handles moving files to/from our various cdn locations
|
18
|
+
def initialize config, branch=nil
|
19
|
+
@config = config
|
20
|
+
@root = config.cdn_dir
|
21
|
+
@branch = branch || config.branch
|
22
|
+
@manifest = Manifest.new( File.join( @root, "#{@branch}.manifest" ) )
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param where [Symbol] :local, :remote, :all (default)
|
26
|
+
# @return [Array]
|
27
|
+
def list where=:all
|
28
|
+
case where
|
29
|
+
when :local then
|
30
|
+
return list_local_files
|
31
|
+
when :remote then
|
32
|
+
return list_remote_files
|
33
|
+
when :both then
|
34
|
+
return list_local_files & list_remote_files
|
35
|
+
else
|
36
|
+
return list_local_files + list_remote_files
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# destructive pull, removes references to remote
|
41
|
+
# files once they're pulled
|
42
|
+
# @return [Array] the pulled files
|
43
|
+
def pull!
|
44
|
+
pull_list = pull
|
45
|
+
@manifest.remove_files( pull_list )
|
46
|
+
@manifest.update_source_file!
|
47
|
+
return pull_list
|
48
|
+
end
|
49
|
+
|
50
|
+
# destructive push, removes the local versions of
|
51
|
+
# the pushed files and adds them to the manifest
|
52
|
+
# @return [Array] the pushed files
|
53
|
+
def push!
|
54
|
+
setup_remote
|
55
|
+
|
56
|
+
push_list = push
|
57
|
+
invalidate( push_list ) if config.env.to_sym == :production
|
58
|
+
|
59
|
+
@manifest.merge_files( push_list )
|
60
|
+
@manifest.update_source_file!
|
61
|
+
|
62
|
+
return push_list
|
63
|
+
end
|
64
|
+
|
65
|
+
# remove local files that exist on the remote
|
66
|
+
# @return [Array]
|
67
|
+
def prune!
|
68
|
+
puts "files to be removed:\n#{list(:both).join("\n")}"
|
69
|
+
abort unless Shell.get_input("do you want to continue? (y|n) ") == "y"
|
70
|
+
|
71
|
+
list(:both).each do |f|
|
72
|
+
Shell.run_local "rm #{File.join( root, f )}"
|
73
|
+
end
|
74
|
+
remove_empty_directories
|
75
|
+
end
|
76
|
+
|
77
|
+
# move the current cdn files to the production cdn
|
78
|
+
# guarded to only run from prod
|
79
|
+
def move_to_production branch
|
80
|
+
branch_cdn = FileManager.new( Config.new( config.raw_data, branch ) )
|
81
|
+
if branch_cdn.manifest.files.empty?
|
82
|
+
puts "no files to move from #{branch}"
|
83
|
+
abort
|
84
|
+
end
|
85
|
+
branch_cdn.pull!
|
86
|
+
push!
|
87
|
+
end
|
88
|
+
|
89
|
+
def remove_empty_directories
|
90
|
+
empty_directories.each do |dir|
|
91
|
+
Dir.rmdir( dir )
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def find file
|
96
|
+
return :local if list(:local).include?(file)
|
97
|
+
return File.join( config.url, file ) if list(:remote).include?(file)
|
98
|
+
|
99
|
+
unless config.env == :production
|
100
|
+
production_config = Config.new( config.raw_data, config.production_branch )
|
101
|
+
production_manifest = Manifest.new( File.join( root, "#{config.production_branch}.manifest" ) )
|
102
|
+
return File.join( production_config.url, file ) if production_manifest.files.include?(file)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def invalidate files
|
109
|
+
to_invalidate = ( files & list(:local) ).map do |f|
|
110
|
+
File.join( config.cloudfront[:path_prefix], f )
|
111
|
+
end
|
112
|
+
|
113
|
+
Cloudfront.new( config.cloudfront )
|
114
|
+
.invalidate_files( to_invalidate )
|
115
|
+
end
|
116
|
+
|
117
|
+
# create the root dir on remote
|
118
|
+
def setup_remote
|
119
|
+
resp = Shell.run_remote "mkdir -p #{config.root}", hostname: config.host
|
120
|
+
raise resp.stderr unless resp.success?
|
121
|
+
end
|
122
|
+
|
123
|
+
def empty_directories
|
124
|
+
Dir[ File.join( Dir.pwd, root, '**/*' ) ].select do |d|
|
125
|
+
File.directory?(d) && (Dir.entries(d) - ['.','..']).empty?
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return [Array] the pushed files (only those updated on remote)
|
130
|
+
def push
|
131
|
+
list_file = create_list_file( list(:local).join("\n") )
|
132
|
+
ensure_local_file_permissions
|
133
|
+
push_list = map_rsync_output_to_list rsync_files_to_remote(list_file)
|
134
|
+
list_file.unlink
|
135
|
+
|
136
|
+
push_list = resolve_conflicts(push_list)
|
137
|
+
puts "#{push_list.length} files pushed to #{branch}"
|
138
|
+
return push_list
|
139
|
+
end
|
140
|
+
|
141
|
+
def rsync_files_to_remote list_file
|
142
|
+
resp = Shell.run_local "rsync -aviz --files-from=#{list_file.path} " +
|
143
|
+
"-e ssh #{root}/ #{config.host}:#{config.root}"
|
144
|
+
|
145
|
+
raise "rsync failed\n#{resp.stderr}" unless resp.success?
|
146
|
+
return resp.stdout
|
147
|
+
end
|
148
|
+
|
149
|
+
def ensure_local_file_permissions
|
150
|
+
list_local_files.each do |file|
|
151
|
+
Shell.run_local "chmod 644 #{File.join( root, file )}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Take files from the cdn and move them locally
|
156
|
+
# @return [Array] list of files pulled
|
157
|
+
def pull
|
158
|
+
# create list for rsync
|
159
|
+
list_file = create_list_file( list(:remote).join("\n") )
|
160
|
+
pull_list = map_rsync_output_to_list rsync_files_from_remote(list_file)
|
161
|
+
list_file.unlink
|
162
|
+
|
163
|
+
puts "#{pull_list.length} files pulled from #{branch}"
|
164
|
+
return pull_list
|
165
|
+
end
|
166
|
+
|
167
|
+
def rsync_files_from_remote list_file
|
168
|
+
resp = Shell.run_local "rsync -aviz --files-from=#{list_file.path}" +
|
169
|
+
" -e ssh #{config.host}:#{config.root} #{root}/"
|
170
|
+
|
171
|
+
raise "rsync failed\n#{resp.stderr}" unless resp.success?
|
172
|
+
return resp.stdout
|
173
|
+
end
|
174
|
+
|
175
|
+
# @return [Array] resolved list
|
176
|
+
def resolve_conflicts push_list
|
177
|
+
conflicts = list(:local) - push_list
|
178
|
+
return push_list if conflicts.empty?
|
179
|
+
|
180
|
+
puts "these local files were not pushed by rsync:"
|
181
|
+
puts conflicts.join("\n")
|
182
|
+
if Shell.get_input("add to the #{branch} manifest? (y|n) ") == "y"
|
183
|
+
push_list.push(*conflicts)
|
184
|
+
end
|
185
|
+
return push_list
|
186
|
+
end
|
187
|
+
|
188
|
+
# creates a temp file with contens and returns it's path
|
189
|
+
# @param contents [String] content for the file
|
190
|
+
# @return [String] file path
|
191
|
+
def create_list_file contents
|
192
|
+
file = Tempfile.new( "list_file" )
|
193
|
+
file.write contents
|
194
|
+
file.close
|
195
|
+
|
196
|
+
return file
|
197
|
+
end
|
198
|
+
|
199
|
+
# @param output [String]
|
200
|
+
# @return [Array]
|
201
|
+
def map_rsync_output_to_list output
|
202
|
+
return output.split("\n").keep_if do |f|
|
203
|
+
f.match( /^(?:<|>)f/ ) && !f.match(/\/$/ )
|
204
|
+
end.map do |f|
|
205
|
+
f.split()[1]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def list_local_files
|
210
|
+
Dir[ File.join( Dir.pwd, root, '**/*' ) ].keep_if do |f|
|
211
|
+
!f.match(/\.manifest$/) && !File.directory?(f)
|
212
|
+
end.map do |f|
|
213
|
+
f.sub( "#{File.join( Dir.pwd, root )}/", "" )
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def list_remote_files
|
218
|
+
manifest.files
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module BranchableCDNAssets
|
2
|
+
class FileManager
|
3
|
+
module Checks
|
4
|
+
|
5
|
+
def manifest_list_clean_for_master
|
6
|
+
manifests = Dir[ File.join( root, "*.manifest" ) ]
|
7
|
+
if Asgit.current_branch == "master" && manifests.length > 1
|
8
|
+
raise "there should only be a production manifest on the master branch\n" +
|
9
|
+
"consider cleaning up with rake cdn:move_to_production[branch_name]"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def local_file_conflict
|
14
|
+
intersection = list(:both)
|
15
|
+
if !intersection.empty?
|
16
|
+
puts "#{intersection.length} files are duplicated locally and on the remote"
|
17
|
+
unless Shell.get_input("do you want to continue? (y|n)") == "y"
|
18
|
+
puts "the conflicting files: \n#{intersection.join("\n")}"
|
19
|
+
abort
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def ready_for_production
|
25
|
+
if Asgit.current_branch != "master"
|
26
|
+
puts "you shouldn't move to production except from master"
|
27
|
+
abort
|
28
|
+
end
|
29
|
+
if !Asgit.remote_up_to_date?
|
30
|
+
puts "make sure you've pulled all remote changes"
|
31
|
+
abort
|
32
|
+
end
|
33
|
+
if !list(:local).empty?
|
34
|
+
puts "push all your local files and commit first"
|
35
|
+
abort
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|