phoebo 0.1.2 → 0.2.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: b51f88a605d5610ffe5ad9a116133f1bfbd827a6
4
- data.tar.gz: 5eab25352327affc5cd992d7992665ccaef95550
3
+ metadata.gz: 31be5faeea137754c6c0047c9f1bc0da857512df
4
+ data.tar.gz: db94bcf4c396136cf0838d1273b75cc8acd7beda
5
5
  SHA512:
6
- metadata.gz: 9ab14f8c74e16a695d331ad31beb983b43e7b84506802861ab6fa177d69f2ad6193a7b212c8e06dd7605c9c55701d6f535583ab1770f69cc9d72848f5796b72b
7
- data.tar.gz: ebf9b8b8d13cf6b8897e1a03aa0d39adedbad4d8c279001d13a0a839c045c654f1a0a82841cca73d769bd73466f3a4362dcca865dc47dbfe491a39f7bcb8a45e
6
+ metadata.gz: 08325e381ee53679aac752bd2809b0fd2a27b81ee7a511cfdd8605d716342cc029d2241e30d4efb39cd2854eb593c035f4e32e7febfec81f0cc994c08685de6d
7
+ data.tar.gz: 71b3afc4d20ff1c830e68901a725ca09608f3e8cc091d529aef847c520e78a66b46521cb3d001daffc8fc1e4cec4f3d4b46634fa7b6eeed2eb724b02ce99750d
data/README.md CHANGED
@@ -1,11 +1,95 @@
1
- # Run
1
+ # Phoebo
2
2
 
3
- ```bash
4
- $ bundle exec bin/phoebo
5
- ```
3
+ ## DSL example
6
4
 
7
- # Run tests
5
+ ~~~ruby
6
+ Phoebo.configure(1) {
8
7
 
9
- ```bash
10
- rspec
11
- ```
8
+ # Create image based on Apache + mod_php
9
+ image('phoebo/nette-example', 'phoebo/simple-apache-php') {
10
+ # Copy all application data
11
+ add('.', '/app/')
12
+
13
+ # Install dependencies
14
+ run('composer', 'install', '--prefer-dist')
15
+
16
+ # Set document root
17
+ run('ln', '-s', '/app/www', '/var/www')
18
+ }
19
+
20
+ # Deploy image and keep it runing (default CMD: apache)
21
+ task('deploy', 'phoebo/nette-example')
22
+
23
+ # Run tests on image
24
+ task('test', 'phoebo/nette-example', '/app/bin/tester', '/app/tests')
25
+ }
26
+ ~~~
27
+
28
+ ## Usage
29
+
30
+ ~~~bash
31
+ sudo docker pull phoebo/phoebo:latest
32
+ sudo docker run -ti --privileged -v ~/nette-example-keys:/root/deploy-keys phoebo/phoebo:latest phoebo \
33
+ --repository ssh://gitlab.fit.cvut.cz/phoebo/nette-example.git \
34
+ --ssh-key /root/deploy-keys/key \
35
+ --ssh-public /root/deploy-keys/key.pub \
36
+ --docker-user joe \
37
+ --docker-password secret123 \
38
+ --docker-email joe@domain.tld
39
+ ~~~
40
+
41
+ ### Process request from URL
42
+
43
+ For better integration into your CI workflow you can specify build job by URL instead of verbose CLI arguments.
44
+
45
+ ~~~bash
46
+ phoebo --from-url http://domain.tld/api/requests/2e2996fe8420
47
+ ~~~
48
+
49
+ URL should return JSON formatted payload with following structure:
50
+
51
+ ~~~javascript
52
+ {
53
+ "id": "2e2996fe8420",
54
+ "repo_url": "ssh://gitlab.fit.cvut.cz/phoebo/nette-example.git",
55
+ "ssh_public": "ssh-rsa AAAAB3NzaC1yc2E...",
56
+ "ssh_private": "-----BEGIN RSA PRIVATE KEY----- ...",
57
+ "docker_user": "joe",
58
+ "docker_password": "secret123",
59
+ "docker_email": "joe@domain.tld"
60
+ }
61
+ ~~~
62
+
63
+ If other CLI arguments are present they will override those loaded from URL.
64
+
65
+ ### Ping back
66
+
67
+ You can optionally let Phoebo notify you when the build is ready.
68
+
69
+ ~~~bash
70
+ phoebo --ping-url http://domain.tld/api/notify
71
+ ~~~
72
+
73
+ Notification is sent as JSON formatted HTTP POST request with structure bellow.
74
+ The payload contains the same ID you've passed to application with your JSON request.
75
+ Other than that payload contains list of tasks with arguments and image on which they should be run.
76
+
77
+ ~~~javascript
78
+ {
79
+ "id": "2e2996fe8420",
80
+ "tasks":[
81
+ {
82
+ "name":"deploy",
83
+ "image":"phoebo/nette-example"
84
+ },
85
+ {
86
+ "name":"test",
87
+ "image":"phoebo/nette-example",
88
+ "cmd":[
89
+ "/app/bin/tester",
90
+ "/app/tests"
91
+ ]
92
+ }
93
+ ]
94
+ }
95
+ ~~~
data/lib/phoebo.rb CHANGED
@@ -1,12 +1,16 @@
1
1
  require 'phoebo/version'
2
2
 
3
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'
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 :Git, 'phoebo/git'
10
+ autoload :Ping, 'phoebo/ping'
11
+ autoload :Request, 'phoebo/request'
12
+ autoload :Util, 'phoebo/util'
13
+ autoload :Worker, 'phoebo/worker'
10
14
 
11
15
  class PhoeboError < StandardError
12
16
  def self.status_code(code)
@@ -15,6 +19,7 @@ module Phoebo
15
19
  end
16
20
 
17
21
  class InvalidArgumentError < PhoeboError; status_code(4) ; end
22
+ class InvalidRequestError < InvalidArgumentError ; end
18
23
  class IOError < PhoeboError; status_code(5) ; end
19
24
  class ExternalError < PhoeboError; status_code(6) ; end
20
25
 
@@ -28,7 +33,7 @@ module Phoebo
28
33
  # Configure Phoebo environment from Phoebofile
29
34
  def self.configure(version, &block)
30
35
  raise ConfigError, "Configuration version #{version} not supported" if version != 1
31
- Config.load_from_block(block)
36
+ Config.new_from_block(block)
32
37
  end
33
38
 
34
39
  end
@@ -1,7 +1,5 @@
1
1
  require 'optparse'
2
2
  require 'colorize'
3
- require 'docker'
4
- require 'rugged'
5
3
  require 'pathname'
6
4
 
7
5
  module Phoebo
@@ -23,6 +21,8 @@ module Phoebo
23
21
  class Application
24
22
  include Console
25
23
 
24
+ attr_writer :environment, :temp_file_manager
25
+
26
26
  # Creates application
27
27
  def initialize(args = [])
28
28
  @args = args
@@ -55,7 +55,7 @@ module Phoebo
55
55
  stderr.puts
56
56
  end
57
57
 
58
- send(('run_' + options[:mode].to_s).to_sym, options).to_i || result
58
+ [send(('run_' + options[:mode].to_s).to_sym, options).to_i, result].max
59
59
  end
60
60
 
61
61
  # Cleanup sequence
@@ -72,93 +72,69 @@ module Phoebo
72
72
  # Run in normal mode
73
73
  def run_normal(options)
74
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]
75
+ request = Request.new
76
+ request.load_from_hash!(options[:request])
80
77
 
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]
78
+ if options[:request_file]
79
+ stdout.puts "Loading request from file: " + options[:request_file].cyan
80
+ request.load_from_file!(options[:request_file])
81
+ end
87
82
 
88
- public_path = Pathname.new(options[:ssh_public])
89
- raise InvalidArgumentError, "Public SSH key not found" unless public_path.exist?
83
+ if options[:request_url]
84
+ stdout.puts "Fetching request from URL: " + options[:request_url].cyan + "...".light_black
85
+ request.load_from_url!(options[:request_url])
86
+ end
90
87
 
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
88
+ # Raises InvalidRequestError if invalid
89
+ request.validate
99
90
 
100
- # Create temp dir.
91
+ # Prepare project directory
92
+ if request.repo_url
93
+ # Create temp dir & clone
101
94
  project_path = temp_file_manager.path('project')
95
+ stdout.puts "Cloning remote repository " + "...".light_black
96
+ Git.clone(request, project_path)
102
97
 
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
98
  else
126
- pusher = nil
99
+ project_path = Dir.pwd
127
100
  end
128
101
 
129
- # Exit code
102
+ # Worker
103
+ worker = Worker.new(request)
130
104
  result = 0
105
+ configs = []
131
106
 
132
- # Process all the files
107
+ # Process all
133
108
  options[:files] << '.' if options[:files].empty?
134
109
  options[:files].each do |rel_path|
110
+ path = (Pathname.new(project_path) + rel_path)
135
111
 
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
112
+ # Directory does not exist -> resume with next dir.
113
+ if path.exist?
114
+ path = path.realpath.to_s
115
+ else
116
+ stderr.puts "Directory #{path.to_s} does not exist".red
145
117
  result = 1
146
118
  next
147
119
  end
148
120
 
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
121
+ # Directory does not have any config -> nothing to do -> resume with next dir.
122
+ unless config = worker.process(path)
123
+ stderr.puts "No config found in #{path}".red
124
+ result = 1
125
+ next
157
126
  end
127
+
128
+ configs << config
158
129
  end
159
130
 
160
- puts "Everything done :-)".green
131
+ # Ping
132
+ if request.ping_url
133
+ stdout.puts "Notifying URL: " + request.ping_url.cyan + "...".light_black
134
+ Ping.send(request, configs)
135
+ end
161
136
 
137
+ stdout.puts "Everything done :-)".green
162
138
  result
163
139
  end
164
140
 
@@ -176,7 +152,7 @@ module Phoebo
176
152
 
177
153
  # Parse passed command-line options
178
154
  def parse_options
179
- options = { error: nil, mode: :normal, files: [], ssh_user: 'git' }
155
+ options = { error: nil, mode: :normal, request: {}, files: [], ssh_user: 'git' }
180
156
 
181
157
  unless @option_parser
182
158
  @option_parser = OptionParser.new
@@ -184,31 +160,43 @@ module Phoebo
184
160
  "Usage: #{@option_parser.program_name} [options] file"
185
161
 
186
162
  @option_parser.on_tail('-rURL', '--repository=URL', 'Repository URL') do |value|
187
- options[:repository] = value
163
+ options[:request][:repo_url] = value
188
164
  end
189
165
 
190
166
  @option_parser.on_tail('--ssh-user=USERNAME', 'Username for Git over SSH (Default: git)') do |value|
191
- options[:ssh_user] = value
167
+ options[:request][:ssh_user] = value
192
168
  end
193
169
 
194
170
  @option_parser.on_tail('--ssh-public=PATH', 'Path to public SSH key for Git repository') do |value|
195
- options[:ssh_public] = value
171
+ options[:request][:ssh_public_file] = value
196
172
  end
197
173
 
198
174
  @option_parser.on_tail('--ssh-key=PATH', 'Path to SSH key for Git repository') do |value|
199
- options[:ssh_key] = value
175
+ options[:request][:ssh_private_file] = value
200
176
  end
201
177
 
202
178
  @option_parser.on_tail('--docker-user=USERNAME', 'Username for Docker Registry') do |value|
203
- options[:docker_username] = value
179
+ options[:request][:docker_user] = value
204
180
  end
205
181
 
206
182
  @option_parser.on_tail('--docker-password=PASSWORD', 'Password for Docker Registry') do |value|
207
- options[:docker_password] = value
183
+ options[:request][:docker_password] = value
208
184
  end
209
185
 
210
186
  @option_parser.on_tail('--docker-email=EMAIL', 'E-mail for Docker Registry') do |value|
211
- options[:docker_email] = value
187
+ options[:request][:docker_email] = value
188
+ end
189
+
190
+ @option_parser.on_tail('--ping-url=REQUEST_URL', 'URL will be notified after build') do |value|
191
+ options[:request][:ping_url] = value
192
+ end
193
+
194
+ @option_parser.on_tail('-U', '--from-url=REQUEST_URL', 'Process build request from URL') do |value|
195
+ options[:request_url] = value
196
+ end
197
+
198
+ @option_parser.on_tail('-f', '--from-file=PATH', 'Process build request from local file') do |value|
199
+ options[:request_file] = value
212
200
  end
213
201
 
214
202
  @option_parser.on_tail('--version', 'Show version info') do
data/lib/phoebo/config.rb CHANGED
@@ -3,14 +3,14 @@ module Phoebo
3
3
  autoload :Image, 'phoebo/config/image'
4
4
  autoload :ImageCommands, 'phoebo/config/image_commands'
5
5
 
6
- attr_accessor :images
6
+ attr_accessor :images, :tasks
7
7
 
8
8
  # Loads config from file
9
9
  # @see Phoebo.configure()
10
- def self.load_from_file(file_path)
10
+ def self.new_from_file(file_path)
11
11
  begin
12
12
  @instance = nil
13
- Kernel.load File.expand_path(file_path), true
13
+ Kernel.load file_path, true
14
14
  @instance
15
15
 
16
16
  rescue ::SyntaxError => e
@@ -18,7 +18,7 @@ module Phoebo
18
18
  end
19
19
  end
20
20
 
21
- def self.load_from_block(block)
21
+ def self.new_from_block(block)
22
22
  @instance = self.new
23
23
  @instance.dsl_eval(block)
24
24
  end
@@ -26,6 +26,7 @@ module Phoebo
26
26
  # Instance initialization
27
27
  def initialize
28
28
  @images = []
29
+ @tasks = []
29
30
  end
30
31
 
31
32
  # Evaluate block within DSL context
@@ -42,10 +43,20 @@ module Phoebo
42
43
  end
43
44
 
44
45
  def image(name, from, &block)
45
- @config.images << (img_instance = Image.new(name, from))
46
- img_instance.dsl_eval(block)
46
+ @config.images << (img_instance = Image.new(name, from, block))
47
47
  img_instance
48
48
  end
49
+
50
+ def task(name, image, *args)
51
+ task = {
52
+ name: name,
53
+ image: image
54
+ }
55
+
56
+ task[:cmd] = args unless args.empty?
57
+ @config.tasks << task
58
+ task
59
+ end
49
60
  end
50
61
 
51
62
  end
@@ -3,10 +3,11 @@ module Phoebo
3
3
  class Image
4
4
  attr_accessor :actions, :name, :from
5
5
 
6
- def initialize(name, from)
6
+ def initialize(name, from, block = nil)
7
7
  @actions = []
8
8
  @name = name
9
9
  @from = from
10
+ dsl_eval(block) if block
10
11
  end
11
12
 
12
13
  # Evaluate block within DSL context
@@ -1,4 +1,6 @@
1
1
  require 'rubygems/package'
2
+ require 'docker'
3
+ require 'colorize'
2
4
 
3
5
  module Phoebo
4
6
  module Docker
@@ -1,3 +1,5 @@
1
+ require 'docker'
2
+
1
3
  module Phoebo
2
4
  module Docker
3
5
  class ImagePusher
data/lib/phoebo/git.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'rugged'
2
+
3
+ module Phoebo
4
+ class Git
5
+ def self.clone(request, clone_path)
6
+ # Use passed SSH keys or fallback to system authentication
7
+ if request.ssh_private_file
8
+ cred = Rugged::Credentials::SshKey.new(
9
+ username: request.ssh_user,
10
+ publickey: request.ssh_public_file,
11
+ privatekey: request.ssh_private_file
12
+ )
13
+ else
14
+ cred = Rugged::Credentials::Default.new
15
+ end
16
+
17
+ # Clone remote repository
18
+ begin
19
+ Rugged::Repository.clone_at request.repo_url,
20
+ clone_path, credentials: cred
21
+
22
+ rescue Rugged::SshError => e
23
+ raise unless e.message.include?('authentication')
24
+ raise IOError, "Unable to clone remote repository. SSH authentication failed."
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+
4
+ module Phoebo
5
+ class Ping
6
+ def self.send(request, configs)
7
+ payload = {
8
+ id: request.id,
9
+ tasks: []
10
+ }
11
+
12
+ configs.each do |config|
13
+ payload[:tasks] += config.tasks
14
+ end
15
+
16
+ uri = URI(request.ping_url)
17
+ req = Net::HTTP::Post.new(uri, {'Content-Type' =>'application/json'})
18
+ req.body = payload.to_json
19
+ res = Net::HTTP.start(uri.hostname, uri.port) do |http|
20
+ http.request(req)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,132 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ module Phoebo
6
+ # Application request
7
+ # Class is used as a container for user input and to perform easy merging user
8
+ # input from multiple sources (HTTP request, CLI options) and provides data
9
+ # validation with human-readable error messages.
10
+ class Request
11
+ attr_accessor :repo_url, :ssh_user, :ssh_private_file, :ssh_public_file
12
+ attr_accessor :docker_user, :docker_password, :docker_email
13
+ attr_accessor :id, :ping_url
14
+ attr_writer :temp_file_manager
15
+
16
+ def initialize
17
+ @ssh_user = 'git'
18
+ end
19
+
20
+ # Temporary file manager
21
+ def temp_file_manager
22
+ @temp_file_manager ||= Application.instance.temp_file_manager
23
+ end
24
+
25
+ # Allows to use SSH content instead of SSH key file
26
+ # (We need to place it into temporary file because Rugged needs it)
27
+ def ssh_private=(key_content)
28
+ @ssh_private_file = temp_file_manager.path('key')
29
+ IO.write(@ssh_private_file, key_content)
30
+ end
31
+
32
+ # Allows to use SSH content instead of SSH key file
33
+ # (We need to place it into temporary file because Rugged needs it)
34
+ def ssh_public=(key_content)
35
+ @ssh_public_file = temp_file_manager.path('key.pub')
36
+ IO.write(@ssh_public_file, key_content)
37
+ end
38
+
39
+ # Loads request with data from hash
40
+ def load_from_hash!(hash)
41
+ hash.each do |k, v|
42
+ m = (k.to_s + '=').to_sym
43
+ raise InvalidRequestError, "Invalid key #{k.inspect}" unless respond_to?(m)
44
+ send(m, v)
45
+ end
46
+
47
+ self
48
+ end
49
+
50
+ # Loads request from JSON string
51
+ def load_from_json!(raw_data)
52
+ begin
53
+ hash = JSON.parse(raw_data)
54
+ rescue JSON::ParserError => e
55
+ raise InvalidRequestError, "Malformed JSON data."
56
+ end
57
+
58
+ load_from_hash!(hash)
59
+ end
60
+
61
+ # Loads request fro local file
62
+ def load_from_file!(file_path)
63
+ raise IOError, "File #{file_path} not exist" unless File.exist?(file_path)
64
+ raw_data = IO.read(file_path)
65
+ load_from_json!(raw_data)
66
+ end
67
+
68
+ # Loads request from URL
69
+ def load_from_url!(url)
70
+ uri = URI(url)
71
+ req = Net::HTTP::Get.new(uri)
72
+ req['Accept'] = 'application/json'
73
+
74
+ res = Net::HTTP.start(uri.hostname, uri.port) do |http|
75
+ http.request(req)
76
+ end
77
+
78
+ # TODO: handle redirects
79
+ raise IOError, "Unable to load URL #{url}" unless res.code == 200
80
+
81
+ load_from_json!(res.body)
82
+ end
83
+
84
+ # Validates request and returns array of error messages
85
+ # or empty array if there are none
86
+ def errors
87
+ errors = []
88
+
89
+ if repo_url
90
+ begin
91
+ uri = URI(repo_url)
92
+ errors << "Invalid repository URL. Only SSH supported at the moment. Expected format: ssh://host/path/to/repo.git" \
93
+ unless uri.scheme == 'ssh'
94
+ rescue URI::InvalidURIError
95
+ errors << "Invalid repository URL. Expected format: ssh://host/path/to/repo.git"
96
+ end
97
+ end
98
+
99
+ unless [ssh_private_file, ssh_public_file].select { |v| v } .empty?
100
+ errors << "Missing SSH user." unless ssh_user
101
+ errors << "Missing private SSH key." unless ssh_private_file
102
+ errors << "Missing public SSH key." unless ssh_public_file
103
+ end
104
+
105
+ unless [docker_user, docker_password, docker_email].select { |v| v } .empty?
106
+ errors << "Missing Docker user." unless docker_user
107
+ errors << "Missing Docker password." unless docker_password
108
+ errors << "Missing Docker email." unless docker_email
109
+ end
110
+
111
+ if ping_url
112
+ begin
113
+ uri = URI(ping_url)
114
+ errors << "Invalid ping URL. Only HTTP / HTTPS supported at the moment. Expected format: https://domain.tld/api/notify" \
115
+ unless uri.scheme == 'http' || uri.scheme == 'https'
116
+ rescue URI::InvalidURIError
117
+ errors << "Invalid ping URL. Expected format: https://domain.tld/api/notify"
118
+ end
119
+ end
120
+
121
+ errors
122
+ end
123
+
124
+ # Validate request and raises first discovered error as an exception
125
+ def validate
126
+ unless (e = errors).empty?
127
+ raise InvalidRequestError, e.first
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -1,3 +1,3 @@
1
1
  module Phoebo
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,38 @@
1
+ module Phoebo
2
+ class Worker
3
+ def initialize(request)
4
+ @request = request
5
+ end
6
+
7
+ def pusher
8
+ @pusher ||= Docker::ImagePusher.new(
9
+ @request.docker_user,
10
+ @request.docker_password,
11
+ @request.docker_email
12
+ )
13
+ end
14
+
15
+ def process(path)
16
+
17
+ # Prepare config path
18
+ config_filename = 'Phoebofile'
19
+ config_path = "#{path}#{File::SEPARATOR}#{config_filename}"
20
+
21
+ if File.exists?(config_path)
22
+ # Load config
23
+ config = Config.new_from_file(config_path)
24
+
25
+ # Build & push image
26
+ builder = Docker::ImageBuilder.new(path)
27
+ config.images.each do |image|
28
+ image_id = builder.build(image)
29
+ pusher.push(image_id) if @request.docker_user
30
+ end
31
+
32
+ config
33
+ else
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
@@ -48,4 +48,64 @@ describe Phoebo::Application do
48
48
  end
49
49
  end
50
50
 
51
+ context '--help argument' do
52
+ subject(:app) { described_class.new(['--help']) }
53
+
54
+ it 'returns 0' do
55
+ expect(app.run).to eq 0
56
+ end
57
+
58
+ it 'shows usage' do
59
+ app.run
60
+ expect(app.stdout.string).to include('Usage:')
61
+ end
62
+ end
63
+
64
+ context 'normal run with all arguments' do
65
+ let(:args) {[
66
+ '--repository', 'ssh://host/path/to/repo.git',
67
+ '--ssh-user', 'git',
68
+ '--ssh-key', './key',
69
+ '--ssh-public', './key.pub',
70
+ '--docker-user', 'joe',
71
+ '--docker-password', 'secret123',
72
+ '--docker-email', 'joe@domain.tld',
73
+ 'dir1', 'dir2'
74
+ ]}
75
+
76
+ subject(:app) {
77
+ app = described_class.new(args)
78
+ app.temp_file_manager = instance_double(Phoebo::Util::TempFileManager)
79
+ allow(app.temp_file_manager).to receive(:path) { |*args| "/tmp/random/#{args.join('/')}" }
80
+ allow(app.temp_file_manager).to receive(:need_cleanup?).and_return(true)
81
+ app
82
+ }
83
+
84
+ before(:each) {
85
+ allow(Phoebo::Git).to receive(:clone).and_return(nil)
86
+ }
87
+
88
+ it 'returns 1 if no files were processed' do
89
+ expect(app.run).to eq 1
90
+ end
91
+
92
+ it 'cleans temporary files' do
93
+ expect(app.temp_file_manager).to receive(:cleanup)
94
+ app.cleanup
95
+ end
96
+ end
97
+
98
+ context 'bad arguments' do
99
+ subject(:app) { described_class.new(['--foobar']) }
100
+
101
+ it 'returns 1' do
102
+ expect(app.run).to eq 1
103
+ end
104
+
105
+ it 'shows usage' do
106
+ app.run
107
+ expect(app.stdout.string).to include('Usage:')
108
+ end
109
+ end
110
+
51
111
  end
@@ -0,0 +1,19 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ describe Phoebo::Config::Image do
4
+
5
+ let(:dsl) {
6
+ Proc.new {
7
+ add('.', '/app/')
8
+ run('composer', 'install', '--prefer-dist')
9
+ }
10
+ }
11
+
12
+ subject { described_class.new('image-name', 'base-image-name') }
13
+
14
+ it 'processes commands' do
15
+ expect(Phoebo::Config::ImageCommands::Add).to receive(:action).with('.', '/app/')
16
+ expect(Phoebo::Config::ImageCommands::Run).to receive(:action).with('composer', 'install', '--prefer-dist')
17
+ subject.dsl_eval(dsl)
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Phoebo::Config do
4
+
5
+ subject { described_class }
6
+
7
+ context 'loading DSL' do
8
+ subject { described_class }
9
+
10
+ let(:dsl_content) { Proc.new { } }
11
+
12
+ it 'loads block' do
13
+ allow(Kernel).to receive(:load).with('./Phoebofile', true) do
14
+ Phoebo.configure(1, &dsl_content)
15
+ end
16
+
17
+ # It goes like this:
18
+ # 1) Config.new_from_file('./Phoebofile')
19
+ # 2) It loads file using Kernel.load (immediatly interprets it's content)
20
+ # 3) Inside of this file there is call Phoebo.configure(version) { DSL BLOCK }
21
+ # which introduces our DSL.
22
+ # 4) When Phoebo.configure() is called it passes DSL BLOCK to Config.new_from_block()
23
+ # 5) Config.new_from_block() saves instance internally
24
+ # 6) Config.new_from_file() returns saved instance
25
+
26
+ expect(subject).to receive(:new_from_block).with(dsl_content).and_call_original
27
+ expect(subject.new_from_file('./Phoebofile').is_a?(described_class)).to eq true
28
+ end
29
+
30
+ it 'raises human readable message on syntax error' do
31
+ allow(Kernel).to receive(:load).with('./Phoebofile', true) do
32
+ raise ::SyntaxError
33
+ end
34
+
35
+ expect { subject.new_from_file('./Phoebofile') }.to raise_error(Phoebo::SyntaxError)
36
+ end
37
+ end
38
+
39
+ context 'images' do
40
+ subject { described_class.new }
41
+ let(:image_dsl_block) { Proc.new { } }
42
+ let(:dsl) {
43
+ child_block = image_dsl_block
44
+ Proc.new {
45
+ image('image-name', 'base-image-name', &child_block)
46
+ }
47
+ }
48
+
49
+ it 'processes images' do
50
+ image = instance_double(Phoebo::Config::Image)
51
+ expect(Phoebo::Config::Image).to receive(:new).with('image-name', 'base-image-name', image_dsl_block).and_return(image)
52
+
53
+ subject.dsl_eval(dsl)
54
+ expect(subject.images).to include(image)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,45 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ describe Phoebo::Docker::ImageBuilder 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
+ subject { described_class.new('./somedir') }
18
+
19
+ let(:image) {
20
+ actions = [
21
+ Phoebo::Config::ImageCommands::Add.action('.', '/app/'),
22
+ Phoebo::Config::ImageCommands::Run.action('composer', 'install', '--prefer-dist')
23
+ ]
24
+
25
+ image = instance_double(Phoebo::Config::Image)
26
+ allow(image).to receive(:name).and_return('image-name')
27
+ allow(image).to receive(:from).and_return('debian')
28
+ allow(image).to receive(:actions).and_return(actions)
29
+
30
+ image
31
+ }
32
+
33
+ let(:docker_image) {
34
+ image = instance_double(Docker::Image)
35
+ allow(image).to receive(:id).and_return('c90d655b99b2')
36
+ allow(image).to receive(:tag).and_return(image)
37
+ allow(image).to receive(:json).and_return({'VirtualSize' => 123, 'Size' => 123})
38
+ image
39
+ }
40
+
41
+ it 'processes commands' do
42
+ expect(Docker::Image).to receive(:build_from_tar).and_return(docker_image)
43
+ subject.build(image)
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Phoebo::Git do
4
+
5
+ subject { described_class }
6
+
7
+ context 'without credentials' do
8
+ let(:request) do
9
+ Phoebo::Request.new do
10
+ repo_url = 'ssh://somehost.tld/user/repo.git'
11
+ ssh_private_file = 'key'
12
+ ssh_public_file = 'key.pub'
13
+ end
14
+ end
15
+
16
+ it 'clones' do
17
+ expect(Rugged::Repository).to receive(:clone_at)
18
+ subject.clone(request, '/tmp/somepath')
19
+ end
20
+ end
21
+
22
+ context 'with SSH credentials' do
23
+ let(:request) do
24
+ Phoebo::Request.new do
25
+ repo_url = 'ssh://somehost.tld/user/repo.git'
26
+ end
27
+ end
28
+
29
+ it 'clones' do
30
+ expect(Rugged::Repository).to receive(:clone_at)
31
+ subject.clone(request, '/tmp/somepath')
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,37 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Phoebo::Ping do
4
+
5
+ subject { described_class }
6
+
7
+ let(:request) do
8
+ request = double(Phoebo::Request)
9
+ allow(request).to receive(:id).and_return('2e2996fe8420')
10
+ allow(request).to receive(:ping_url).and_return('http://domain.tld/api/notify')
11
+ request
12
+ end
13
+
14
+ let(:configs) do
15
+ tasks = [{
16
+ name: 'test',
17
+ image: 'someimage',
18
+ cmd: ['do_something', '--with', 'that']
19
+ }]
20
+
21
+ configs = [ config = double(Phoebo::Config) ]
22
+ allow(config).to receive(:tasks).and_return(tasks)
23
+ configs
24
+ end
25
+
26
+ let(:http_response) do
27
+ response = double
28
+ allow(response).to receive(:code).and_return(200)
29
+ response
30
+ end
31
+
32
+ it 'pings' do
33
+ allow(Net::HTTP).to receive(:start).and_return(http_response)
34
+ subject.send(request, configs)
35
+ end
36
+
37
+ end
@@ -0,0 +1,141 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Phoebo::Request do
4
+
5
+ subject { described_class.new }
6
+
7
+ context 'request validation' do
8
+ it 'defaults are valid' do
9
+ expect(subject.errors).to be_empty
10
+ end
11
+
12
+ # Unix like format is not accepted and should produce human-readable message
13
+ it 'returns error on invalid repo URL' do
14
+ subject.repo_url = '@host:a.git'
15
+ expect(subject.errors.select { |e| e.include?('Invalid repository URL.') }).not_to be_empty
16
+ end
17
+
18
+ # Non SSH repos not supported at the moment
19
+ it 'returns error when non SSH repo is requested' do
20
+ subject.repo_url = 'http://host/repo.git'
21
+ expect(subject.errors.select { |e| e.include?('Invalid repository URL.') }).not_to be_empty
22
+ end
23
+
24
+ it 'returns error on invalid ping URL' do
25
+ subject.ping_url = '@host:a.git'
26
+ expect(subject.errors.select { |e| e.include?('Invalid ping URL.') }).not_to be_empty
27
+ end
28
+
29
+ it 'valid repo url returns no errors' do
30
+ subject.repo_url = 'ssh://host/path/to/repo.git'
31
+ expect(subject.errors.select { |e| e.include?('Invalid repository URL.') }).to be_empty
32
+ end
33
+
34
+ it 'validates private SSH key if required' do
35
+ subject.ssh_public_file = 'secret.pub'
36
+ expect(subject.errors.select { |e| e.include?('private') && e.include?('SSH') }).not_to be_empty
37
+ end
38
+
39
+ it 'validates public SSH key if required' do
40
+ subject.ssh_private_file = 'secret'
41
+ expect(subject.errors.select { |e| e.include?('public') && e.include?('SSH') }).not_to be_empty
42
+ end
43
+
44
+ it 'no error with both SSH keys valid' do
45
+ subject.repo_url = 'ssh://host/path/to/repo.git'
46
+ subject.ssh_private_file = 'secret'
47
+ subject.ssh_public_file = 'secret.pub'
48
+ expect(subject.errors.select { |e| e.include?('SSH') }).to be_empty
49
+ end
50
+
51
+ it 'enforces none or all docker parameters' do
52
+ subject.docker_user = 'user'
53
+ expect(subject.errors.select { |e| e.include?('Docker') }.size).to be 2
54
+ subject.docker_password = 'password'
55
+ expect(subject.errors.select { |e| e.include?('Docker') }.size).to be 1
56
+ subject.docker_email = 'email'
57
+ expect(subject.errors.select { |e| e.include?('Docker') }).to be_empty
58
+ end
59
+
60
+ it 'raises exception' do
61
+ subject.repo_url = '@host:a.git'
62
+ expect { subject.validate }.to raise_error(Phoebo::InvalidRequestError)
63
+ end
64
+ end
65
+
66
+ context 'loading from hash' do
67
+ let(:valid_hash) do
68
+ {
69
+ ssh_private_file: 'secret',
70
+ ssh_public_file: 'secret.pub'
71
+ }
72
+ end
73
+
74
+ it 'raises error on unknown argument' do
75
+ expect { subject.load_from_hash!({ a: 13 }) }.to raise_error(Phoebo::InvalidRequestError)
76
+ end
77
+
78
+ it 'applies arguments' do
79
+ expect(subject.load_from_hash!(valid_hash).is_a?(described_class)).to eq true
80
+ expect(subject.ssh_private_file).to be valid_hash[:ssh_private_file]
81
+ expect(subject.ssh_public_file).to be valid_hash[:ssh_public_file]
82
+ end
83
+ end
84
+
85
+ let(:json) do
86
+ <<-EOS
87
+ {
88
+ "repo_url": "ssh://somehost.tld/user/repo.git"
89
+ }
90
+ EOS
91
+ end
92
+
93
+ let(:malformed_json) do
94
+ <<-EOS
95
+ {
96
+ "repo_url
97
+ }
98
+ EOS
99
+ end
100
+
101
+ context 'loading from JSON' do
102
+ it 'raises error on malformed data' do
103
+ expect { subject.load_from_json!(malformed_json) }.to raise_error(Phoebo::InvalidRequestError)
104
+ end
105
+
106
+ it 'applies arguments' do
107
+ subject.load_from_json!(json)
108
+ expect(subject.repo_url).to eql('ssh://somehost.tld/user/repo.git')
109
+ end
110
+ end
111
+
112
+ context 'loading from file' do
113
+ it 'raises error if file does not exist' do
114
+ allow(File).to receive(:exist?).and_return(false)
115
+ expect { subject.load_from_file!('somefile.json') }.to raise_error(Phoebo::IOError)
116
+ end
117
+
118
+ it 'loads data' do
119
+ allow(File).to receive(:exist?).and_return(true)
120
+ allow(IO).to receive(:read).and_return(json)
121
+ expect(subject).to receive(:load_from_json!).with(json)
122
+ subject.load_from_file!('somefile.json')
123
+ end
124
+ end
125
+
126
+ # TODO: cover all possible states
127
+ context 'loading from url' do
128
+ let(:http_response) do
129
+ response = double
130
+ allow(response).to receive(:code).and_return(200)
131
+ allow(response).to receive(:body).and_return(json)
132
+ response
133
+ end
134
+
135
+ it 'loads data' do
136
+ allow(Net::HTTP).to receive(:start).and_return(http_response)
137
+ expect(subject).to receive(:load_from_json!).with(json)
138
+ subject.load_from_url!('http://test')
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Phoebo::Worker do
4
+
5
+ let(:path) { './somepath' }
6
+ let(:request) {
7
+ image = instance_double(Phoebo::Request)
8
+ }
9
+
10
+ let(:config) {
11
+ config = instance_double(Phoebo::Config)
12
+ allow(config).to receive(:images).and_return([])
13
+ config
14
+ }
15
+
16
+ subject { described_class.new(request) }
17
+
18
+ context 'Pheobofile' do
19
+ before(:each) do
20
+ allow(File).to receive(:exists?).and_return(true)
21
+ allow(Phoebo::Config).to receive(:new_from_file).and_return(config)
22
+ end
23
+
24
+ it 'processes directories' do
25
+ subject.process(path)
26
+ end
27
+ end
28
+
29
+
30
+ end
data/spec/spec_helper.rb CHANGED
@@ -4,7 +4,10 @@ require 'bundler/setup'
4
4
  Bundler.setup
5
5
 
6
6
  require 'simplecov'
7
- SimpleCov.start
7
+
8
+ SimpleCov.start do
9
+ add_filter "/spec/"
10
+ end
8
11
 
9
12
  require 'phoebo'
10
13
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phoebo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Staněk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-11 00:00:00.000000000 Z
11
+ date: 2015-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -134,14 +134,25 @@ files:
134
134
  - lib/phoebo/docker/image_builder.rb
135
135
  - lib/phoebo/docker/image_pusher.rb
136
136
  - lib/phoebo/environment.rb
137
+ - lib/phoebo/git.rb
138
+ - lib/phoebo/ping.rb
139
+ - lib/phoebo/request.rb
137
140
  - lib/phoebo/util.rb
138
141
  - lib/phoebo/util/temp_file_manager.rb
139
142
  - lib/phoebo/version.rb
143
+ - lib/phoebo/worker.rb
140
144
  - phoebo.gemspec
141
145
  - spec/phoebo/application_spec.rb
146
+ - spec/phoebo/config/image_spec.rb
147
+ - spec/phoebo/config_spec.rb
142
148
  - spec/phoebo/console_spec.rb
149
+ - spec/phoebo/docker/image_builder_spec.rb
143
150
  - spec/phoebo/environment_spec.rb
151
+ - spec/phoebo/git_spec.rb
152
+ - spec/phoebo/ping_spec.rb
153
+ - spec/phoebo/request_spec.rb
144
154
  - spec/phoebo/util/temp_file_manager_spec.rb
155
+ - spec/phoebo/worker_spec.rb
145
156
  - spec/spec_helper.rb
146
157
  homepage: https://gitlab.fit.cvut.cz/phoebo/phoebo
147
158
  licenses:
@@ -169,7 +180,14 @@ specification_version: 4
169
180
  summary: CI worker for creating Docker images
170
181
  test_files:
171
182
  - spec/phoebo/application_spec.rb
183
+ - spec/phoebo/config/image_spec.rb
184
+ - spec/phoebo/config_spec.rb
172
185
  - spec/phoebo/console_spec.rb
186
+ - spec/phoebo/docker/image_builder_spec.rb
173
187
  - spec/phoebo/environment_spec.rb
188
+ - spec/phoebo/git_spec.rb
189
+ - spec/phoebo/ping_spec.rb
190
+ - spec/phoebo/request_spec.rb
174
191
  - spec/phoebo/util/temp_file_manager_spec.rb
192
+ - spec/phoebo/worker_spec.rb
175
193
  - spec/spec_helper.rb