construi 0.30.0 → 0.31.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/README.md +144 -2
- data/construi.gemspec +1 -0
- data/construi.yml +1 -0
- data/lib/construi.rb +3 -36
- data/lib/construi/config.rb +89 -17
- data/lib/construi/container.rb +17 -8
- data/lib/construi/image.rb +84 -11
- data/lib/construi/runner.rb +41 -0
- data/lib/construi/target.rb +35 -0
- data/lib/construi/version.rb +1 -1
- data/spec/lib/construi/config_spec.rb +287 -0
- data/spec/lib/construi/container_spec.rb +132 -0
- data/spec/lib/construi/image_spec.rb +251 -0
- data/spec/lib/construi/runner_spec.rb +44 -0
- data/spec/lib/construi/target_spec.rb +14 -0
- data/spec/lib/construi_spec.rb +20 -0
- data/spec/lib/container_spec.rb +132 -0
- data/spec/spec_helper.rb +11 -3
- metadata +36 -6
- data/spec/lib/config_spec.rb +0 -27
data/.rspec
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,147 @@
|
|
1
1
|
# Construi
|
2
2
|
|
3
|
-
[![Gem](https://img.shields.io/gem/v/construi.svg?style=plastic)](https://rubygems.org/gems/construi)
|
4
|
-
[![
|
3
|
+
[![Gem](https://img.shields.io/gem/v/construi.svg?style=plastic)](https://rubygems.org/gems/construi)
|
4
|
+
[![Build Status](http://jenkins.mylonelybear.org/buildStatus/icon?job=construi-develop)](http://jenkins.mylonelybear.org/job/construi-develop/)
|
5
|
+
[![Code Climate](https://img.shields.io/codeclimate/github/lstephen/construi.svg?style=plastic)](https://codeclimate.com/github/lstephen/construi)
|
6
|
+
[![Coveralls](https://img.shields.io/coveralls/lstephen/construi/origin/develop.svg?style=plastic)](https://coveralls.io/r/lstephen/construi)
|
7
|
+
|
8
|
+
Construi allows you to use [Docker](http://www.docker.com) containers as your build environment.
|
9
|
+
This allows a consistent and recreatable build environment on any machine running Construi
|
10
|
+
and Docker.
|
11
|
+
|
12
|
+
This is useful for ensuring consistency between development machines.
|
13
|
+
It is also useful in a CI environment where you may need to build projects from many different
|
14
|
+
languages and/or versions (e.g., Java 6, Java 8, Ruby).
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Construi requires [Ruby](http://www.ruby-lang.org) version 1.9 or higher.
|
19
|
+
It can be installed as a Gem.
|
20
|
+
|
21
|
+
```
|
22
|
+
> gem install construi
|
23
|
+
```
|
24
|
+
|
25
|
+
## Running
|
26
|
+
|
27
|
+
Construi requires that a `construi.yml` file be present in the root directory of your project.
|
28
|
+
Targets can then be run by specifying them on the command line. For example:
|
29
|
+
|
30
|
+
```
|
31
|
+
> construi build
|
32
|
+
> construi build install
|
33
|
+
```
|
34
|
+
|
35
|
+
Construi will create a Docker container with the project directory as a volume.
|
36
|
+
It will then run the commands configured for the given targets.
|
37
|
+
|
38
|
+
## The Construi File
|
39
|
+
|
40
|
+
As a minimal `construi.yml` requires an image and a target to be configured.
|
41
|
+
For example a simple configuration for a Java 8 project built with Maven could be:
|
42
|
+
|
43
|
+
```
|
44
|
+
image: maven:3-jdk-8
|
45
|
+
|
46
|
+
targets:
|
47
|
+
install: mvn install
|
48
|
+
```
|
49
|
+
|
50
|
+
Construi is built using itself, so it's
|
51
|
+
[`construi.yml`](https://github.com/lstephen/construi/blob/develop/construi.yml)
|
52
|
+
can be used as an example also.
|
53
|
+
|
54
|
+
### Image
|
55
|
+
|
56
|
+
Specifies an image to be pulled that will be used as the build environment.
|
57
|
+
It can also be given on a per target basis.
|
58
|
+
|
59
|
+
```
|
60
|
+
image: maven:3-jdk-7
|
61
|
+
|
62
|
+
targets:
|
63
|
+
install: mvn install
|
64
|
+
|
65
|
+
test-java-8:
|
66
|
+
image: maven:3-jdk-8
|
67
|
+
run: mvn verify
|
68
|
+
```
|
69
|
+
|
70
|
+
### Build
|
71
|
+
|
72
|
+
Specifies a directory containing a `Dockerfile`.
|
73
|
+
Construi will build a Docker container based on that `Dockerfile` and use it as the build
|
74
|
+
environment.
|
75
|
+
Can be used as an alternative to providing an image.
|
76
|
+
Can also be given on a per target basis.
|
77
|
+
|
78
|
+
```
|
79
|
+
build: etc/build_environment
|
80
|
+
|
81
|
+
targets:
|
82
|
+
build:
|
83
|
+
- mvn install
|
84
|
+
- /usr/local/bin/custom_installed_command.sh
|
85
|
+
```
|
86
|
+
|
87
|
+
### Environment
|
88
|
+
|
89
|
+
Declares environment variables that will be passed through or set in the build environment.
|
90
|
+
If no value is provided then the value from the host environment will be used.
|
91
|
+
In this example `NEXUS_SERVER_URL` will be set as provided, while `NEXUS_USERNAME` and
|
92
|
+
`NEXUS_PASSWORD` will be retrieved from the host.
|
93
|
+
|
94
|
+
```
|
95
|
+
image: maven:3-jdk-7
|
96
|
+
|
97
|
+
environment:
|
98
|
+
- NEXUS_SERVER_URL=http://nexus.example.com
|
99
|
+
- NEXUS_USERNAME
|
100
|
+
- NEXUS_PASSWORD
|
101
|
+
targets:
|
102
|
+
build: mvn install
|
103
|
+
```
|
104
|
+
|
105
|
+
### Files
|
106
|
+
|
107
|
+
Declares files to be copied into the build environment before the build is run.
|
108
|
+
Also allows setting of permissions.
|
109
|
+
Can be used on a per target basis.
|
110
|
+
|
111
|
+
```
|
112
|
+
image: maven:3-jdk-7
|
113
|
+
|
114
|
+
files:
|
115
|
+
- etc/maven-settings.xml:/home/root/.m2/settings.xml
|
116
|
+
|
117
|
+
targets:
|
118
|
+
deploy:
|
119
|
+
files:
|
120
|
+
- $GIT_SSH_KEY:/home/root/.ssh/id_rsa:0600
|
121
|
+
run: scripts/construi/deploy.sh
|
122
|
+
```
|
123
|
+
|
124
|
+
### Targets
|
125
|
+
|
126
|
+
Any number of targets can be specified.
|
127
|
+
Each must specify at least one command.
|
128
|
+
If additional configuration is required for a target then the commands should be provided
|
129
|
+
under the `run` key.
|
130
|
+
If more than one command is required then a YAML list should be used.
|
131
|
+
|
132
|
+
```
|
133
|
+
image: maven:3-jdk-7
|
134
|
+
|
135
|
+
targets:
|
136
|
+
build: mvn install
|
137
|
+
|
138
|
+
test-java-8:
|
139
|
+
image: maven:3-jdk-8
|
140
|
+
run: mvn verify
|
141
|
+
|
142
|
+
deploy:
|
143
|
+
- mvn deploy
|
144
|
+
- curl http://ci.example.com/deploy/trigger
|
145
|
+
```
|
146
|
+
|
5
147
|
|
data/construi.gemspec
CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_dependency 'docker-api', '~> 1.20'
|
22
|
+
spec.add_dependency 'colorize', '~> 0.7.5'
|
22
23
|
|
23
24
|
spec.add_development_dependency 'bundler', '~> 1.7'
|
24
25
|
spec.add_development_dependency 'codeclimate-test-reporter'
|
data/construi.yml
CHANGED
data/lib/construi.rb
CHANGED
@@ -1,46 +1,13 @@
|
|
1
1
|
require 'construi/config'
|
2
|
-
require 'construi/
|
3
|
-
require 'construi/image'
|
4
|
-
require 'construi/version'
|
2
|
+
require 'construi/runner'
|
5
3
|
|
6
|
-
require 'docker'
|
7
4
|
require 'yaml'
|
8
5
|
|
9
6
|
module Construi
|
10
|
-
|
11
|
-
class Runner
|
12
|
-
def initialize(config)
|
13
|
-
@config = config
|
14
|
-
end
|
15
|
-
|
16
|
-
def run(targets)
|
17
|
-
puts "Construi version: #{Construi::VERSION}"
|
18
|
-
|
19
|
-
docker_host = ENV['DOCKER_HOST']
|
20
|
-
Docker.url = docker_host unless docker_host.nil?
|
21
|
-
|
22
|
-
puts "Docker url: #{Docker.url}"
|
23
|
-
puts "Current directory: #{Dir.pwd}"
|
24
|
-
|
25
|
-
Docker.validate_version!
|
26
|
-
Docker.options[:read_timeout] = 60
|
27
|
-
Docker.options[:chunk_size] = 8
|
28
|
-
|
29
|
-
initial_image = Image.create(@config.image) { |s| puts s }
|
30
|
-
|
31
|
-
commands = targets.map { |t| @config.target(t).commands }.flatten
|
32
|
-
|
33
|
-
final_image = commands.reduce(IntermediateImage.seed(initial_image)) do |image, command|
|
34
|
-
puts " > #{command}"
|
35
|
-
image.run(command, @config.env)
|
36
|
-
end
|
37
|
-
|
38
|
-
final_image.delete
|
39
|
-
end
|
40
|
-
end
|
7
|
+
String.disable_colorization = false
|
41
8
|
|
42
9
|
def self.run(targets)
|
43
10
|
Runner.new(Config.load_file('construi.yml')).run(targets)
|
44
11
|
end
|
45
|
-
|
46
12
|
end
|
13
|
+
|
data/lib/construi/config.rb
CHANGED
@@ -1,31 +1,82 @@
|
|
1
1
|
|
2
|
-
module Construi
|
2
|
+
module Construi::Config
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
module Image
|
5
|
+
def image
|
6
|
+
image_configured :image
|
7
|
+
end
|
6
8
|
|
7
|
-
|
9
|
+
def build
|
10
|
+
image_configured :build
|
11
|
+
end
|
8
12
|
|
9
|
-
def
|
10
|
-
|
13
|
+
def image_configured?
|
14
|
+
yaml.is_a?(Hash) && (yaml.has_key?('build') || yaml.has_key?('image'))
|
15
|
+
end
|
16
|
+
|
17
|
+
def image_configured(what)
|
18
|
+
image_configured? ? yaml[what.to_s] : with_parent(&what)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Files
|
23
|
+
|
24
|
+
class File
|
25
|
+
attr_reader :container, :permissions
|
26
|
+
|
27
|
+
def initialize(host, container, permissions)
|
28
|
+
@host = host
|
29
|
+
@container = container
|
30
|
+
@permissions = permissions
|
31
|
+
end
|
32
|
+
|
33
|
+
def host
|
34
|
+
@host.gsub(/\$(\w+)/) { ENV[$1] }
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.parse(str)
|
38
|
+
split = str.split(':')
|
39
|
+
File.new split[0], split[1], split[2]
|
40
|
+
end
|
11
41
|
end
|
12
42
|
|
13
|
-
def
|
14
|
-
|
43
|
+
def files_configured?
|
44
|
+
yaml.is_a? Hash and yaml.has_key? 'files'
|
15
45
|
end
|
16
46
|
|
17
|
-
def
|
18
|
-
|
47
|
+
def files
|
48
|
+
fs = files_configured? ? yaml['files'].map { |str| File.parse(str) } : []
|
49
|
+
|
50
|
+
with_parent([], &:files).concat fs
|
19
51
|
end
|
52
|
+
end
|
20
53
|
|
21
|
-
|
22
|
-
|
54
|
+
module Environment
|
55
|
+
include Image
|
56
|
+
include Files
|
57
|
+
|
58
|
+
def parent
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def with_parent(or_else = nil)
|
63
|
+
parent ? yield(parent) : or_else
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Global
|
68
|
+
include Environment
|
69
|
+
|
70
|
+
attr_reader :yaml
|
71
|
+
|
72
|
+
def initialize(yaml)
|
73
|
+
@yaml = yaml
|
23
74
|
end
|
24
75
|
|
25
76
|
def env
|
26
|
-
return [] if
|
77
|
+
return [] if yaml['environment'].nil?
|
27
78
|
|
28
|
-
|
79
|
+
yaml['environment'].reduce([]) do |acc, e|
|
29
80
|
key = e.partition('=').first
|
30
81
|
value = e.partition('=').last
|
31
82
|
|
@@ -37,18 +88,39 @@ module Construi
|
|
37
88
|
end
|
38
89
|
|
39
90
|
def target(target)
|
40
|
-
|
91
|
+
targets = yaml['targets']
|
92
|
+
|
93
|
+
return nil if targets.nil?
|
94
|
+
|
95
|
+
return Target.new yaml['targets'][target], self
|
41
96
|
end
|
42
97
|
end
|
43
98
|
|
44
99
|
class Target
|
45
|
-
|
100
|
+
include Environment
|
101
|
+
|
102
|
+
attr_reader :yaml, :parent
|
103
|
+
|
104
|
+
def initialize(yaml, parent)
|
46
105
|
@yaml = yaml
|
106
|
+
@parent = parent
|
47
107
|
end
|
48
108
|
|
49
109
|
def commands
|
50
|
-
@yaml
|
110
|
+
Array(@yaml.is_a?(Hash) ? @yaml['run'] : @yaml)
|
51
111
|
end
|
112
|
+
|
113
|
+
def env
|
114
|
+
parent.env
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.load(content)
|
119
|
+
Global.new YAML.load(content)
|
52
120
|
end
|
53
121
|
|
122
|
+
def self.load_file(path)
|
123
|
+
Global.new YAML.load_file(path)
|
124
|
+
end
|
54
125
|
end
|
126
|
+
|
data/lib/construi/container.rb
CHANGED
@@ -9,14 +9,20 @@ module Construi
|
|
9
9
|
@container = container
|
10
10
|
end
|
11
11
|
|
12
|
+
def id
|
13
|
+
@container.id
|
14
|
+
end
|
15
|
+
|
12
16
|
def delete
|
13
17
|
@container.delete
|
14
18
|
end
|
15
19
|
|
16
20
|
def attach_stdout
|
17
21
|
@container.attach(:stream => true, :logs => true) { |s, c| puts c; $stdout.flush }
|
22
|
+
true
|
18
23
|
rescue Docker::Error::TimeoutError
|
19
|
-
puts 'Failed to
|
24
|
+
puts 'Failed to attach to stdout'.yellow
|
25
|
+
false
|
20
26
|
end
|
21
27
|
|
22
28
|
def commit
|
@@ -25,14 +31,20 @@ module Construi
|
|
25
31
|
|
26
32
|
def run
|
27
33
|
@container.start
|
28
|
-
attach_stdout
|
34
|
+
attached = attach_stdout
|
29
35
|
status_code = @container.wait['StatusCode']
|
30
36
|
|
31
|
-
|
37
|
+
puts @container.logs(:stdout => true) unless attached
|
38
|
+
|
39
|
+
raise Error, "Cmd returned status code: #{status_code}" unless status_code == 0
|
32
40
|
|
33
41
|
commit
|
34
42
|
end
|
35
43
|
|
44
|
+
def ==(other)
|
45
|
+
other.is_a? Container and id == other.id
|
46
|
+
end
|
47
|
+
|
36
48
|
def self.create(image, cmd, env)
|
37
49
|
wrap Docker::Container.create(
|
38
50
|
'Cmd' => cmd.split,
|
@@ -58,12 +70,9 @@ module Construi
|
|
58
70
|
use(image, cmd, env, &:run)
|
59
71
|
end
|
60
72
|
|
61
|
-
|
62
|
-
|
63
|
-
class ContainerError < StandardError
|
64
|
-
end
|
73
|
+
class Error < StandardError
|
74
|
+
end
|
65
75
|
|
66
|
-
class RunError < ContainerError
|
67
76
|
end
|
68
77
|
|
69
78
|
end
|
data/lib/construi/image.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'construi/container'
|
2
2
|
|
3
|
+
require 'colorize'
|
4
|
+
require 'docker'
|
5
|
+
|
3
6
|
module Construi
|
4
7
|
|
5
8
|
class Image
|
@@ -17,40 +20,102 @@ module Construi
|
|
17
20
|
@image.delete
|
18
21
|
end
|
19
22
|
|
23
|
+
def docker_image
|
24
|
+
@image
|
25
|
+
end
|
26
|
+
|
20
27
|
def tagged?
|
21
28
|
@image.info['RepoTags'] != '<none>:<none>'
|
22
29
|
end
|
23
30
|
|
24
|
-
def
|
31
|
+
def insert_local(file)
|
32
|
+
puts "\nCopying #{file.host} to #{file.container}...".green
|
33
|
+
|
34
|
+
img = IntermediateImage.seed(self)
|
35
|
+
|
36
|
+
img.map do |i|
|
37
|
+
Image.wrap i.docker_image
|
38
|
+
.insert_local 'localPath' => file.host, 'outputPath' => file.container
|
39
|
+
end
|
40
|
+
|
41
|
+
img.map { |i| i.chmod file.container, file.permissions } if file.permissions
|
42
|
+
|
43
|
+
img.run "ls -l #{file.container}"
|
44
|
+
|
45
|
+
img.image
|
46
|
+
end
|
47
|
+
|
48
|
+
def insert_locals(files)
|
49
|
+
IntermediateImage.seed(self).reduce(files) { |i, f| i.insert_local f }.image
|
50
|
+
end
|
51
|
+
|
52
|
+
def chmod(file, permissions)
|
53
|
+
chmod = "chmod -R #{permissions} #{file}"
|
54
|
+
|
55
|
+
puts " > #{chmod}"
|
56
|
+
run chmod
|
57
|
+
end
|
58
|
+
|
59
|
+
def run(cmd, env = [])
|
25
60
|
Container.run(self, cmd, env)
|
26
61
|
end
|
27
62
|
|
63
|
+
def ==(other)
|
64
|
+
other.is_a? Image and id == other.id
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.from(config)
|
68
|
+
image = create(config.image) unless config.image.nil?
|
69
|
+
image = build(config.build) unless config.build.nil?
|
70
|
+
|
71
|
+
raise Error, "Invalid image configuration: #{config}" unless image
|
72
|
+
|
73
|
+
image.insert_locals config.files
|
74
|
+
end
|
75
|
+
|
28
76
|
def self.create(image)
|
29
|
-
|
77
|
+
puts
|
78
|
+
puts "Creating image: '#{image}'...".green
|
79
|
+
wrap Docker::Image.create('fromImage' => image) { |s|
|
80
|
+
status = JSON.parse(s)
|
81
|
+
|
82
|
+
id = status['id']
|
83
|
+
progress = status['progressDetail']
|
84
|
+
|
85
|
+
if progress.nil? or progress.empty?
|
86
|
+
print "#{id}: " unless id.nil?
|
87
|
+
puts "#{status['status']}"
|
88
|
+
end
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.build(build)
|
93
|
+
puts
|
94
|
+
puts "Building image: '#{build}'...".green
|
95
|
+
wrap Docker::Image.build_from_dir(build, :rm => 0) { |s|
|
96
|
+
puts JSON.parse(s)['stream']
|
97
|
+
}
|
30
98
|
end
|
31
99
|
|
32
100
|
def self.wrap(image)
|
33
101
|
new image
|
34
102
|
end
|
35
103
|
|
36
|
-
|
37
|
-
begin
|
38
|
-
i = create(image)
|
39
|
-
yield i
|
40
|
-
ensure
|
41
|
-
i.delete unless i.tagged?
|
42
|
-
end
|
104
|
+
class Error < StandardError
|
43
105
|
end
|
44
106
|
end
|
45
107
|
|
46
108
|
class IntermediateImage
|
47
109
|
private_class_method :new
|
48
110
|
|
111
|
+
attr_reader :image
|
112
|
+
|
49
113
|
def initialize(image)
|
50
114
|
@image = image
|
115
|
+
@first = true
|
51
116
|
end
|
52
117
|
|
53
|
-
def run(cmd, env)
|
118
|
+
def run(cmd, env = [])
|
54
119
|
map { |i| i.run(cmd, env) }
|
55
120
|
end
|
56
121
|
|
@@ -58,9 +123,17 @@ module Construi
|
|
58
123
|
update(yield @image)
|
59
124
|
end
|
60
125
|
|
126
|
+
def reduce(iter)
|
127
|
+
iter.reduce(self) do |intermediate_image, item|
|
128
|
+
intermediate_image.map { |i| yield i, item }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
61
132
|
def update(image)
|
62
|
-
|
133
|
+
delete unless @first
|
134
|
+
@first = false
|
63
135
|
@image = image
|
136
|
+
self
|
64
137
|
end
|
65
138
|
|
66
139
|
def delete
|