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