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 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