baps 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 26742e6633e6fd17a446096d5205ea9d845fa4f0
4
+ data.tar.gz: 89c927289e60d5e8d017dfa929431a03d4b24088
5
+ SHA512:
6
+ metadata.gz: 63acc006bf8feec859a0edc6d407e59556daefc73446d655d45fe7f39afeb2be96337378fc16a0e2f0aecc4352d6198512450fdbd5286cc9474ec8a8e6a53c93
7
+ data.tar.gz: 08977962a992b220517cba693876ae2b0847d97d18a5aa2f16bdeef27bb9c3476c6911ec32aa3d2b54975c42efbece0a32e79df3386c5c8f14ac947aec69e4a5
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1 @@
1
+ 2.3.1
@@ -0,0 +1,8 @@
1
+ FROM ruby:2.3
2
+
3
+ # throw errors if Gemfile has been modified since Gemfile.lock
4
+ RUN bundle config --global frozen 1
5
+
6
+ COPY . /app
7
+ WORKDIR /app
8
+ RUN bundle install
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test, :development do
6
+ gem 'pry'
7
+ end
@@ -0,0 +1,32 @@
1
+ # BAPS - Build and Packaging System
2
+ Define your build using provided DSL and `baps` will take care of building
3
+ the given software in a clean room environment, package it as `deb` and push to packagecloud.
4
+
5
+ ## Usage
6
+ `exe/baps -p ./definitions`
7
+ This will go through every definition file in the given `./definitions` folder and if there's at least
8
+ one definition that needs to be executed `baps` will pull prebuilt Docker image, copy the content of `./definitions`
9
+ folder to it and fire a container based on the image for each definition needed to be executed. Inside the container
10
+ `baps` will execute the given definition using `exe/baps -p ./definitions/path_to_definition_file.rb`
11
+
12
+ If you don't want a clean room environment i.e not execute your definition in Docker you can directly use `exe/baps --no-docker -p ./definitions/path_to_definition_file.rb`.
13
+
14
+ ## Example
15
+ An example definition to build `pmtud` from a remote git repository:
16
+ ```
17
+ deb do
18
+ name 'pmtud'
19
+ version '0.7'
20
+ dependencies %w(libpcap0.8 libnetfilter-log1)
21
+ description 'Path MTU daemon - broadcast lost ICMP packets on ECMP networks'
22
+
23
+ build do
24
+ source git: 'https://github.com/Shopify/pmtud.git', branch: "v#{@version}"
25
+ dependencies %w(libnetfilter-log-dev libnfnetlink-dev libpcap-dev)
26
+ run 'make pmtud BUILD=release CC=gcc'
27
+ run "install -d 0700 #{deb_dir}/usr/local/bin"
28
+ run "install pmtud #{deb_dir}/usr/local/bin"
29
+ end
30
+ end
31
+ ```
32
+ More examples can be found at https://github.com/Shopify/shopify-open-source-builds
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'baps/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "baps"
8
+ spec.version = Baps::VERSION
9
+ spec.authors = ["Elvin Efendi"]
10
+ spec.email = ["elvin.efendiyev@gmail.com"]
11
+
12
+ spec.summary = %q{Build and Packaging System Edit.}
13
+ spec.description = %q{Define your build using provided DSL and baps will take care of building the given software in a clean room environment, package it as deb and push to packagecloud.}
14
+ spec.homepage = ""
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_runtime_dependency 'fpm'
24
+ spec.add_runtime_dependency 'packagecloud-ruby'
25
+ spec.add_runtime_dependency 'docker-api'
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.13"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency "minitest", "~> 5.0"
30
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'bundler/setup'
7
+ require 'baps'
8
+ require 'pry'
9
+
10
+ binding.pry
@@ -0,0 +1,15 @@
1
+ machine:
2
+ services:
3
+ - docker
4
+
5
+ test:
6
+ override:
7
+ - bundle exec rake test
8
+
9
+ deployment:
10
+ production:
11
+ branch: master
12
+ commands:
13
+ - docker build --rm=false -t elvinefendi/baps .
14
+ - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
15
+ - docker push elvinefendi/baps
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'bundler/setup'
7
+ require 'baps'
8
+ require 'baps/executor'
9
+ require 'optparse'
10
+
11
+ options = { use_docker: true }
12
+ OptionParser.new do |opts|
13
+ opts.banner = 'Usage: exe/baps [options]. Either -d or -f has to be provided.'
14
+
15
+ opts.on('-p', '--path PATH', 'Path to a directory with definitions or absolute path of definition file') do |v|
16
+ options[:path] = v
17
+ end
18
+
19
+ opts.on("--no-docker", "Path to a definition file") do
20
+ options[:use_docker] = false
21
+ end
22
+ end.parse!
23
+
24
+ executor = Baps::Executor.new(options)
25
+ executor.execute
@@ -0,0 +1,24 @@
1
+ require 'baps/build'
2
+ require 'baps/command'
3
+ require 'baps/deb'
4
+ require 'baps/definition'
5
+ require 'baps/executor'
6
+ require 'baps/packagecloud_api'
7
+ require 'baps/version'
8
+
9
+ require 'docker'
10
+
11
+ module Baps
12
+ extend self
13
+
14
+ TIMEOUT = 5*60
15
+ Excon.defaults[:write_timeout] = TIMEOUT
16
+ Excon.defaults[:read_timeout] = TIMEOUT
17
+
18
+ class ChecksumError < StandardError; end
19
+ class ExecutorError < StandardError; end
20
+
21
+ def packagecloud
22
+ @@packagecloud ||= PackagecloudAPI.new('shopify', ENV['PACKAGECLOUD_TOKEN'])
23
+ end
24
+ end
@@ -0,0 +1,82 @@
1
+ require 'tmpdir'
2
+
3
+ module Baps
4
+ class Build
5
+ def initialize(name, version, &block)
6
+ raise StandardError.new('Nothing defined') unless block_given?
7
+ @name = name
8
+ @version = version
9
+ @commands = []
10
+ instance_eval(&block)
11
+ @commands.unshift(Command.new("apt-get install -y -q #{dependencies.join(' ')}")) if dependencies
12
+ end
13
+
14
+ def start
15
+ Dir.chdir(dir) do
16
+ unpack_source
17
+ @commands.each(&:execute)
18
+ end
19
+ end
20
+
21
+ def deb_dir
22
+ @deb_dir ||= File.join(dir, "debian-#{Time.now.utc.to_i}")
23
+ end
24
+
25
+ def dir
26
+ @dir ||= Dir.mktmpdir("baps-#{@name}-#{@version}-")
27
+ end
28
+
29
+ private
30
+
31
+ def unpack_source
32
+ return unless source
33
+ if source[:git]
34
+ Command.new("git clone #{source[:git]} ./").execute
35
+ if source[:branch]
36
+ Command.new("git reset --hard #{source[:branch]}").execute
37
+ end
38
+ elsif source[:remote_tar]
39
+ path = "#{@name}-#{@version}.tar.gz"
40
+ IO.copy_stream(open(source[:remote_tar]), path)
41
+ content = File.read(path)
42
+ if sha256sum
43
+ raise ChecksumError, 'sha256sum cheksum did not match.' unless Digest::SHA256.hexdigest(content) == sha256sum
44
+ end
45
+ if md5sum
46
+ raise ChecksumError, 'md5sum cheksum did not match.' unless Digest::MD5.hexdigest(content) == md5sum
47
+ end
48
+ unpack_tar(path)
49
+ elsif source[:local_tar]
50
+ unpack_tar(File.join('baps-definitions', "#{@name}-#{@version}.tar.gz"))
51
+ end
52
+ end
53
+
54
+ def run(cmd)
55
+ @commands << Command.new(cmd)
56
+ end
57
+
58
+ def unpack_tar(path)
59
+ cmd = Command.new("tar -tf #{path}")
60
+ cmd.execute
61
+ dirs = cmd.stdout.split("\n")
62
+ have_root_dir = true
63
+ i = 0
64
+ while have_root_dir && i < dirs.size - 1
65
+ curr_dir = dirs[i].split('/').first
66
+ next_dir = dirs[i+1].split('/').first
67
+ have_root_dir = (curr_dir == next_dir)
68
+ i +=1
69
+ end
70
+ Command.new("tar -xzf #{path} -C . --strip #{have_root_dir ? 1 : 0}").execute
71
+ ensure
72
+ FileUtils.rm(path) if File.exist?(path)
73
+ end
74
+
75
+ %i(source dependencies sha256sum md5sum).each do |method|
76
+ define_method(method) do |value = nil|
77
+ return instance_variable_get("@#{method}") if value.nil?
78
+ instance_variable_set("@#{method}", value)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ require 'open3'
2
+
3
+ module Baps
4
+ class Command
5
+ attr_reader :command, :exit_status, :stdout, :stderr
6
+
7
+ def initialize(command)
8
+ @command = command
9
+ @stdout, @stderr = String.new, String.new
10
+ end
11
+
12
+ def on_stdout(data)
13
+ @stdout += data
14
+ end
15
+
16
+ def on_stderr(data)
17
+ @stderr += data
18
+ end
19
+
20
+ def exit_status=(value)
21
+ @exit_status = value
22
+ if @exit_status > 0
23
+ message = ''
24
+ message += "#{command} exit status: #{@exit_status}\n"
25
+ message += "#{command} stdout: " + (stdout.strip.empty? ? "Nothing written" : stdout.strip) + "\n"
26
+ message += "#{command} stderr: " + (stderr.strip.empty? ? 'Nothing written' : stderr.strip) + "\n"
27
+ raise StandardError, message
28
+ end
29
+ end
30
+
31
+ def execute
32
+ puts "Executing: #{command}"
33
+ Open3.popen3(command) do |_stdin, stdout, stderr, wait_thr|
34
+ stdout_thread = Thread.new do
35
+ while (line = stdout.gets) do
36
+ on_stdout(line)
37
+ end
38
+ end
39
+
40
+ stderr_thread = Thread.new do
41
+ while (line = stderr.gets) do
42
+ on_stderr(line)
43
+ end
44
+ end
45
+
46
+ stdout_thread.join
47
+ stderr_thread.join
48
+
49
+ self.exit_status = wait_thr.value.to_i
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,49 @@
1
+ module Baps
2
+ class Deb
3
+ def initialize(&block)
4
+ raise StandardError.new('Nothing defined') unless block_given?
5
+ instance_eval(&block)
6
+ end
7
+
8
+ def already_exist?
9
+ Baps.packagecloud.package_exist?(name: name, version: version)
10
+ end
11
+
12
+ def create
13
+ build.start
14
+ package
15
+ publish
16
+ end
17
+
18
+ private
19
+
20
+ def publish
21
+ file = Dir['*.deb'].first
22
+ Baps.packagecloud.publish_package(file)
23
+ end
24
+
25
+ def package
26
+ command = "fpm -s dir -C #{build.deb_dir} -v #{version} -t deb "\
27
+ "--description '#{description}' --vendor '' "\
28
+ "-n #{name} --deb-priority #{priority or 'optional'} --deb-compression bzip2"
29
+ command << " #{dependencies.map { |d| "-d '#{d}'" }.join(' ')}" if dependencies
30
+ command << " --replaces '#{replaces}'" if replaces
31
+ command << " #{targets.join(' ')}" if targets
32
+ # TODO use programming API
33
+ # as described in https://github.com/jordansissel/fpm/blob/master/examples/api/dir-to-deb-rpm-with-init.rb
34
+ Command.new(command).execute
35
+ end
36
+
37
+ def build(&block)
38
+ return @build unless block_given?
39
+ @build = Build.new(name, version, &block)
40
+ end
41
+
42
+ %i(name version dependencies description targets priority replaces).each do |method|
43
+ define_method(method) do |value = nil|
44
+ return instance_variable_get("@#{method}") if value.nil?
45
+ instance_variable_set("@#{method}", value)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,26 @@
1
+ module Baps
2
+ class Definition
3
+ attr_reader :path
4
+
5
+ def initialize(path)
6
+ @path = path
7
+ instance_eval(File.read(@path), @path)
8
+ end
9
+
10
+ def execute
11
+ puts "Executing #{@path}"
12
+ deb.create
13
+ end
14
+
15
+ def executed?
16
+ deb.already_exist?
17
+ end
18
+
19
+ private
20
+
21
+ def deb(&block)
22
+ return @deb unless block_given?
23
+ @deb = Deb.new(&block)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,66 @@
1
+ module Baps
2
+ class Executor
3
+ DOCKER_IMAGE_ID = 'elvinefendi/baps'
4
+
5
+ def initialize(path:, use_docker: true)
6
+ raise ArgumentError.new('Given path does not exist.') unless File.exist?(path)
7
+ @path = File.expand_path(path)
8
+ @use_docker = use_docker
9
+ @definitions = definitions_from_directory if File.directory?(@path)
10
+ @definitions = [Definition.new(@path)] if File.file?(@path)
11
+ end
12
+
13
+
14
+ def execute
15
+ if !@definitions || @definitions.empty?
16
+ puts 'No definition to execute'
17
+ return
18
+ end
19
+
20
+ return @definitions.each(&:execute) unless @use_docker
21
+
22
+ errors = {}
23
+ @definitions.each do |definition|
24
+ container = base_container.run("exe/baps -p #{path_in_docker} --no-docker")
25
+ container.attach { |stream, chunk| puts "#{stream}: #{chunk}" }
26
+ if (code = container.wait(Baps::TIMEOUT)['StatusCode']) > 0
27
+ errors[definition] = code
28
+ end
29
+ end
30
+ unless errors.empty?
31
+ raise ExecutorError, errors.map { |d, c| "#{d.path} exited with #{c}" }.join("\n")
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def base_container
38
+ return @base_container if @base_container
39
+ @base_container = image.run('apt-get update -y -q', 'Env' => ["PACKAGECLOUD_TOKEN=#{ENV['PACKAGECLOUD_TOKEN']}"])
40
+ @base_container.attach { |stream, chunk| puts "#{stream}: #{chunk}" }
41
+ @base_container
42
+ end
43
+
44
+ def image
45
+ return @image if @image
46
+ begin
47
+ @image = Docker::Image.get(DOCKER_IMAGE_ID)
48
+ rescue Docker::Error::NotFoundError
49
+ @image = Docker::Image.create('fromImage' => DOCKER_IMAGE_ID)
50
+ end
51
+ @image = @image.insert_local('localPath' => @path, 'outputPath' => path_in_docker)
52
+ end
53
+
54
+ def path_in_docker
55
+ "/baps-definitions/#{File.basename(@path)}"
56
+ end
57
+
58
+ def definitions_from_directory
59
+ Dir[File.join(@path, '**/*.rb')].map do |path|
60
+ definition = Definition.new(path)
61
+ next if definition.executed?
62
+ definition
63
+ end.compact
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,28 @@
1
+ require 'packagecloud'
2
+
3
+ module Baps
4
+ class PackagecloudAPI
5
+ def initialize(username, token)
6
+ connection = Packagecloud::Connection.new('https', 'packages.shopify.io')
7
+ credentials = Packagecloud::Credentials.new(username, token)
8
+ @client = Packagecloud::Client.new(credentials, "packagecloud-ruby #{Packagecloud::VERSION}", connection)
9
+ end
10
+
11
+ def publish_package(file)
12
+ package = Packagecloud::Package.new(file: file)
13
+ result = @client.put_package('public', package, 'ubuntu/trusty')
14
+ if result.succeeded
15
+ puts "Package #{file} has been pushed to packagecloud"
16
+ else
17
+ puts "Could not push #{file} to packagecoud: #{result.response}"
18
+ end
19
+ end
20
+
21
+ def package_exist?(name:, version:, arch: 'amd64', type: 'deb')
22
+ url = "/api/v1/repos/shopify/public/package/#{type}/ubuntu/trusty/#{name}/#{arch}/#{version}/stats/downloads/count.json"
23
+ json = @client.send(:get, url)
24
+ result = @client.send(:parsed_json_result, json)
25
+ result.succeeded
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Baps
2
+ VERSION = '0.1.1'
3
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: baps
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Elvin Efendi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-01-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fpm
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: packagecloud-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: docker-api
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.13'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.13'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '5.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '5.0'
97
+ description: Define your build using provided DSL and baps will take care of building
98
+ the given software in a clean room environment, package it as deb and push to packagecloud.
99
+ email:
100
+ - elvin.efendiyev@gmail.com
101
+ executables:
102
+ - baps
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".ruby-version"
108
+ - Dockerfile
109
+ - Gemfile
110
+ - Gemfile.lock
111
+ - README.md
112
+ - Rakefile
113
+ - baps.gemspec
114
+ - bin/console
115
+ - circle.yml
116
+ - exe/baps
117
+ - lib/baps.rb
118
+ - lib/baps/build.rb
119
+ - lib/baps/command.rb
120
+ - lib/baps/deb.rb
121
+ - lib/baps/definition.rb
122
+ - lib/baps/executor.rb
123
+ - lib/baps/packagecloud_api.rb
124
+ - lib/baps/version.rb
125
+ homepage: ''
126
+ licenses: []
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.5.1
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Build and Packaging System Edit.
148
+ test_files: []