branchable_cdn_assets 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,63 @@
1
+ require 'set'
2
+
3
+ module BranchableCDNAssets
4
+ class Manifest
5
+
6
+ attr_reader :source_file, :file_set
7
+
8
+ def initialize source_file
9
+ @source_file = source_file
10
+ @file_set = File.exists?(source_file) ? read_source_file : Set.new
11
+ end
12
+
13
+ # list files set as an Array
14
+ # @return [Array]
15
+ def files
16
+ file_set.to_a
17
+ end
18
+
19
+ # add new files to the manifest set
20
+ # @param new_files [Array]
21
+ # @return [Set]
22
+ def merge_files new_files
23
+ file_set.merge Array(new_files)
24
+ end
25
+
26
+ # remove a set of files from the manifest
27
+ # @param files_to_remove [Array]
28
+ # @return [Set]
29
+ def remove_files files_to_remove
30
+ file_set.subtract Array(files_to_remove)
31
+ end
32
+
33
+ # updates the manifest file with the
34
+ # most recent set of files
35
+ # @return [Void]
36
+ def update_source_file!
37
+ destroy! && return if file_set.empty?
38
+
39
+ File.open( source_file, 'w' ) do |file|
40
+ file.write manifest_content
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # read the manifest source_file
47
+ def read_source_file
48
+ IO.read(source_file).split("\n").to_set
49
+ end
50
+
51
+ # render/serialize the manifest set
52
+ def manifest_content
53
+ file_set.to_a.join("\n")
54
+ end
55
+
56
+ # remove the manifest source_file
57
+ def destroy!
58
+ File.delete(source_file)
59
+ true
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,69 @@
1
+ require 'rake'
2
+ require 'branchable_cdn_assets'
3
+
4
+ module BranchableCDNAssets
5
+
6
+ class RakeTasks
7
+ include ::Rake::DSL
8
+
9
+ class << self
10
+ def register namespace, data={}
11
+ RakeTasks.new( namespace, Config.new(data) ).register_tasks
12
+ end
13
+ end
14
+
15
+ attr_reader :file_manager, :rake_namespace
16
+
17
+ def initialize namespace, config
18
+ @file_manager = FileManager.new config
19
+ @rake_namespace = namespace
20
+ end
21
+
22
+ def tasks
23
+ {
24
+ list: 'list of local files',
25
+ pull!: 'move the current branch\'s remote files to local',
26
+ push!: 'move local files to the current branch\'s remote',
27
+ prune!: 'remove local files with the same name as remote files',
28
+ move_to_production: 'move named branch files to production cdn'
29
+ }
30
+ end
31
+
32
+ def register_tasks
33
+ tasks.each do |name, desc|
34
+ if self.respond_to?(:"register_#{name}")
35
+ self.send(:"register_#{name}", desc)
36
+ else
37
+ register_task name, desc
38
+ end
39
+ end
40
+ end
41
+
42
+ def register_move_to_production task_desc
43
+ in_namespace do
44
+ desc task_desc
45
+ task :move_to_production, :branch do |t,args|
46
+ puts file_manager.with_check(:move_to_production, args[:branch])
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def in_namespace &block
54
+ namespace rake_namespace do
55
+ yield
56
+ end
57
+ end
58
+
59
+ def register_task task_name, task_desc
60
+ in_namespace do
61
+ desc task_desc
62
+ task task_name.to_s.sub('!','') do
63
+ puts file_manager.public_send( :with_check, task_name )
64
+ end
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,17 @@
1
+ module BranchableCDNAssets
2
+ module Shell
3
+
4
+ class << self
5
+ include HereOrThere
6
+
7
+ # get input from the command line
8
+ # @return [String]
9
+ def get_input message
10
+ print message
11
+ $stdin.gets.chomp
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module BranchableCDNAssets
2
+ VERSION = "0.5.0"
3
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe BranchableCDNAssets::CheckBefore do
4
+
5
+ class Stub
6
+ extend BranchableCDNAssets::CheckBefore
7
+
8
+ def hello; 'world'; end
9
+ def filter; true; end
10
+ end
11
+
12
+ after :each do
13
+ Stub.remove_instance_variable :@_before_checks
14
+ end
15
+
16
+ describe "::before_checks" do
17
+ it "returns an array if @_before_checks not set" do
18
+ expect( Stub.before_checks.is_a?(Array) ).to be_truthy
19
+ end
20
+
21
+ it "returs @_before_checks if set" do
22
+ Stub.instance_variable_set :@_before_checks, ['hello']
23
+ expect( Stub.before_checks ).to match_array ['hello']
24
+ end
25
+ end
26
+
27
+ describe "::check_before" do
28
+ it "adds hash with params to before_checks" do
29
+ Stub.check_before :check, {hello: 'world'}
30
+ expect( Stub.before_checks.first ).to eq check: :check, hello: 'world'
31
+ end
32
+ end
33
+
34
+ describe "#with_check" do
35
+
36
+ it "sends the the called method" do
37
+ stub = Stub.new
38
+ expect( stub ).to receive(:hello)
39
+ stub.with_check( :hello )
40
+ end
41
+
42
+ context "with check defined on method" do
43
+ before :each do
44
+ Stub.instance_eval do; check_before :filter, methods: [:hello]; end
45
+ end
46
+
47
+ it "runs included checks" do
48
+ stub = Stub.new
49
+ expect( stub ).to receive(:filter)
50
+ stub.with_check(:hello)
51
+ end
52
+
53
+ it "runs the called method" do
54
+ stub = Stub.new
55
+ expect( stub ).to receive(:hello)
56
+ stub.with_check(:hello)
57
+ end
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,152 @@
1
+ require 'spec_helper'
2
+
3
+ describe BranchableCDNAssets::Config do
4
+
5
+ let(:data) do
6
+ {
7
+ production_branch: 'production',
8
+ default_env: 'default',
9
+ dir: 'cdn_dir',
10
+ environments: {
11
+ production: {
12
+ host: 'production',
13
+ url: 'http://production.com',
14
+ root: '/var/www/production'
15
+ },
16
+ test: {
17
+ host: 'test',
18
+ url: 'http://test.com',
19
+ root: '/var/www/test'
20
+ },
21
+ staging: {
22
+ host: 'staging',
23
+ url: 'http://staging.com',
24
+ root: '/var/www/staging'
25
+ },
26
+ default: {
27
+ host: 'default',
28
+ url: 'http://default.com',
29
+ root: '/var/www/default'
30
+ }
31
+ },
32
+ cloudfront: {
33
+ access_key: 'FooBar',
34
+ secret_key: 'SecretBaz',
35
+ distribution: 'Distro',
36
+ path_prefix: '/'
37
+ }
38
+ }
39
+ end
40
+
41
+ let(:yaml) do
42
+ "foo: bar\n" +
43
+ "baz: wu\n"
44
+ end
45
+
46
+ describe "#raw_data" do
47
+ context "when given a hash" do
48
+ it "uses hash data as is" do
49
+ expect( described_class.new( data ).raw_data ).to eq data
50
+ end
51
+ it "symbolizes data's keys if they are strings" do
52
+ expect( described_class.new( {'foo'=>'foo', 'bar' => { 'baz' => 'baz' }} ).raw_data ).to eq foo: 'foo', bar: { baz: 'baz' }
53
+ end
54
+ end
55
+ context "when given a path" do
56
+ it "reads yaml data in config" do
57
+ allow(File).to receive(:exists?).with('yaml').and_return(true)
58
+ allow(IO).to receive(:read).with('yaml').and_return(yaml)
59
+
60
+ expect( described_class.new('yaml').raw_data ).to eq foo: 'bar', baz: 'wu'
61
+ end
62
+ it "raises exception if file not found" do
63
+ allow(File).to receive(:exists?).with('yaml').and_return(false)
64
+
65
+ expect{
66
+ described_class.new('yaml')
67
+ }.to raise_error
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "#branch" do
73
+ it "defaults to the Asgit.current_branch" do
74
+ allow( Asgit ).to receive(:current_branch).and_return('foo_bar')
75
+ expect( described_class.new( data ).branch ).to eq 'foo_bar'
76
+ end
77
+ it "can be overriden by passing in a branch" do
78
+ expect( described_class.new( data, 'baz' ).branch ).to eq 'baz'
79
+ end
80
+ end
81
+
82
+ describe "#production_branch" do
83
+ it "defaults to 'master' if none is passed" do
84
+ expect( described_class.new( data.without_key(:production_branch) ).production_branch ).to eq 'master'
85
+ end
86
+ it "can be set with data" do
87
+ expect( described_class.new( data ).production_branch ).to eq 'production'
88
+ end
89
+ end
90
+
91
+ describe "#cloudfront" do
92
+ it "is set to the passed value" do
93
+ expect( described_class.new( data ).cloudfront ).to eq data[:cloudfront]
94
+ end
95
+ end
96
+
97
+ describe "#cdn_dir" do
98
+ it "is set to the passed value" do
99
+ expect( described_class.new( data ).cdn_dir ).to eq 'cdn_dir'
100
+ end
101
+
102
+ it "falls back to 'cdn' if no value is passed" do
103
+ expect( described_class.new( data.without_key(:dir) ).cdn_dir ).to eq 'cdn'
104
+ end
105
+ end
106
+
107
+ describe "#env" do
108
+ context "when the current branch matches production_branch" do
109
+ before :each do
110
+ allow( Asgit ).to receive(:current_branch).and_return('production')
111
+ end
112
+ it "is :production" do
113
+ expect( described_class.new( data ).env ).to eq :production
114
+ end
115
+ end
116
+ context "when on a branch with a matching environment key" do
117
+ before :each do
118
+ allow( Asgit ).to receive(:current_branch).and_return('test')
119
+ end
120
+ it "matchs the current_branch" do
121
+ expect( described_class.new( data ).env ).to eq :test
122
+ end
123
+ end
124
+ context "when on a branch that doesn't match an env key" do
125
+ before :each do
126
+ allow( Asgit ).to receive(:current_branch).and_return('random')
127
+ end
128
+ it "sets env to the set default_env" do
129
+ expect( described_class.new( data ).env ).to eq :default
130
+ end
131
+ it "falls back to 'staging' if no default env given" do
132
+ expect( described_class.new( data.without_key(:default_env) ).env ).to eq :staging
133
+ end
134
+ end
135
+ end
136
+
137
+ describe "#environments" do
138
+ it "returns data namespaced with branch" do
139
+ production = described_class.new( data, 'production' )
140
+ default = described_class.new( data, 'foo_bar' )
141
+
142
+ expect( production.root ).to eq '/var/www/production'
143
+ expect( production.host ).to eq 'production'
144
+ expect( production.url ).to eq 'http://production.com'
145
+
146
+ expect( default.root ).to eq '/var/www/default/foo_bar/'
147
+ expect( default.host ).to eq 'default'
148
+ expect( default.url ).to eq 'http://default.com/foo_bar'
149
+ end
150
+ end
151
+
152
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe BranchableCDNAssets::FileManager do
4
+
5
+ describe "#find" do
6
+
7
+ before :each do
8
+ Given.fixture 'base'
9
+ Given.file 'cdn/master.manifest', "image_one\nimage_remote_one\n"
10
+ Given.file 'cdn/staging.manifest', "image_one\nimage_remote_two\n"
11
+ Given.file 'cdn/image_one', ''
12
+ Given.file 'cdn/image_two', ''
13
+ end
14
+
15
+ context "when on production branch" do
16
+ before :each do
17
+ allow( Asgit ).to receive(:current_branch)
18
+ .and_return('master')
19
+ @manager = described_class.new BranchableCDNAssets::Config.new('config/cdn_assets.yaml')
20
+ end
21
+
22
+ it "returns :local if asset is local" do
23
+ expect( @manager.find('image_one') ).to eq :local
24
+ end
25
+
26
+ it "returns the production url of the asset" do
27
+ expect( @manager.find('image_remote_one') ).to eq 'http://production.com/image_remote_one'
28
+ end
29
+
30
+ it "returns nil if asset is missing" do
31
+ expect( @manager.find('missing') ).to be_nil
32
+ end
33
+ end
34
+
35
+ context "when on staging branch" do
36
+ before :each do
37
+ allow( Asgit ).to receive(:current_branch)
38
+ .and_return('staging')
39
+ @manager = described_class.new BranchableCDNAssets::Config.new('config/cdn_assets.yaml')
40
+ end
41
+
42
+ it "returns :local if asset is local" do
43
+ expect( @manager.find('image_one') ).to eq :local
44
+ end
45
+
46
+ it "returns namespaced staging url if listed in staging manifest" do
47
+ expect( @manager.find('image_remote_two') ).to eq 'http://staging.com/staging/image_remote_two'
48
+ end
49
+
50
+ it "returns production url if listed only in production manifest" do
51
+ expect( @manager.find('image_remote_one') ).to eq 'http://production.com/image_remote_one'
52
+ end
53
+
54
+ it "returns nil if asset is missing" do
55
+ expect( @manager.find('missing') ).to be_nil
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,256 @@
1
+ require 'spec_helper'
2
+
3
+ describe BranchableCDNAssets::FileManager do
4
+
5
+ before :each do
6
+ @config_data = {
7
+ env: 'default',
8
+ branch: 'branch',
9
+ cdn_dir: 'cdn',
10
+ host: 'host',
11
+ root: 'root',
12
+ cloudfront: {
13
+ path_prefix: '/prefix'
14
+ }
15
+ }
16
+ @config = instance_double( "BranchableCDNAssets::Config", @config_data )
17
+ allow( BranchableCDNAssets::Config ).to receive(:new).and_return( @config )
18
+
19
+ @file_list = [
20
+ 'filename_one',
21
+ 'filename_two',
22
+ 'directory/dir_file'
23
+ ]
24
+
25
+ allow( Dir).to receive(:[]).with(anything()).and_call_original
26
+ allow( Dir).to receive(:[]).with( "#{Dir.pwd}/cdn/**/*" ).and_return( @file_list.map { |f| File.join( Dir.pwd, 'cdn', f ) } )
27
+
28
+ @manifest = instance_double("BranchableCDNAssets::Manifest", files: @file_list )
29
+ allow( BranchableCDNAssets::Manifest ).to receive(:new).and_return( @manifest )
30
+
31
+ @cloudfront = instance_double("BranchableCDNAssets::Cloudfront")
32
+ allow( BranchableCDNAssets::Cloudfront ).to receive(:new).and_return( @cloudfront )
33
+
34
+ @tempfile = instance_double( "Tempfile",
35
+ unlink: nil,
36
+ path: 'tempfile_path',
37
+ write: nil,
38
+ close: nil
39
+ )
40
+ allow( Tempfile ).to receive(:new).and_return( @tempfile )
41
+
42
+ @success_response = instance_double("HereOrThere::Response",
43
+ success?: true,
44
+ stderr: "",
45
+ stdout: ">f+++++++++ filename_one\n" +
46
+ ">f+++++++++ filename_two\n" +
47
+ ">d+++++++++ directory/\n" +
48
+ ">f+++++++++ directory/dir_file\n"
49
+ )
50
+
51
+ @error_response = instance_double("HereOrThere::Response",
52
+ success?: false,
53
+ stderr: "stderr",
54
+ stdout: ""
55
+ )
56
+ end
57
+
58
+ describe '#config' do
59
+ it "returns the provided config" do
60
+ files = described_class.new( @config )
61
+ expect( files.config ).to eq @config
62
+ end
63
+ end
64
+
65
+ describe '#root' do
66
+ it "returns the provided root from config" do
67
+ files = described_class.new( @config )
68
+ expect( files.root ).to eq 'cdn'
69
+ end
70
+ end
71
+
72
+ describe '#manifest' do
73
+ it "returns a manifest with the environment manifest path" do
74
+ expect( BranchableCDNAssets::Manifest ).to receive(:new).with( "cdn/#{@config.branch}.manifest" )
75
+ files = described_class.new( @config )
76
+ expect( files.manifest ).to eq @manifest
77
+ end
78
+ end
79
+
80
+ describe "#branch" do
81
+ it "is defaults to the config.branch" do
82
+ expect( described_class.new(@config).branch ).to eq @config.branch
83
+ end
84
+ it "can be overriden by passed arg" do
85
+ expect( described_class.new(@config, 'foo').branch ).to eq 'foo'
86
+ end
87
+ end
88
+
89
+ describe '#list' do
90
+
91
+ let(:remote_files) { ['filename_one', 'remote_one', 'remote_two'] }
92
+ let(:local_files) { @file_list }
93
+
94
+ before :each do
95
+ allow( @manifest ).to receive(:files).and_return(remote_files)
96
+ end
97
+
98
+ it "returns remote files when arg is :remote" do
99
+ expect( described_class.new(@config).list(:remote) ).to match_array remote_files
100
+ end
101
+
102
+ it "returns local files when arg is :local" do
103
+ expect( described_class.new(@config).list(:local) ).to match_array local_files
104
+ end
105
+
106
+ it "returns both local and remote files when :all" do
107
+ expect( described_class.new(@config).list() ).to match_array remote_files + local_files
108
+ end
109
+
110
+ it "returns intersecting files whe :both" do
111
+ expect( described_class.new(@config).list(:both) ).to match_array ['filename_one']
112
+ end
113
+
114
+ end
115
+
116
+ describe "#pull!" do
117
+
118
+ context "when pull is successful" do
119
+ before :each do
120
+ allow( BranchableCDNAssets::Shell ).to receive(:run_local).and_return( @success_response )
121
+ allow( @manifest ).to receive(:remove_files)
122
+ allow( @manifest ).to receive(:update_source_file!)
123
+ end
124
+
125
+ it "returns list of pulled files" do
126
+ expect( described_class.new(@config).pull! ).to match_array @file_list
127
+ end
128
+
129
+ it "removes files & updates manifest" do
130
+ expect( @manifest ).to receive(:remove_files).with(@file_list)
131
+ expect( @manifest ).to receive(:update_source_file!)
132
+ described_class.new(@config).pull!
133
+ end
134
+ end
135
+
136
+ context "when pull returns error" do
137
+ it "raises an error" do
138
+ allow( BranchableCDNAssets::Shell ).to receive(:run_local).and_return( @error_response )
139
+
140
+ expect{
141
+ described_class.new(@config).pull!
142
+ }.to raise_error
143
+ end
144
+ end
145
+ end
146
+
147
+ describe "#push!" do
148
+
149
+ before :each do
150
+ allow( BranchableCDNAssets::Shell ).to receive(:run_remote)
151
+ allow( BranchableCDNAssets::Shell ).to receive(:run_local).and_return( @success_response )
152
+ allow( @manifest ).to receive(:merge_files)
153
+ allow( @manifest ).to receive(:update_source_file!)
154
+ end
155
+
156
+ context "when setup_remote is successful" do
157
+ before :each do
158
+ allow( BranchableCDNAssets::Shell ).to receive(:run_remote).with('mkdir -p root', hostname: 'host').and_return(@success_response)
159
+ end
160
+
161
+ it "sets up remote" do
162
+ expect( BranchableCDNAssets::Shell ).to receive(:run_remote).with('mkdir -p root', hostname: 'host').and_return(@success_response)
163
+ described_class.new(@config).push!
164
+ end
165
+
166
+ it "returns an array of pushed files" do
167
+ expect( described_class.new(@config).push! ).to match_array @file_list
168
+ end
169
+
170
+ it "adds files and updates the manifest" do
171
+ expect( @manifest ).to receive(:merge_files).with(@file_list)
172
+ expect( @manifest ).to receive(:update_source_file!)
173
+
174
+ described_class.new(@config).push!
175
+ end
176
+
177
+ context "when env is production" do
178
+
179
+ before :each do
180
+ allow( @config ).to receive(:env).and_return(:production)
181
+ end
182
+
183
+ it "invalidates intersecting files" do
184
+ expect( @cloudfront ).to receive(:invalidate_files).with( @file_list.map { |f| "/prefix/#{f}"} )
185
+ described_class.new(@config).push!
186
+ end
187
+ end
188
+ end
189
+
190
+ context "when setup_remote fails" do
191
+ it "aborts and prints error" do
192
+ allow( BranchableCDNAssets::Shell ).to receive(:run_remote).and_return( @error_response )
193
+ expect {
194
+ described_class.new(@config).push!
195
+ }.to raise_error
196
+ end
197
+ end
198
+ end
199
+
200
+ describe "#prune!" do
201
+
202
+ let(:remote_files) { ['filename_one', 'remote_one', 'remote_two'] }
203
+ let(:local_files) { @file_list }
204
+
205
+ before :each do
206
+ allow( @manifest ).to receive(:files).and_return(remote_files)
207
+ end
208
+
209
+ context "when confirm removal" do
210
+ it "removes files that are local and on remote" do
211
+ expect( BranchableCDNAssets::Shell ).to receive(:run_local).with("rm cdn/filename_one")
212
+ release_stdin 'y' do
213
+ described_class.new(@config).prune!
214
+ end
215
+ end
216
+
217
+ it "removes empty directories" do
218
+ files = described_class.new(@config)
219
+ expect( files ).to receive(:remove_empty_directories)
220
+ release_stdin 'y' do
221
+ files.prune!
222
+ end
223
+ end
224
+ end
225
+
226
+ context "when removal aborted" do
227
+ it "does not remove files and exits" do
228
+ expect( BranchableCDNAssets::Shell ).not_to receive(:run_local).with(/rm\s[\w\/\.]+$/)
229
+
230
+ expect{
231
+ release_stdin 'n' do
232
+ described_class.new(@config).prune!
233
+ end
234
+ }.to raise_error SystemExit
235
+ end
236
+ end
237
+ end
238
+
239
+ describe "#move_to_production" do
240
+ before :each do
241
+ @orig_cdn = described_class.new(@config)
242
+ @branch_cdn = instance_double("BranchableCDNAssets::FileManager", manifest: @manifest )
243
+ allow( BranchableCDNAssets::Shell ).to receive(:run_local).and_return(@success_response)
244
+ allow( described_class ).to receive(:new).and_call_original
245
+ allow( @config ).to receive(:raw_data)
246
+ allow( described_class ).to receive(:new).with( @config ).and_return( @branch_cdn )
247
+ end
248
+
249
+ it "pulls from remote branch & pushes on current branch" do
250
+ expect( @branch_cdn ).to receive(:pull!).and_return([])
251
+ expect( @orig_cdn ).to receive(:push!).and_return([])
252
+ @orig_cdn.move_to_production('test')
253
+ end
254
+ end
255
+
256
+ end