phoebo 0.1.1

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c2f04f78db0118a1c401d6e8fb38767540a0770c
4
+ data.tar.gz: 6b74da1cd4f1d534057c5e67c001a315978087ee
5
+ SHA512:
6
+ metadata.gz: 354236b15bef8f208762c2bbe4459a60066d7c323a3aacac03625cd56fa3417b89d573d4c8ca1c7746def9ee4889d4dfa9505962ac12b3ab5fc6c4a71c004cba
7
+ data.tar.gz: f5d1ef725707f8427491bf10fb525ad8a30c77503160b6a2f3de7ce44ea08c02bbc60a1d235c9e7849dc67100133e264b880b5d37088e2cf692690c38bbefff5
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in phoebo.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Adam Staněk
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,11 @@
1
+ # Run
2
+
3
+ ```bash
4
+ $ bundle exec bin/phoebo
5
+ ```
6
+
7
+ # Run tests
8
+
9
+ ```bash
10
+ rspec
11
+ ```
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Exit cleanly from an early interrupt
4
+ Signal.trap("INT") { exit 130 }
5
+
6
+ require 'phoebo'
7
+
8
+ begin
9
+ app = Phoebo::Application.new(ARGV)
10
+ code = app.run
11
+ app.cleanup
12
+ exit code
13
+
14
+ rescue Phoebo::PhoeboError => err
15
+ $stderr.puts "Error: ".red + err.message
16
+ $stdout.puts
17
+ exit err.status_code
18
+ ensure
19
+ app.cleanup
20
+ end
@@ -0,0 +1,34 @@
1
+ require 'phoebo/version'
2
+
3
+ module Phoebo
4
+ autoload :Application, 'phoebo/application'
5
+ autoload :Config, 'phoebo/config'
6
+ autoload :Console, 'phoebo/console'
7
+ autoload :Docker, 'phoebo/docker'
8
+ autoload :Environment, 'phoebo/environment'
9
+ autoload :Util, 'phoebo/util'
10
+
11
+ class PhoeboError < StandardError
12
+ def self.status_code(code)
13
+ define_method(:status_code) { code }
14
+ end
15
+ end
16
+
17
+ class InvalidArgumentError < PhoeboError; status_code(4) ; end
18
+ class IOError < PhoeboError; status_code(5) ; end
19
+ class ExternalError < PhoeboError; status_code(6) ; end
20
+
21
+ # Phoebofile errors
22
+ class ConfigError < PhoeboError; status_code(7) ; end
23
+ class SyntaxError < ConfigError; end
24
+
25
+ # Error while generating Docker image
26
+ class DockerError < PhoeboError; status_code(8) ; end
27
+
28
+ # Configure Phoebo environment from Phoebofile
29
+ def self.configure(version, &block)
30
+ raise ConfigError, "Configuration version #{version} not supported" if version != 1
31
+ Config.load_from_block(block)
32
+ end
33
+
34
+ end
@@ -0,0 +1,238 @@
1
+ require 'optparse'
2
+ require 'colorize'
3
+ require 'docker'
4
+ require 'rugged'
5
+ require 'pathname'
6
+
7
+ module Phoebo
8
+ #
9
+ # Main Application
10
+ #
11
+ # Performs run sequence and holds instances of necessary services to ease
12
+ # access without need of dependency injection. You should still allow to
13
+ # pass dependency if necessary (for testing).
14
+ # Use following snippet in your class:
15
+ #
16
+ # class MyClass
17
+ # attr_writer :environment
18
+ # def environment
19
+ # @environment ||= Application.instance.environment
20
+ # end
21
+ # end
22
+ #
23
+ class Application
24
+ include Console
25
+
26
+ # Creates application
27
+ def initialize(args = [])
28
+ @args = args
29
+ @@instance = self
30
+ end
31
+
32
+ # Returns last application instance
33
+ def self.instance
34
+ @@instance
35
+ end
36
+
37
+ # Environment service instance
38
+ def environment
39
+ @environment ||= Environment.new
40
+ end
41
+
42
+ # Temporary File Manager
43
+ def temp_file_manager
44
+ @temp_file_manager ||= Util::TempFileManager.new(environment.temp_path)
45
+ end
46
+
47
+ # Run sequence
48
+ def run()
49
+ result = 0
50
+ options = parse_options
51
+
52
+ if options[:error]
53
+ result = 1
54
+ stderr.puts 'Error: '.red + options[:error]
55
+ stderr.puts
56
+ end
57
+
58
+ send(('run_' + options[:mode].to_s).to_sym, options).to_i || result
59
+ end
60
+
61
+ # Cleanup sequence
62
+ def cleanup
63
+ if @temp_file_manager && @temp_file_manager.need_cleanup?
64
+ stdout.puts "Cleaning up"
65
+ stdout.puts
66
+ @temp_file_manager.cleanup
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Run in normal mode
73
+ def run_normal(options)
74
+
75
+ # Default project path, is PWD
76
+ project_path = Dir.pwd
77
+
78
+ # If we are building image from Git repository
79
+ if options[:repository]
80
+
81
+ # Prepare SSH credentials if necessary
82
+ if options[:ssh_key]
83
+ private_path = Pathname.new(options[:ssh_key])
84
+
85
+ raise InvalidArgumentError, "SSH key not found" unless private_path.exist?
86
+ raise InvalidArgumentError, "Missing public SSH key" unless options[:ssh_public]
87
+
88
+ public_path = Pathname.new(options[:ssh_public])
89
+ raise InvalidArgumentError, "Public SSH key not found" unless public_path.exist?
90
+
91
+ cred = Rugged::Credentials::SshKey.new(
92
+ username: options[:ssh_user],
93
+ publickey: public_path.realpath.to_s,
94
+ privatekey: private_path.realpath.to_s
95
+ )
96
+ else
97
+ cred = nil
98
+ end
99
+
100
+ # Create temp dir.
101
+ project_path = temp_file_manager.path('project')
102
+
103
+ # Clone remote repository
104
+ begin
105
+ stdout.puts "Cloning remote repository " + "...".light_black
106
+ Rugged::Repository.clone_at options[:repository],
107
+ project_path, credentials: cred
108
+
109
+ rescue Rugged::SshError => e
110
+ raise unless e.message.include?('authentication')
111
+ raise IOError, "Unable to clone remote repository. SSH authentication failed."
112
+ end
113
+ end
114
+
115
+ # Prepare Docker credentials
116
+ if options[:docker_username]
117
+ raise InvalidArgumentError, "Missing docker password." unless options[:docker_password]
118
+ raise InvalidArgumentError, "Missing docker e-mail." unless options[:docker_email]
119
+
120
+ pusher = Docker::ImagePusher.new(
121
+ options[:docker_username],
122
+ options[:docker_password],
123
+ options[:docker_email]
124
+ )
125
+ else
126
+ pusher = nil
127
+ end
128
+
129
+ # Exit code
130
+ result = 0
131
+
132
+ # Process all the files
133
+ options[:files] << '.' if options[:files].empty?
134
+ options[:files].each do |rel_path|
135
+
136
+ path = (Pathname.new(project_path) + rel_path).realpath.to_s
137
+
138
+ # Prepare config path
139
+ config_filename = 'Phoebofile'
140
+ config_path = "#{path}#{File::SEPARATOR}#{config_filename}"
141
+
142
+ # Check if config exists -> resumable error
143
+ unless File.exists?(config_path)
144
+ stderr.puts "No #{config_filename} found in #{path}".red
145
+ result = 1
146
+ next
147
+ end
148
+
149
+ # Load config
150
+ config = Config.load_from_file(config_path)
151
+
152
+ # Build & push image
153
+ builder = Docker::ImageBuilder.new(path)
154
+ config.images.each do |image|
155
+ image_id = builder.build(image)
156
+ pusher.push(image_id) if pusher
157
+ end
158
+ end
159
+
160
+ puts "Everything done :-)".green
161
+
162
+ result
163
+ end
164
+
165
+ # Show help
166
+ def run_help(_options)
167
+ stdout.puts @option_parser
168
+ stdout.puts
169
+ end
170
+
171
+ # Show version info
172
+ def run_version(_options)
173
+ stdout.puts "Version: #{Phoebo::VERSION}"
174
+ stdout.puts
175
+ end
176
+
177
+ # Parse passed command-line options
178
+ def parse_options
179
+ options = { error: nil, mode: :normal, files: [], ssh_user: 'git' }
180
+
181
+ unless @option_parser
182
+ @option_parser = OptionParser.new
183
+ @option_parser.banner = \
184
+ "Usage: #{@option_parser.program_name} [options] file"
185
+
186
+ @option_parser.on_tail('-rURL', '--repository=URL', 'Repository URL') do |value|
187
+ options[:repository] = value
188
+ end
189
+
190
+ @option_parser.on_tail('--ssh-user=USERNAME', 'Username for Git over SSH (Default: git)') do |value|
191
+ options[:ssh_user] = value
192
+ end
193
+
194
+ @option_parser.on_tail('--ssh-public=PATH', 'Path to public SSH key for Git repository') do |value|
195
+ options[:ssh_public] = value
196
+ end
197
+
198
+ @option_parser.on_tail('--ssh-key=PATH', 'Path to SSH key for Git repository') do |value|
199
+ options[:ssh_key] = value
200
+ end
201
+
202
+ @option_parser.on_tail('--docker-user=USERNAME', 'Username for Docker Registry') do |value|
203
+ options[:docker_username] = value
204
+ end
205
+
206
+ @option_parser.on_tail('--docker-password=PASSWORD', 'Password for Docker Registry') do |value|
207
+ options[:docker_password] = value
208
+ end
209
+
210
+ @option_parser.on_tail('--docker-email=EMAIL', 'E-mail for Docker Registry') do |value|
211
+ options[:docker_email] = value
212
+ end
213
+
214
+ @option_parser.on_tail('--version', 'Show version info') do
215
+ options[:mode] = :version
216
+ end
217
+
218
+ @option_parser.on_tail('-h', '--help', 'Show this message') do
219
+ options[:mode] = :help
220
+ end
221
+ end
222
+
223
+ begin
224
+ # Parse and go through all non-options
225
+ @option_parser.parse(@args).each do |arg|
226
+ options[:files] << arg
227
+ end
228
+
229
+ rescue OptionParser::ParseError => err
230
+ options[:mode] = :help
231
+ options[:error] = err.message
232
+ end
233
+
234
+ options
235
+ end
236
+ end
237
+ end
238
+
@@ -0,0 +1,52 @@
1
+ module Phoebo
2
+ class Config
3
+ autoload :Image, 'phoebo/config/image'
4
+ autoload :ImageCommands, 'phoebo/config/image_commands'
5
+
6
+ attr_accessor :images
7
+
8
+ # Loads config from file
9
+ # @see Phoebo.configure()
10
+ def self.load_from_file(file_path)
11
+ begin
12
+ @instance = nil
13
+ Kernel.load File.expand_path(file_path), true
14
+ @instance
15
+
16
+ rescue ::SyntaxError => e
17
+ raise Phoebo::SyntaxError, e.message
18
+ end
19
+ end
20
+
21
+ def self.load_from_block(block)
22
+ @instance = self.new
23
+ @instance.dsl_eval(block)
24
+ end
25
+
26
+ # Instance initialization
27
+ def initialize
28
+ @images = []
29
+ end
30
+
31
+ # Evaluate block within DSL context
32
+ def dsl_eval(block)
33
+ @dsl ||= DSL.new(self)
34
+ @dsl.instance_eval(&block)
35
+ self
36
+ end
37
+
38
+ # Private DSL
39
+ class DSL
40
+ def initialize(config)
41
+ @config = config
42
+ end
43
+
44
+ def image(name, from, &block)
45
+ @config.images << (img_instance = Image.new(name, from))
46
+ img_instance.dsl_eval(block)
47
+ img_instance
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,35 @@
1
+ module Phoebo
2
+ class Config
3
+ class Image
4
+ attr_accessor :actions, :name, :from
5
+
6
+ def initialize(name, from)
7
+ @actions = []
8
+ @name = name
9
+ @from = from
10
+ end
11
+
12
+ # Evaluate block within DSL context
13
+ def dsl_eval(block)
14
+ @dsl ||= DSL.new(self)
15
+ @dsl.instance_eval(&block)
16
+ self
17
+ end
18
+
19
+ # Private DSL
20
+ class DSL
21
+ def initialize(image)
22
+ @image = image
23
+ end
24
+
25
+ # Define action methods for all commands
26
+ ImageCommands.commands.each do |id, command_class|
27
+ define_method(id) do |*args|
28
+ @image.actions << command_class.send(:action, *args)
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # Auto-require for all Image commands
2
+ Dir[File.dirname(__FILE__) + '/image_commands/*.rb'].each { |file| require file }
3
+
4
+ # Module containing all Image commands
5
+ module Phoebo::Config::ImageCommands
6
+
7
+ # Returns all discovered commands
8
+ def self.commands
9
+ unless @commands
10
+ @commands = {}
11
+ self.constants.each do |c|
12
+ c = self.const_get(c)
13
+ next unless Class === c
14
+ @commands[c.id] = c
15
+ end
16
+ end
17
+
18
+ @commands
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module Phoebo::Config::ImageCommands
2
+ class Add
3
+ def self.id
4
+ :add
5
+ end
6
+
7
+ def self.action(src, dest)
8
+ return Proc.new do |dockerfile, files|
9
+
10
+ virtual = 'project/' + src
11
+ files[virtual] = src
12
+
13
+ dockerfile << 'ADD ' + virtual + ' ' + dest
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ module Phoebo::Config::ImageCommands
2
+ class Run
3
+ def self.id
4
+ :run
5
+ end
6
+
7
+ def self.action(cmd, *args)
8
+ return Proc.new do |dockerfile, files|
9
+ # TODO: decide which exec method we will use and enforce escaping
10
+ dockerfile << "RUN #{cmd} #{args.join(' ')}"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ #
2
+ # Helper mixin which allows to pass different IO objects as STDOUT/STDERR.
3
+ # It creates consistent output layer for our classes.
4
+ #
5
+ # Use `include Phoebo::Console` in classes which require to write console output.
6
+ # You can override output stream for each object on class and instance level.
7
+ #
8
+ # MyClass.stdout = StringIO.new sets stream for all future instances
9
+ # MyClass.new.stdout = StringIO.new sets stream for a particular instance
10
+ #
11
+ # Default output streams are $stdout / $stderr
12
+ #
13
+ module Phoebo::Console
14
+
15
+ attr_writer :stdout, :stderr
16
+
17
+ def stdout
18
+ @stdout ||= self.class.stdout
19
+ end
20
+
21
+ def stderr
22
+ @stderr ||= self.class.stderr
23
+ end
24
+
25
+ # We want to also allow Class defaults
26
+ module ClassMethods
27
+ attr_writer :stdout, :stderr
28
+
29
+ def stdout
30
+ @stdout ||= $stdout
31
+ end
32
+
33
+ def stderr
34
+ @stderr ||= $stderr
35
+ end
36
+ end
37
+
38
+ # On include: also extend by ClassMethods
39
+ def self.included(host_class)
40
+ host_class.extend(ClassMethods)
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ module Phoebo
2
+ module Docker
3
+ autoload :ImageBuilder, 'phoebo/docker/image_builder'
4
+ autoload :ImagePusher, 'phoebo/docker/image_pusher'
5
+ end
6
+ end
@@ -0,0 +1,85 @@
1
+ require 'rubygems/package'
2
+
3
+ module Phoebo
4
+ module Docker
5
+ class ImageBuilder
6
+ include Console
7
+
8
+ def initialize(base_path)
9
+ @base_path = Pathname.new(base_path)
10
+ end
11
+
12
+ def build(image)
13
+ project_files = { }
14
+ dockerfile = []
15
+ dockerfile << "FROM #{image.from}"
16
+
17
+ # Apply all defined actions
18
+ image.actions.each do |action|
19
+ action.call(dockerfile, project_files)
20
+ end
21
+
22
+ # TODO: Honor file mode
23
+ tar_stream = StringIO.new
24
+ Gem::Package::TarWriter.new(tar_stream) do |tar|
25
+ tar.add_file('Dockerfile', 0640) { |out_stream| out_stream.write(dockerfile.join("\n")) }
26
+ tar.mkdir('project', 0750)
27
+
28
+ # Copy project file into virtual destination
29
+ while !project_files.empty? do
30
+ virtual, relative = project_files.shift
31
+ real = @base_path + relative
32
+
33
+ if real.directory?
34
+ real.each_entry do |child|
35
+ next if child.fnmatch?('..') or child.fnmatch?('.')
36
+ if child.directory?
37
+ tar.mkdir("#{virtual}/#{child}", 0750)
38
+ end
39
+
40
+ # Add to stack
41
+ project_files["#{virtual}/#{child}"] = "#{relative}/#{child}"
42
+ end
43
+
44
+ elsif real.file?
45
+
46
+ # 'file.txt' -> 'dest/'
47
+ if virtual.end_with?('/')
48
+ dest += real.basename.to_s
49
+ end
50
+
51
+ # Read file to our TAR output stream
52
+ tar.add_file(virtual, 0640) do |out_stream|
53
+ in_stream = real.open('r')
54
+ IO.copy_stream(in_stream, out_stream)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ begin
61
+ stdout.puts "Building image " + "...".light_black
62
+ built_image = ::Docker::Image.build_from_tar(tar_stream.tap(&:rewind))
63
+
64
+
65
+ # TODO: Perhaps we should calculate size from virtual size of the base,
66
+ # this is not working very well while merging layers.
67
+ stdout.puts "Image build successful".green
68
+ stdout.print "ID: " + built_image.id.to_s.cyan
69
+ stdout.print " size: " + ('%.2f MB' % (built_image.json['Size'].to_f / 1000 / 1000)).cyan if built_image.json['Size'] > 100
70
+ stdout.print " virtual size: " + ('%.2f MB' % (built_image.json['VirtualSize'].to_f / 1000 / 1000)).cyan
71
+ stdout.puts
72
+
73
+ stdout.puts "Tagging image #{built_image.id.to_s.cyan} -> #{image.name.cyan}"
74
+ built_image.tag('repo' => image.name, 'force' => true)
75
+
76
+ # Return image ID
77
+ built_image.id
78
+
79
+ rescue ::Docker::Error::UnexpectedResponseError => e
80
+ raise DockerError, e.message
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,30 @@
1
+ module Phoebo
2
+ module Docker
3
+ class ImagePusher
4
+ include Console
5
+
6
+ def initialize(username, password, email)
7
+ @username = username
8
+ @password = password
9
+ @email = email
10
+ end
11
+
12
+ def push(image_id)
13
+ unless @authenticated
14
+ @authenticated = true
15
+ stdout.puts "Authenticating " + "...".light_black
16
+ begin
17
+ ::Docker.authenticate!({'username' => @username, 'password' => @password, 'email' => @email}, ::Docker.connection)
18
+ rescue ::Docker::Error::AuthenticationError => e
19
+ raise DockerError, 'Authentication to docker registry failed.'
20
+ end
21
+ end
22
+
23
+ stdout.puts "Pushing image " + image_id.cyan + " ...".light_black
24
+
25
+ built_image = ::Docker::Image.get(image_id)
26
+ built_image.push
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,63 @@
1
+ # Provides layer for accessing external environment
2
+ class Phoebo::Environment
3
+
4
+ # Reader definition macro
5
+ def self.exe_path_reader(name, bin = nil)
6
+ define_method("#{name}_path".to_sym) do
7
+ if instance_variable_defined? "@#{name}"
8
+ instance_variable_get "@#{name}"
9
+ else
10
+ instance_variable_set "@#{name}", path = exe_lookup(bin || name)
11
+ path
12
+ end
13
+ end
14
+ end
15
+
16
+ # Path to executables
17
+ # Example: exe_path_reader :our_bin, 'binary-name-to-lookup'
18
+ # -> Environment.our_bin_path
19
+ exe_path_reader :git
20
+ exe_path_reader :bash
21
+ exe_path_reader :ssh_agent, 'ssh-agent'
22
+ exe_path_reader :ssh_add, 'ssh-add'
23
+
24
+ # Constructor
25
+ def initialize(env = ENV)
26
+ @env = env
27
+ end
28
+
29
+ # Temporary files
30
+ def temp_path
31
+ "#{File::SEPARATOR}tmp"
32
+ end
33
+
34
+ # Check availability
35
+ def check_availability(*executables)
36
+ executables.each do |exe|
37
+ fail Phoebo::ExternalError, "Program #{exe} not found" \
38
+ unless send("#{exe}_path".to_sym)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Look up executable in system path ($PATH)
45
+ def exe_lookup(bin)
46
+ if @system_path || @env['PATH']
47
+ @system_path ||= @env['PATH'].encode(
48
+ 'UTF-8', 'binary',
49
+ invalid: :replace,
50
+ undef: :replace,
51
+ replace: ''
52
+ ).split(File::PATH_SEPARATOR)
53
+
54
+ @system_path.each do |path|
55
+ file_path = "#{path}#{File::SEPARATOR}#{bin}"
56
+ return file_path if File.executable? file_path
57
+ end
58
+
59
+ nil
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,5 @@
1
+ module Phoebo
2
+ module Util
3
+ autoload :TempFileManager, 'phoebo/util/temp_file_manager'
4
+ end
5
+ end
@@ -0,0 +1,35 @@
1
+ require 'fileutils'
2
+
3
+ # Temporary File Manager
4
+ #
5
+ # - Creates temporary directory exclusive for our process
6
+ # - Cleans up
7
+ #
8
+ class Phoebo::Util::TempFileManager
9
+
10
+ def initialize(temp_path)
11
+ @base_path = temp_path
12
+ end
13
+
14
+ def path(*components)
15
+ unless @process_path
16
+ @process_path = "#{@base_path}#{File::SEPARATOR}phoebo-#{Process.pid}"
17
+ FileUtils::mkdir_p @process_path
18
+ FileUtils::chmod 0600, @process_path
19
+ end
20
+
21
+ File.join(@process_path, components)
22
+ end
23
+
24
+ def need_cleanup?
25
+ @process_path != nil && File.exist?(@process_path)
26
+ end
27
+
28
+ def cleanup
29
+ if @process_path
30
+ FileUtils.remove_dir @process_path
31
+ @process_path = nil
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,3 @@
1
+ module Phoebo
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'phoebo/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "phoebo"
8
+ spec.version = Phoebo::VERSION
9
+ spec.authors = ["Adam Staněk"]
10
+ spec.email = ["adam.stanek@v3net.cz"]
11
+ spec.summary = %q{CI worker for creating Docker images}
12
+ spec.description = %q{Phoebo creates ready-to-deploy Docker images from project Git repository}
13
+ spec.homepage = "https://gitlab.fit.cvut.cz/phoebo/phoebo"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = '~> 2.1'
22
+
23
+ spec.add_dependency "colorize", '~> 0'
24
+ spec.add_dependency "docker-api", '~> 1.17'
25
+ spec.add_dependency "rugged", '~> 0.21'
26
+ spec.add_development_dependency "bundler", "~> 1.7"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "rspec", '~> 3.0'
29
+ end
@@ -0,0 +1,51 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Phoebo::Application do
4
+
5
+ # Set output stream before test
6
+ before(:all) {
7
+ described_class.stdout = StringIO.new
8
+ described_class.stderr = StringIO.new
9
+ }
10
+
11
+ # Clean output streams before each run
12
+ before(:each) {
13
+ described_class.stdout.truncate(0)
14
+ described_class.stderr.truncate(0)
15
+ }
16
+
17
+ context 'general' do
18
+ subject(:app) { described_class.new }
19
+
20
+ it 'is runnable' do
21
+ expect(app.respond_to?(:run)).to eq true
22
+ end
23
+
24
+ it 'holds instance' do
25
+ app
26
+ expect(described_class.instance.is_a?(described_class)).to be true
27
+ end
28
+
29
+ it 'creates Environment' do
30
+ expect(app.environment.is_a?(Phoebo::Environment)).to be true
31
+ end
32
+
33
+ it 'creates TemporaryFileManager' do
34
+ expect(app.temp_file_manager.is_a?(Phoebo::Util::TempFileManager)).to be true
35
+ end
36
+ end
37
+
38
+ context '--version argument' do
39
+ subject(:app) { described_class.new(['--version']) }
40
+
41
+ it 'returns 0' do
42
+ expect(app.run).to eq 0
43
+ end
44
+
45
+ it 'shows version' do
46
+ app.run
47
+ expect(app.stdout.string).to include(Phoebo::VERSION)
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,80 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Phoebo::Console do
4
+ before {
5
+ @sample_class = Class.new do
6
+ include Phoebo::Console
7
+ end
8
+ }
9
+
10
+ context 'default streams' do
11
+ subject {
12
+ @sample_class.new
13
+ }
14
+
15
+ it 'stdout returns io' do
16
+ expect(subject.stdout.is_a?(IO)).to eq true
17
+ end
18
+
19
+ it 'stderr returns io' do
20
+ expect(subject.stderr.is_a?(IO)).to eq true
21
+ end
22
+ end
23
+
24
+ context 'streams set for class' do
25
+ subject {
26
+ sample_class = @sample_class.dup
27
+ sample_class.stdout = StringIO.new
28
+ sample_class.stderr = StringIO.new
29
+ sample_class.new
30
+ }
31
+
32
+ it 'does not effect default streams' do
33
+ expect(subject.stdout).not_to be @sample_class.stdout
34
+ expect(subject.stderr).not_to be @sample_class.stderr
35
+ end
36
+
37
+ it 'stdout returns class default' do
38
+ expect(subject.stdout.is_a?(StringIO)).to eq true
39
+ expect(subject.stdout).to be subject.class.stdout
40
+ end
41
+
42
+ it 'stderr returns class default' do
43
+ expect(subject.stderr.is_a?(StringIO)).to eq true
44
+ expect(subject.stderr).to be subject.class.stderr
45
+ end
46
+
47
+ it 'stdout != stderr' do
48
+ expect(subject.stdout).not_to be subject.stderr
49
+ end
50
+ end
51
+
52
+ context 'streams set for instance' do
53
+ subject {
54
+ sample_class = @sample_class.dup
55
+ sample_class.stdout = StringIO.new
56
+ sample_class.stderr = StringIO.new
57
+
58
+ subject = sample_class.new
59
+ subject.stdout = StringIO.new
60
+ subject.stderr = StringIO.new
61
+
62
+ subject
63
+ }
64
+
65
+ it 'stdout returns set io' do
66
+ expect(subject.stdout.is_a?(StringIO)).to eq true
67
+ expect(subject.stdout).not_to be subject.class.stdout
68
+ end
69
+
70
+ it 'stderr returns set io' do
71
+ expect(subject.stderr.is_a?(StringIO)).to eq true
72
+ expect(subject.stderr).not_to be subject.class.stderr
73
+ end
74
+
75
+ it 'stdout != stderr' do
76
+ expect(subject.stdout).not_to be subject.stderr
77
+ end
78
+ end
79
+
80
+ end
@@ -0,0 +1,31 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Phoebo::Environment do
4
+ before(:all) {
5
+ @path = [
6
+ "#{File::SEPARATOR}a",
7
+ "#{File::SEPARATOR}b",
8
+ "#{File::SEPARATOR}c"
9
+ ]
10
+ }
11
+
12
+ subject(:env) {
13
+ described_class.new({'PATH' => @path.join(File::PATH_SEPARATOR) })
14
+ }
15
+
16
+ context 'path lookup' do
17
+ it 'looks up path' do
18
+ allow(File).to receive(:executable?).with("#{@path[0]}#{File::SEPARATOR}bash").and_return(false)
19
+ allow(File).to receive(:executable?).with("#{@path[1]}#{File::SEPARATOR}bash").and_return(true)
20
+ allow(File).to receive(:executable?).with("#{@path[2]}#{File::SEPARATOR}bash").and_return(true)
21
+ expect(env.bash_path).to eq "#{@path[1]}#{File::SEPARATOR}bash"
22
+ end
23
+
24
+ it 'nil if not found' do
25
+ @path.each { |path|
26
+ allow(File).to receive(:executable?).with("#{path}#{File::SEPARATOR}git").and_return(false)
27
+ }
28
+ expect(env.git_path).to eq nil
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ describe Phoebo::Util::TempFileManager, :order => :defined do
4
+ before (:all) {
5
+ @manager = described_class.new("#{File::SEPARATOR}tmp")
6
+ }
7
+
8
+ it 'does not need clean-up at init' do
9
+ expect(@manager.need_cleanup?).to eq false
10
+ end
11
+ # ->
12
+ it 'creates path' do
13
+ @manager.path 'a', 'b'
14
+ end
15
+ # ->
16
+ it 'does need to clean-up' do
17
+ expect(@manager.need_cleanup?).to eq true
18
+ end
19
+ # ->
20
+ it 'cleans up' do
21
+ @manager.cleanup
22
+ end
23
+ end
@@ -0,0 +1,99 @@
1
+ $:.unshift File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'bundler/setup'
4
+ Bundler.setup
5
+
6
+ require 'phoebo'
7
+
8
+ # This file was generated by the `rspec --init` command. Conventionally, all
9
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
10
+ # The generated `.rspec` file contains `--require spec_helper` which will cause this
11
+ # file to always be loaded, without a need to explicitly require it in any files.
12
+ #
13
+ # Given that it is always loaded, you are encouraged to keep this file as
14
+ # light-weight as possible. Requiring heavyweight dependencies from this file
15
+ # will add to the boot time of your test suite on EVERY test run, even for an
16
+ # individual file that may not need all of that loaded. Instead, consider making
17
+ # a separate helper file that requires the additional dependencies and performs
18
+ # the additional setup, and require it from the spec files that actually need it.
19
+ #
20
+ # The `.rspec` file also contains a few flags that are not defaults but that
21
+ # users commonly want.
22
+ #
23
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
24
+ RSpec.configure do |config|
25
+
26
+ config.formatter = :documentation
27
+
28
+ # rspec-expectations config goes here. You can use an alternate
29
+ # assertion/expectation library such as wrong or the stdlib/minitest
30
+ # assertions if you prefer.
31
+ config.expect_with :rspec do |expectations|
32
+ # This option will default to `true` in RSpec 4. It makes the `description`
33
+ # and `failure_message` of custom matchers include text for helper methods
34
+ # defined using `chain`, e.g.:
35
+ # be_bigger_than(2).and_smaller_than(4).description
36
+ # # => "be bigger than 2 and smaller than 4"
37
+ # ...rather than:
38
+ # # => "be bigger than 2"
39
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
40
+ end
41
+
42
+ # rspec-mocks config goes here. You can use an alternate test double
43
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
44
+ config.mock_with :rspec do |mocks|
45
+ # Prevents you from mocking or stubbing a method that does not exist on
46
+ # a real object. This is generally recommended, and will default to
47
+ # `true` in RSpec 4.
48
+ mocks.verify_partial_doubles = true
49
+ end
50
+
51
+ # The settings below are suggested to provide a good initial experience
52
+ # with RSpec, but feel free to customize to your heart's content.
53
+ =begin
54
+ # These two settings work together to allow you to limit a spec run
55
+ # to individual examples or groups you care about by tagging them with
56
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
57
+ # get run.
58
+ config.filter_run :focus
59
+ config.run_all_when_everything_filtered = true
60
+
61
+ # Limits the available syntax to the non-monkey patched syntax that is recommended.
62
+ # For more details, see:
63
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
64
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
65
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
66
+ config.disable_monkey_patching!
67
+
68
+ # This setting enables warnings. It's recommended, but in some cases may
69
+ # be too noisy due to issues in dependencies.
70
+ config.warnings = true
71
+
72
+ # Many RSpec users commonly either run the entire suite or an individual
73
+ # file, and it's useful to allow more verbose output when running an
74
+ # individual spec file.
75
+ if config.files_to_run.one?
76
+ # Use the documentation formatter for detailed output,
77
+ # unless a formatter has already been configured
78
+ # (e.g. via a command-line flag).
79
+ config.default_formatter = 'doc'
80
+ end
81
+
82
+ # Print the 10 slowest examples and example groups at the
83
+ # end of the spec run, to help surface which specs are running
84
+ # particularly slow.
85
+ config.profile_examples = 10
86
+
87
+ # Run specs in random order to surface order dependencies. If you find an
88
+ # order dependency and want to debug it, you can fix the order by providing
89
+ # the seed, which is printed after each run.
90
+ # --seed 1234
91
+ config.order = :random
92
+
93
+ # Seed global randomization in this process using the `--seed` CLI option.
94
+ # Setting this allows you to use `--seed` to deterministically reproduce
95
+ # test failures related to randomization by passing the same `--seed` value
96
+ # as the one that triggered the failure.
97
+ Kernel.srand config.seed
98
+ =end
99
+ end
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: phoebo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Adam Staněk
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
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: docker-api
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.17'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.17'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rugged
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.21'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.21'
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.7'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.7'
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: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ description: Phoebo creates ready-to-deploy Docker images from project Git repository
98
+ email:
99
+ - adam.stanek@v3net.cz
100
+ executables:
101
+ - phoebo
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - Gemfile
108
+ - LICENSE.txt
109
+ - README.md
110
+ - Rakefile
111
+ - bin/phoebo
112
+ - lib/phoebo.rb
113
+ - lib/phoebo/application.rb
114
+ - lib/phoebo/config.rb
115
+ - lib/phoebo/config/image.rb
116
+ - lib/phoebo/config/image_commands.rb
117
+ - lib/phoebo/config/image_commands/add.rb
118
+ - lib/phoebo/config/image_commands/run.rb
119
+ - lib/phoebo/console.rb
120
+ - lib/phoebo/docker.rb
121
+ - lib/phoebo/docker/image_builder.rb
122
+ - lib/phoebo/docker/image_pusher.rb
123
+ - lib/phoebo/environment.rb
124
+ - lib/phoebo/util.rb
125
+ - lib/phoebo/util/temp_file_manager.rb
126
+ - lib/phoebo/version.rb
127
+ - phoebo.gemspec
128
+ - spec/phoebo/application_spec.rb
129
+ - spec/phoebo/console_spec.rb
130
+ - spec/phoebo/environment_spec.rb
131
+ - spec/phoebo/util/temp_file_manager_spec.rb
132
+ - spec/spec_helper.rb
133
+ homepage: https://gitlab.fit.cvut.cz/phoebo/phoebo
134
+ licenses:
135
+ - MIT
136
+ metadata: {}
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '2.1'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubyforge_project:
153
+ rubygems_version: 2.4.5
154
+ signing_key:
155
+ specification_version: 4
156
+ summary: CI worker for creating Docker images
157
+ test_files:
158
+ - spec/phoebo/application_spec.rb
159
+ - spec/phoebo/console_spec.rb
160
+ - spec/phoebo/environment_spec.rb
161
+ - spec/phoebo/util/temp_file_manager_spec.rb
162
+ - spec/spec_helper.rb