pipely 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ffc556ad3308b03b691185eb21f4ee10f26592f9
4
- data.tar.gz: caf8a2a7b8c63c281979bde1ad75f75bdc0f963f
3
+ metadata.gz: 84180497ce48add0459eec37acfdb95bf66ebe05
4
+ data.tar.gz: d39f9fef3430493934320d8a88b38a2ced74b42f
5
5
  SHA512:
6
- metadata.gz: 3b6a6b168162ec377f29e5f416a956fa90c3a1d7a21c462f5104d0ebf8db11d340e95b90ddbaf6a5de74f21673cb15084e2a99eb156d200b69acc54d6243bd06
7
- data.tar.gz: 7b659bdb5a01ebe0b4306a3a8c8ec7c426df709e3e56df95a50c1f7134fa3c7d5235efd887b187d1ebbf1c545c2d57d50c33bcb6fa4599a167e6b5930271eda6
6
+ metadata.gz: dae88dbeb0ed78e2197b90cba2601ed218e7bfc8787e5ca4a28618e5b167b2e9d010bfe378670b8b4a8d688f7b624f44abc123f75173046fbf25e3afa1d69555
7
+ data.tar.gz: acbb97a002b4f611be91bd2768e25ed75637e9eba962a632d04a4203bfb2db35ca4141d03fe14dfed196313c53e4f49e4945187e3b4b9d8fbc2a9f64d84a0866
@@ -0,0 +1,29 @@
1
+ require 'pipely/bundler/bundle'
2
+ require 'pipely/bundler/gem_packager'
3
+ require 'pipely/bundler/project_gem'
4
+
5
+ module Pipely
6
+
7
+ #
8
+ # Module for packaging up a gem project and its dependencies, as they exist
9
+ # on your machine, for deployment.
10
+ #
11
+ # None of this code is specific to AWS Data Pipelines, and it could be used
12
+ # anywhere else you want to an in-development gem with frozen dependencies.
13
+ #
14
+ module Bundler
15
+
16
+ # List all the gems used in this project in the format:
17
+ #
18
+ # { name => path_to_cache_file }
19
+ #
20
+ # For gems that are git- or path-sourced, it will first build a fresh cache
21
+ # file for the gem.
22
+ #
23
+ def self.gem_files(vendor_dir='vendor/pipeline')
24
+ ProjectGem.load(vendor_dir).gem_files
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,73 @@
1
+ require 'fileutils'
2
+
3
+ module Pipely
4
+ module Bundler
5
+
6
+ #
7
+ # Provides access to a bundle's list of gems
8
+ #
9
+ class Bundle
10
+
11
+ attr_reader :spec_set
12
+
13
+ SOURCE_TYPES = %w[Bundler::Source::Git Bundler::Source::Path]
14
+
15
+ def self.build(vendor_dir,
16
+ groups=[:default],
17
+ definition=::Bundler.definition)
18
+ new(
19
+ vendor_dir,
20
+ definition.specs_for(groups),
21
+ definition.instance_variable_get(:@locked_sources)
22
+ )
23
+ end
24
+
25
+ def initialize(vendor_dir, spec_set, locked_sources)
26
+ @spec_set = spec_set
27
+ @locked_sources = locked_sources
28
+ @vendor_dir = vendor_dir
29
+ unless Dir.exists? @vendor_dir
30
+ FileUtils.mkdir_p(@vendor_dir)
31
+ end
32
+
33
+ end
34
+
35
+ def gem_files(gem_packager=GemPackager.new(@vendor_dir))
36
+ gem_files = {}
37
+
38
+ @spec_set.to_a.each do |spec|
39
+ gem_files.merge!(gem_file(spec, gem_packager))
40
+ end
41
+
42
+ gem_files
43
+ end
44
+
45
+ private
46
+
47
+ def gem_file(spec, gem_packager)
48
+ if source = locked_sources_by_name[spec.name]
49
+ gem_packager.build_from_source(source.name, source.path)
50
+ else
51
+ gem_packager.package(spec)
52
+ end
53
+ end
54
+
55
+ def locked_sources_by_name
56
+ return @locked_sources_by_name if @locked_sources_by_name
57
+
58
+ @locked_sources_by_name = {}
59
+
60
+ @locked_sources.each do |source|
61
+ # Only include git or path sources.
62
+ if SOURCE_TYPES.include?(source.class.name)
63
+ @locked_sources_by_name[source.name] = source
64
+ end
65
+ end
66
+
67
+ locked_sources_by_name
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,119 @@
1
+ require 'fileutils'
2
+ require 'excon'
3
+
4
+ module Pipely
5
+ module Bundler
6
+
7
+ #
8
+ # Builds cache files for git- or path-sourced gems.
9
+ #
10
+ class GemPackager
11
+
12
+ # Alert upon gem-building failures
13
+ class GemBuildError < RuntimeError ; end
14
+
15
+ # Alert upon gem-fetching failures
16
+ class GemFetchError < RuntimeError ; end
17
+
18
+ def initialize(vendor_dir)
19
+ @vendor_dir = vendor_dir
20
+ unless Dir.exists? @vendor_dir
21
+ FileUtils.mkdir_p(@vendor_dir)
22
+ end
23
+ end
24
+
25
+ def package(spec)
26
+ if vendored_gem = vendor_local_gem(spec)
27
+ vendored_gem
28
+
29
+ # Finally, some gems do not exist in the cache or as source. For
30
+ # instance, json is shipped with the ruby dist. Try to fetch directly
31
+ # from rubygems.
32
+ else
33
+ gem_file_name = "#{spec.name}-#{spec.version}.gem"
34
+ { spec.name => download_from_rubygems(gem_file_name)}
35
+ end
36
+ end
37
+
38
+ def vendor_local_gem(spec)
39
+ gem_file = spec.cache_file
40
+ vendored_gem = File.join( @vendor_dir, File.basename(gem_file) )
41
+
42
+ if File.exists?(vendored_gem)
43
+ { spec.name => vendored_gem }
44
+
45
+ # Gem exists in the local ruby gems cache
46
+ elsif File.exists? gem_file
47
+
48
+ # Copy to vendor dir
49
+ FileUtils.cp(gem_file, vendored_gem)
50
+
51
+ { spec.name => vendored_gem }
52
+
53
+ # If source exists, build a gem from it
54
+ elsif File.directory?(spec.gem_dir)
55
+ build_from_source(spec.name, spec.gem_dir)
56
+ else
57
+ nil
58
+ end
59
+ end
60
+
61
+ def build_from_source(spec_name, source_path)
62
+ gem_spec_path = "#{spec_name}.gemspec"
63
+
64
+ # Build the gemspec
65
+ gem_spec = Gem::Specification::load(
66
+ File.join(source_path,gem_spec_path))
67
+
68
+ gem_file = build_gem(spec_name, source_path)
69
+
70
+ # Move to vendor dir
71
+ FileUtils.mv(
72
+ File.join(source_path,gem_file),
73
+ File.join(@vendor_dir,gem_file))
74
+
75
+ { gem_spec.name => File.join(@vendor_dir, gem_file) }
76
+ end
77
+
78
+ def build_gem(spec_name, source_path)
79
+ gem_spec_path = "#{spec_name}.gemspec"
80
+
81
+ Dir.chdir(source_path) do
82
+ result = `gem build #{gem_spec_path} 2>&1`
83
+
84
+ if result =~ /ERROR/i
85
+ raise GemBuildError.new(
86
+ "Failed to build #{gem_spec_path} \n" << result)
87
+ else
88
+ result.scan(
89
+ /File:(.+.gem)$/).flatten.first.strip
90
+ end
91
+ end
92
+ end
93
+
94
+ def download_from_rubygems(gem_file_name)
95
+ vendored_gem = File.join( @vendor_dir, gem_file_name )
96
+
97
+ # XXX: add link on wiki details what is going on here
98
+ puts "Fetching gem #{gem_file_name} directly from rubygems, most " +
99
+ "likely this gem was packaged along with your ruby " +
100
+ "distrubtion, for more details see LINK"
101
+
102
+ ruby_gem_url = "https://rubygems.org/downloads/#{gem_file_name}"
103
+ response = Excon.get( ruby_gem_url, {
104
+ middlewares: Excon.defaults[:middlewares] +
105
+ [Excon::Middleware::RedirectFollower]
106
+ })
107
+
108
+ if response.status == 200
109
+ File.open(vendored_gem, 'w') { |file| file.write( response.body ) }
110
+ return vendored_gem
111
+ else
112
+ raise GemFetchError.new(
113
+ "Failed to find #{gem_file_name} at rubygems, recieved
114
+ #{response.status} with #{response.body}" )
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,47 @@
1
+ module Pipely
2
+ module Bundler
3
+
4
+ #
5
+ # Builds the project's gem from gemspec and pulls in its dependencies via
6
+ # the gem's bundle.
7
+ #
8
+ class ProjectGem
9
+
10
+ attr_reader :project_spec
11
+
12
+ def self.load(vendor_dir)
13
+ if gem_spec = Dir.glob("*.gemspec").first
14
+ # project gem spec
15
+ new(Gem::Specification::load(gem_spec), vendor_dir)
16
+ else
17
+ raise "Failed to find gemspec"
18
+ end
19
+ end
20
+
21
+ def initialize(project_spec, vendor_dir)
22
+ @project_spec = project_spec
23
+ @vendor_dir = vendor_dir
24
+ unless Dir.exists? @vendor_dir
25
+ FileUtils.mkdir_p(@vendor_dir)
26
+ end
27
+ end
28
+
29
+ def gem_files
30
+ # Project gem should be at the bottom of the dep list
31
+ @gem_files ||= dependency_gem_files.merge(project_gem_file)
32
+ end
33
+
34
+ def dependency_gem_files(bundle=Bundle.build(@vendor_dir))
35
+ # Always exclude bundler and the project gem
36
+ gems_to_exclude = [ @project_spec.name, 'bundler' ]
37
+
38
+ bundle.gem_files.reject { |name, path| gems_to_exclude.include?(name) }
39
+ end
40
+
41
+ def project_gem_file(gem_packager=GemPackager.new(@vendor_dir))
42
+ gem_packager.build_from_source(@project_spec.name, Dir.pwd)
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -1,115 +1,35 @@
1
+ require 'pipely/bundler'
2
+ require 'pipely/deploy/bootstrap_context'
3
+ require 'pipely/deploy/s3_uploader'
4
+
1
5
  module Pipely
2
6
  module Deploy
3
7
 
4
8
  # Helps bootstrap a pipeline
5
9
  class Bootstrap
6
10
 
7
- attr_reader :bucket_name
8
- attr_reader :s3_gems_path
9
11
  attr_reader :project_spec
10
12
  attr_reader :gem_files
11
13
 
12
- def initialize(s3_bucket, s3_gems_path)
13
- @s3_bucket = s3_bucket
14
- @bucket_name = s3_bucket.name
15
- @s3_gems_path = s3_gems_path
14
+ def initialize(s3_uploader)
15
+ @s3_uploader = s3_uploader
16
16
  end
17
17
 
18
18
  # Builds the project's gem from gemspec, uploads the gem to s3, and
19
19
  # uploads all the gem dependences to S3
20
20
  def build_and_upload_gems
21
-
22
- # placeholder, name will be nil if a gemspec is not defined
23
- project_gem_name = nil
24
-
25
- gem_spec = Dir.glob("*.gemspec").first
26
- if gem_spec
27
-
28
- # Build pipeline gem
29
- @project_spec = Gem::Specification::load(gem_spec)
30
- # build the gem
31
- project_gem_file =
32
- `gem build ./#{gem_spec}`.scan(
33
- /File:(.+.gem)$/).flatten.first.strip
34
- project_gem_name = @project_spec.name
35
- upload_gem(project_gem_file)
36
- end
37
-
38
- @gem_files = upload_gems_from_bundler(project_gem_name)
39
-
40
- # project gem has to be loaded last
41
- @gem_files << project_gem_file if @project_spec
21
+ @gem_files = Pipely::Bundler.gem_files
22
+ @s3_uploader.upload(@gem_files.values)
42
23
  end
43
24
 
44
- def context
45
- BootstrapContext.new(
46
- @gem_files.map{ |file|
47
- File.join("s3://", @s3_bucket.name, gem_s3_path(file) )
48
- } )
49
- end
50
-
51
- def gem_s3_path(gem_file)
52
- filename = File.basename(gem_file)
53
- File.join(@s3_gems_path, filename)
54
- end
55
-
56
- private
57
-
58
- def s3_gem_exists?( gem_file )
59
- !@s3_bucket.objects[gem_s3_path(gem_file)].nil?
60
- end
61
-
62
- def upload_gem( gem_file )
63
- puts "uploading #{gem_file} to #{gem_s3_path(gem_file)}"
64
- @s3_bucket.objects[gem_s3_path(gem_file)].write(File.open(gem_file))
65
- end
66
-
67
- def upload_gems_from_bundler(project_gem_name)
68
- gem_files = []
69
- Bundler.definition.specs_for([:default]).each do |spec|
70
- # Exclude project from gem deps
71
- unless spec.name == project_gem_name
72
- gem_file = spec.cache_file
73
-
74
- # XXX: Some gems do not exist in the cache, e.g. json. Looks the
75
- # gem is already packaged with the ruby dist
76
- if File.exists? gem_file
77
- gem_files << gem_file
78
-
79
- # only upload gem if it doesnt exist already
80
- unless s3_gem_exists?( gem_file )
81
- upload_gem(gem_file)
82
- end
83
- end
84
- end
25
+ def context(s3_steps_path)
26
+ BootstrapContext.new.tap do |context|
27
+ context.gem_files = @s3_uploader.s3_urls(gem_files.values)
28
+ context.s3_steps_path = s3_steps_path
85
29
  end
86
-
87
- gem_files
88
- end
89
- end
90
-
91
- # Context passed to the erb templates
92
- class BootstrapContext
93
- attr_reader :gem_files
94
-
95
- def initialize(gem_files)
96
- @gem_files = gem_files
97
30
  end
98
31
 
99
- def install_gems_script
100
- script = ""
101
-
102
- @gem_files.each do |gem_file|
103
- filename = File.basename(gem_file)
104
- script << %Q[
105
- # #{filename}
106
- hadoop fs -copyToLocal #{gem_file} /home/hadoop/#{filename}
107
- gem install --local /home/hadoop/#{filename} --no-ri --no-rdoc
108
- ]
109
- end
110
-
111
- script
112
- end
113
32
  end
33
+
114
34
  end
115
35
  end
@@ -0,0 +1,37 @@
1
+
2
+ module Pipely
3
+ module Deploy
4
+
5
+ # Context passed to the erb templates
6
+ class BootstrapContext
7
+ attr_accessor :gem_files
8
+ attr_accessor :s3_steps_path
9
+
10
+ def install_gems_script(transport = :hadoop_fs, &blk)
11
+ script = ""
12
+
13
+ case transport.to_sym
14
+ when :hadoop_fs
15
+ transport_cmd = 'hadoop fs -copyToLocal'
16
+ when :awscli
17
+ transport_cmd = 'aws s3 cp'
18
+ else
19
+ raise "Unsupported transport: #{transport}" unless blk
20
+ end
21
+
22
+ @gem_files.each do |gem_file|
23
+ filename = File.basename(gem_file)
24
+ command = "#{transport_cmd} #{gem_file} #{filename}" if transport_cmd
25
+ command = yield(gem_file, filename, command) if blk
26
+ script << %Q[
27
+ # #{filename}
28
+ #{command}
29
+ gem install --force --local #{filename} --no-ri --no-rdoc
30
+ ]
31
+ end
32
+
33
+ script
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,58 @@
1
+ require 'digest/md5'
2
+
3
+ module Pipely
4
+ module Deploy
5
+
6
+ #
7
+ # Manage syncing of local files to a particular S3 path
8
+ #
9
+ class S3Uploader
10
+
11
+ attr_reader :bucket_name
12
+ attr_reader :s3_path
13
+
14
+ def initialize(s3_bucket, s3_path)
15
+ @s3_bucket = s3_bucket
16
+ @bucket_name = s3_bucket.name
17
+ @s3_path = s3_path
18
+ end
19
+
20
+ def s3_file_path(file)
21
+ filename = File.basename(file)
22
+ File.join(@s3_path, filename)
23
+ end
24
+
25
+ def s3_urls(files)
26
+ files.map do |file|
27
+ File.join("s3://", @s3_bucket.name, s3_file_path(file) )
28
+ end
29
+ end
30
+
31
+ def upload(files)
32
+ files.each do |file|
33
+ upload_file(file)
34
+ end
35
+ end
36
+
37
+ #
38
+ # Upload file to S3 unless ETAGs already match.
39
+ #
40
+ def upload_file(file)
41
+ target_path = s3_file_path(file)
42
+ s3_object = @s3_bucket.objects[target_path]
43
+
44
+ content = File.read(file)
45
+ digest = Digest::MD5.hexdigest(content)
46
+
47
+ if s3_object.exists? && (digest == s3_object.etag.gsub('"', ''))
48
+ puts "skipping #{file} to #{target_path} (ETAG matches)"
49
+ else
50
+ puts "uploading #{file} to #{target_path}"
51
+ s3_object.write(content)
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+ end
@@ -1,5 +1,6 @@
1
1
  require 'rake'
2
2
  require 'aws'
3
+ require 'erubis'
3
4
  require 'pipely/deploy/bootstrap'
4
5
 
5
6
  module Pipely
@@ -59,19 +60,18 @@ module Pipely
59
60
  end
60
61
 
61
62
  def build_bootstrap_context
62
- bootstrap_helper =
63
- Pipely::Deploy::Bootstrap.new(s3_bucket, @s3_gems_path)
63
+ s3_uploader = Pipely::Deploy::S3Uploader.new(s3_bucket, @s3_gems_path)
64
+ bootstrap_helper = Pipely::Deploy::Bootstrap.new(s3_uploader)
64
65
  bootstrap_helper.build_and_upload_gems
65
66
 
66
67
  # erb context
67
68
  {
68
- bootstrap: bootstrap_helper.context,
69
+ bootstrap: bootstrap_helper.context(@s3_steps_path),
69
70
  config: config
70
71
  }
71
72
  end
72
73
 
73
74
  def upload_to_s3( upload_filename, body )
74
-
75
75
  s3_dest = File.join(@s3_steps_path, upload_filename)
76
76
  puts "uploading #{s3_dest}" if verbose
77
77
  s3_bucket.objects[s3_dest].write(body)
@@ -57,11 +57,8 @@ module Pipely
57
57
 
58
58
  def run_task(verbose)
59
59
  with_bucket do |bucket|
60
- step_files.each do |file_name|
61
- dest = file_name.sub(/^#{local_path}/, s3_path)
62
- puts "uploading #{dest}" if verbose
63
- bucket.objects[dest].write(File.read(file_name))
64
- end
60
+ s3_uploader = Pipely::Deploy::S3Uploader.new(bucket, s3_path)
61
+ s3_uploader.upload(step_files)
65
62
  end
66
63
  end
67
64
 
@@ -1,3 +1,3 @@
1
1
  module Pipely
2
- VERSION = "0.7.0" unless defined?(::Pipely::VERSION)
2
+ VERSION = "0.8.0" unless defined?(::Pipely::VERSION)
3
3
  end
@@ -0,0 +1,72 @@
1
+ require 'pipely/bundler/bundle'
2
+
3
+ describe Pipely::Bundler::Bundle do
4
+
5
+ describe ".build" do
6
+ let(:groups) { [ :group1 ] }
7
+ let(:definition) { double "Bundler::Definition" }
8
+ let(:spec_set) { double }
9
+
10
+ before do
11
+ definition.stub(:specs_for).with(groups) { spec_set }
12
+ end
13
+
14
+ it 'builds a Bundle instance with the spec_set' do
15
+ bundle = described_class.build('vendor/test', groups, definition)
16
+ expect(bundle.spec_set).to eq(spec_set)
17
+ end
18
+ end
19
+
20
+ let(:pipely_spec) { double("Gem::Specification", name: "pipely") }
21
+ let(:gem1_spec) { double("Gem::Specification", name: "gem1") }
22
+ let(:gem2_spec) { double("Gem::Specification", name: "gem2") }
23
+
24
+ let(:pipely_source) do
25
+ Bundler::Source::Path.new('name' => "pipely", 'path' => '.')
26
+ end
27
+
28
+ let(:spec_set) { [ pipely_spec, gem1_spec, gem2_spec ] }
29
+ let(:locked_sources) { [ pipely_source ] }
30
+
31
+ subject { described_class.new('vendor/test', spec_set, locked_sources) }
32
+
33
+ describe "#gem_files" do
34
+ let(:gem_packager) { double }
35
+
36
+ before do
37
+ gem_packager.stub(:package).and_return do |spec|
38
+ { spec.name => '/path/to/cache/file.gem' }
39
+ end
40
+
41
+ gem_packager.stub(:build_from_source).and_return do |name, path|
42
+ { name => "#{path}/#{name}-X.Y.Z.gem" }
43
+ end
44
+ end
45
+
46
+ it "returns a cache file for each gem" do
47
+ gem_files = subject.gem_files(gem_packager)
48
+ expect(gem_files.keys).to match_array(%w[ gem1 gem2 pipely ])
49
+ end
50
+
51
+ context "given a packaged/non-locked gem" do
52
+ it "returns the gems and their existing cache files" do
53
+ expect(gem_packager).to receive(:package).with(gem1_spec)
54
+ expect(gem_packager).to receive(:package).with(gem2_spec)
55
+
56
+ subject.gem_files(gem_packager)
57
+ end
58
+ end
59
+
60
+ context "given a locked-source gem" do
61
+ it "should build new cache files from source" do
62
+ expect(gem_packager).to receive(:build_from_source).with(
63
+ pipely_source.name,
64
+ pipely_source.path
65
+ )
66
+
67
+ subject.gem_files(gem_packager)
68
+ end
69
+ end
70
+ end
71
+
72
+ end
@@ -0,0 +1,101 @@
1
+ require 'fileutils'
2
+ require 'pipely/bundler/gem_packager'
3
+
4
+ describe Pipely::Bundler::GemPackager do
5
+
6
+ subject { described_class.new(vendor_path) }
7
+ let(:vendor_path) { 'vendor/test' }
8
+
9
+ before(:each) {
10
+ unless Dir.exists? vendor_path
11
+ FileUtils.mkdir_p 'vendor/test'
12
+ end
13
+ }
14
+
15
+ describe "#package" do
16
+ let(:gem_spec) do
17
+ double("spec",
18
+ name: 'test',
19
+ cache_file: 'a/cache/file',
20
+ gem_dir:'a/gem/dir',
21
+ version:'0.0.1'
22
+ )
23
+ end
24
+
25
+ let(:vendored_gem) { "vendor/test/file" }
26
+
27
+ context "with a cache file" do
28
+ before do
29
+ allow(File).to receive(:exists?).with(vendored_gem) { false }
30
+ allow(File).to receive(:exists?).with(gem_spec.cache_file) { true }
31
+ allow(FileUtils).to receive(:cp).with(
32
+ gem_spec.cache_file, vendored_gem)
33
+ end
34
+
35
+ it "returns the cache file" do
36
+ expect(subject.package(gem_spec)).to eq(
37
+ {gem_spec.name => vendored_gem}
38
+ )
39
+ end
40
+ end
41
+
42
+ context "without a cache file" do
43
+ before do
44
+ allow(File).to receive(:exists?).with(gem_spec.cache_file) { false }
45
+ allow(File).to receive(:exists?).with(vendored_gem) { false }
46
+ end
47
+
48
+ context "if source is available" do
49
+ before do
50
+ allow(File).to receive(:directory?).with(gem_spec.gem_dir) { true }
51
+ end
52
+
53
+ it "builds the gem from source" do
54
+ expect(subject).to receive(:build_from_source).and_return(
55
+ {"test"=>"a/packaged/file"})
56
+
57
+ expect(subject.package(gem_spec)).to eq({"test"=>"a/packaged/file"})
58
+ end
59
+ end
60
+
61
+ context "if source not available, e.g. json-1.8.1 built into Ruby 2.1" do
62
+ before do
63
+ allow(File).to receive(:directory?).with(gem_spec.gem_dir) { false }
64
+ end
65
+
66
+ it "downloads from rubygems" do
67
+ response = double(:response, status: 200, body: 'im a gem')
68
+ expect(Excon).to receive(:get).with(
69
+ "https://rubygems.org/downloads/test-0.0.1.gem", anything())
70
+ .and_return(response)
71
+ expect(subject.package(gem_spec)).to eq(
72
+ {"test"=>"vendor/test/test-0.0.1.gem"})
73
+ end
74
+ end
75
+
76
+ context "if source not available and not on rubygems" do
77
+ before do
78
+ allow(File).to receive(:directory?).with(gem_spec.gem_dir) { false }
79
+ end
80
+
81
+ it "raises" do
82
+ response = double(:response, status: 400, body: 'test')
83
+ expect(Excon).to receive(:get).with(
84
+ "https://rubygems.org/downloads/test-0.0.1.gem", anything())
85
+ .and_return(response)
86
+
87
+ expect { subject.package(gem_spec) }.to raise_error
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ describe "#build_from_source" do
94
+ context "with bad spec" do
95
+ it "raises" do
96
+ expect { subject.build_from_source("bad-name", ".") }.to raise_error
97
+ end
98
+ end
99
+ end
100
+
101
+ end
@@ -0,0 +1,95 @@
1
+ # Copyright Swipely, Inc. All rights reserved.
2
+
3
+ require 'spec_helper'
4
+ require 'pipely/deploy/bootstrap'
5
+
6
+ describe Pipely::Bundler::ProjectGem do
7
+
8
+ let(:project_spec) do
9
+ double "Gem::Specification",
10
+ name: "my-project",
11
+ file_name: "/path/to/cache/my-project.gem"
12
+ end
13
+
14
+ subject { described_class.new(project_spec, 'vendor/test') }
15
+
16
+ describe ".load" do
17
+ let(:filename) { 'foo.gemspec' }
18
+ let(:gemspec) { double }
19
+
20
+ before do
21
+ allow(Dir).to receive(:glob).with("*.gemspec") { [ filename ] }
22
+ allow(Gem::Specification).to receive(:load).with(filename) { gemspec }
23
+ end
24
+
25
+ it "loads the gemspec" do
26
+ loaded = described_class.load('vendor/test')
27
+ expect(loaded.project_spec).to eq(gemspec)
28
+ end
29
+ end
30
+
31
+ describe "#gem_files" do
32
+ let(:dependency_gem_files) do
33
+ {
34
+ 'packaged-gem1' => '/path/to/cache/packaged-gem1.gem',
35
+ 'built-from-source-gem1' => '/path/to/cache/built-from-source-gem1.gem',
36
+ }
37
+ end
38
+
39
+ let(:project_gem_file) do
40
+ {
41
+ project_spec.name => project_spec.file_name
42
+ }
43
+ end
44
+
45
+ before do
46
+ allow(subject).to receive(:dependency_gem_files) { dependency_gem_files }
47
+ allow(subject).to receive(:project_gem_file) { project_gem_file }
48
+ end
49
+
50
+ it "combines the dependency_gem_files and the project_gem_file" do
51
+ expect(subject.gem_files.keys).to match_array(
52
+ dependency_gem_files.keys + project_gem_file.keys
53
+ )
54
+ end
55
+
56
+ it "lists the project_gem_file last" do
57
+ expect(subject.gem_files.keys.last).to eq(project_spec.name)
58
+ end
59
+ end
60
+
61
+ describe "#dependency_gem_files" do
62
+ let(:bundle_gem_files) do
63
+ {
64
+ 'packaged-gem1' => '/path/to/cache/packaged-gem1.gem',
65
+ 'built-from-source-gem1' => '/path/to/cache/built-from-source-gem1.gem',
66
+ 'bundler' => '/path/to/cache/bundler.gem',
67
+ project_spec.name => project_spec.file_name,
68
+ }
69
+ end
70
+
71
+ let(:bundle) do
72
+ double "Pipely::Bundler::Bundle", gem_files: bundle_gem_files
73
+ end
74
+
75
+ it "should filter out the bundler gem and the project gem" do
76
+ result = subject.dependency_gem_files(bundle)
77
+ expect(result.keys).to eq(%w[ packaged-gem1 built-from-source-gem1 ])
78
+ end
79
+ end
80
+
81
+ describe "#project_gem_file" do
82
+ let(:gem_packager) { double "Pipely::Bundler::GemPackager" }
83
+ let(:project_gem_file) { double }
84
+
85
+ before do
86
+ allow(gem_packager).to receive(:build_from_source) { project_gem_file }
87
+ end
88
+
89
+ it "should return the project's own gem file" do
90
+ result = subject.project_gem_file(gem_packager)
91
+ expect(result).to eq(project_gem_file)
92
+ end
93
+ end
94
+
95
+ end
@@ -0,0 +1,57 @@
1
+ # Copyright Swipely, Inc. All rights reserved.
2
+
3
+ require 'spec_helper'
4
+ require 'pipely/deploy/bootstrap_context'
5
+ require 'fileutils'
6
+
7
+ describe Pipely::Deploy::BootstrapContext do
8
+ subject do
9
+ Pipely::Deploy::BootstrapContext.new.tap do |context|
10
+ context.gem_files = ['one.gem', 'two.gem']
11
+ end
12
+ end
13
+
14
+ describe "#install_gems_script" do
15
+ it "defaults to hadoop fs" do
16
+ expect(subject.install_gems_script).to eql "
17
+ # one.gem
18
+ hadoop fs -copyToLocal one.gem one.gem
19
+ gem install --force --local one.gem --no-ri --no-rdoc
20
+
21
+ # two.gem
22
+ hadoop fs -copyToLocal two.gem two.gem
23
+ gem install --force --local two.gem --no-ri --no-rdoc
24
+ "
25
+ end
26
+
27
+ context "with aws cli" do
28
+ it "should build script for aws cli" do
29
+ expect(subject.install_gems_script(:awscli) ).to eql "
30
+ # one.gem
31
+ aws s3 cp one.gem one.gem
32
+ gem install --force --local one.gem --no-ri --no-rdoc
33
+
34
+ # two.gem
35
+ aws s3 cp two.gem two.gem
36
+ gem install --force --local two.gem --no-ri --no-rdoc
37
+ "
38
+ end
39
+ end
40
+
41
+ context "with yield" do
42
+ it "should build script for aws cli" do
43
+ expect(subject.install_gems_script(:awscli) do |file,filename,command|
44
+ "custom command - #{file} #{filename} #{command}"
45
+ end).to eql "
46
+ # one.gem
47
+ custom command - one.gem one.gem aws s3 cp one.gem one.gem
48
+ gem install --force --local one.gem --no-ri --no-rdoc
49
+
50
+ # two.gem
51
+ custom command - two.gem two.gem aws s3 cp two.gem two.gem
52
+ gem install --force --local two.gem --no-ri --no-rdoc
53
+ "
54
+ end
55
+ end
56
+ end
57
+ end
@@ -2,85 +2,52 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'pipely/deploy/bootstrap'
5
- require 'fog'
6
5
  require 'fileutils'
7
6
 
8
- module Pipely
9
- describe Deploy::Bootstrap do
10
- subject { described_class.new(s3_bucket, 'test_path/gems') }
11
- let(:s3_bucket) do
12
- s3 = AWS::S3.new
13
- s3.buckets['a-test-bucket']
14
- end
15
-
16
- it "should have bucket name" do
17
- expect(subject.bucket_name).to eql 'a-test-bucket'
18
- end
19
-
20
- it "should have a s3 gems path" do
21
- expect(subject.s3_gems_path).to eql 'test_path/gems'
22
- end
23
-
24
- describe "#build_and_upload_gems" do
25
- let(:build_and_upload_gems) do
26
- VCR.use_cassette('build_and_upload_gems') do
27
- subject.build_and_upload_gems
28
- end
29
- end
30
-
31
- it "should create project gem" do
32
- build_and_upload_gems
33
-
34
- expect(File).to exist(subject.project_spec.file_name)
35
- end
7
+ describe Pipely::Deploy::Bootstrap do
36
8
 
37
- it "should upload gems" do
38
- specs = Bundler.definition.specs_for([:default])
9
+ subject { described_class.new(s3_uploader) }
39
10
 
40
- gems = specs.map(&:file_name).reject do |g|
41
- # Ignore bundler, since it could be a system installed gem (travis)
42
- # without a cache file
43
- g.match(/^json-\d+\.\d+\.\d+\.gem$/) or
44
- g.match(/^bundler-\d+\.\d+\.\d+\.gem$/)
45
- end
11
+ let(:s3_uploader) { double }
46
12
 
47
- objects = double(:objects)
48
- s3_object = double('s3_object', write: nil)
49
-
50
- allow(objects).to receive(:[]) { s3_object }
13
+ let(:gem_files) do
14
+ {
15
+ 'packaged-gem1' => '/path/to/cache/packaged-gem1.gem',
16
+ 'built-from-source-gem1' => '/path/to/cache/built-from-source-gem1.gem',
17
+ }
18
+ end
51
19
 
52
- gems.each do |gem|
53
- expect(objects).to receive(:[]).with(subject.gem_s3_path(gem))
54
- end
20
+ describe "#build_and_upload_gems" do
21
+ before do
22
+ allow(Pipely::Bundler).to receive(:gem_files) { gem_files }
23
+ end
55
24
 
56
- expect(s3_bucket).to(receive(:objects)).
57
- at_least(gems.size).times.
58
- at_most(gems.size + 1).times. # allow for Bundler
59
- and_return(objects)
25
+ it 'uploads each gem' do
26
+ expect(s3_uploader).to receive(:upload).with(gem_files.values)
60
27
 
61
- build_and_upload_gems
62
- end
28
+ subject.build_and_upload_gems
63
29
  end
30
+ end
64
31
 
65
- describe "#context" do
66
- let(:context) { subject.context}
32
+ describe "#context" do
33
+ let(:context) { subject.context(s3_steps_path) }
34
+ let(:s3_steps_path) { 'a/test/path' }
35
+ let(:s3_gem_paths) { double }
67
36
 
68
- before do
69
- VCR.use_cassette('build_and_upload_gems') do
70
- subject.build_and_upload_gems
71
- end
72
- end
37
+ before do
38
+ allow(subject).to receive(:gem_files) { gem_files }
73
39
 
74
- it "should have gem_files" do
75
- expect(context.gem_files).to_not be_nil
40
+ allow(s3_uploader).to receive(:s3_urls).with(gem_files.values) do
41
+ s3_gem_paths
76
42
  end
43
+ end
77
44
 
78
- it "should be an s3 url" do
79
- expect(context.gem_files.first).to(
80
- match( /^s3:\/\/#{subject.bucket_name}\/#{subject.s3_gems_path}/ )
81
- )
82
- end
45
+ it "should have s3 steps path" do
46
+ expect(context.s3_steps_path).to eq(s3_steps_path)
83
47
  end
84
48
 
49
+ it "builds S3 urls to the uploaded gem files" do
50
+ expect(context.gem_files).to eq(s3_gem_paths)
51
+ end
85
52
  end
86
53
  end
@@ -0,0 +1,54 @@
1
+ # Copyright Swipely, Inc. All rights reserved.
2
+
3
+ require 'spec_helper'
4
+ require 'pipely/deploy/s3_uploader'
5
+ require 'tempfile'
6
+
7
+ describe Pipely::Deploy::S3Uploader do
8
+
9
+ subject { described_class.new(s3_bucket, 'test_path/gems') }
10
+
11
+ let(:s3_bucket) do
12
+ s3 = AWS::S3.new
13
+ s3.buckets['a-test-bucket']
14
+ end
15
+
16
+ it "should have bucket name" do
17
+ expect(subject.bucket_name).to eq('a-test-bucket')
18
+ end
19
+
20
+ it "should have a s3 path" do
21
+ expect(subject.s3_path).to eq('test_path/gems')
22
+ end
23
+
24
+ describe "#upload(files)" do
25
+ let(:objects) { double(:objects) }
26
+
27
+ let(:s3_object) do
28
+ double('s3_object', write: nil, exists?: true, etag: 'mismatch')
29
+ end
30
+
31
+ let(:files) do
32
+ [
33
+ Tempfile.new('packaged-gem1.gem').path,
34
+ Tempfile.new('built-from-source-gem1.gem').path,
35
+ ]
36
+ end
37
+
38
+ before do
39
+ allow(objects).to receive(:[]) { s3_object }
40
+ allow(s3_bucket).to receive(:objects) { objects }
41
+ end
42
+
43
+ it 'uploads each file' do
44
+ files.each do |file|
45
+ expect(objects).to receive(:[]).with(subject.s3_file_path(file))
46
+ end
47
+
48
+ expect(s3_bucket).to receive(:objects).exactly(files.size).times
49
+
50
+ subject.upload(files)
51
+ end
52
+ end
53
+
54
+ end
@@ -3,6 +3,7 @@ require 'aws-sdk'
3
3
  require 'fog'
4
4
  require 'rspec'
5
5
  require 'vcr'
6
+ require 'pry'
6
7
 
7
8
  $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
8
9
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pipely
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Gillooly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-11 00:00:00.000000000 Z
11
+ date: 2014-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-graphviz
@@ -206,6 +206,20 @@ dependencies:
206
206
  - - ">="
207
207
  - !ruby/object:Gem::Version
208
208
  version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: pry
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
209
223
  description:
210
224
  email:
211
225
  - matt@swipely.com
@@ -230,12 +244,18 @@ files:
230
244
  - lib/pipely/build/s3_path_builder.rb
231
245
  - lib/pipely/build/template.rb
232
246
  - lib/pipely/build/template_helpers.rb
247
+ - lib/pipely/bundler.rb
248
+ - lib/pipely/bundler/bundle.rb
249
+ - lib/pipely/bundler/gem_packager.rb
250
+ - lib/pipely/bundler/project_gem.rb
233
251
  - lib/pipely/component.rb
234
252
  - lib/pipely/definition.rb
235
253
  - lib/pipely/dependency.rb
236
254
  - lib/pipely/deploy.rb
237
255
  - lib/pipely/deploy/bootstrap.rb
256
+ - lib/pipely/deploy/bootstrap_context.rb
238
257
  - lib/pipely/deploy/client.rb
258
+ - lib/pipely/deploy/s3_uploader.rb
239
259
  - lib/pipely/fog_client.rb
240
260
  - lib/pipely/graph_builder.rb
241
261
  - lib/pipely/live_pipeline.rb
@@ -257,11 +277,16 @@ files:
257
277
  - spec/lib/pipely/build/s3_path_builder_spec.rb
258
278
  - spec/lib/pipely/build/template_spec.rb
259
279
  - spec/lib/pipely/build_spec.rb
280
+ - spec/lib/pipely/bundler/bundle_spec.rb
281
+ - spec/lib/pipely/bundler/gem_packager_spec.rb
282
+ - spec/lib/pipely/bundler/project_gem_spec.rb
260
283
  - spec/lib/pipely/component_spec.rb
261
284
  - spec/lib/pipely/definition_spec.rb
262
285
  - spec/lib/pipely/dependency_spec.rb
286
+ - spec/lib/pipely/deploy/bootstrap_context_spec.rb
263
287
  - spec/lib/pipely/deploy/bootstrap_spec.rb
264
288
  - spec/lib/pipely/deploy/client_spec.rb
289
+ - spec/lib/pipely/deploy/s3_uploader_spec.rb
265
290
  - spec/lib/pipely/graph_builder_spec.rb
266
291
  - spec/lib/pipely/reference_list_spec.rb
267
292
  - spec/lib/pipely_spec.rb
@@ -297,11 +322,16 @@ test_files:
297
322
  - spec/lib/pipely/build/s3_path_builder_spec.rb
298
323
  - spec/lib/pipely/build/template_spec.rb
299
324
  - spec/lib/pipely/build_spec.rb
325
+ - spec/lib/pipely/bundler/bundle_spec.rb
326
+ - spec/lib/pipely/bundler/gem_packager_spec.rb
327
+ - spec/lib/pipely/bundler/project_gem_spec.rb
300
328
  - spec/lib/pipely/component_spec.rb
301
329
  - spec/lib/pipely/definition_spec.rb
302
330
  - spec/lib/pipely/dependency_spec.rb
331
+ - spec/lib/pipely/deploy/bootstrap_context_spec.rb
303
332
  - spec/lib/pipely/deploy/bootstrap_spec.rb
304
333
  - spec/lib/pipely/deploy/client_spec.rb
334
+ - spec/lib/pipely/deploy/s3_uploader_spec.rb
305
335
  - spec/lib/pipely/graph_builder_spec.rb
306
336
  - spec/lib/pipely/reference_list_spec.rb
307
337
  - spec/lib/pipely_spec.rb