construi 0.30.0 → 0.31.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.
- 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
|
-
[](https://rubygems.org/gems/construi)
|
4
|
-
[](https://rubygems.org/gems/construi)
|
4
|
+
[](http://jenkins.mylonelybear.org/job/construi-develop/)
|
5
|
+
[](https://codeclimate.com/github/lstephen/construi)
|
6
|
+
[](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
|