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
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
require 'construi/container'
|
3
|
+
require 'construi/image'
|
4
|
+
require 'construi/target'
|
5
|
+
|
6
|
+
require 'construi/version'
|
7
|
+
|
8
|
+
require 'colorize'
|
9
|
+
require 'docker'
|
10
|
+
|
11
|
+
module Construi
|
12
|
+
|
13
|
+
class Runner
|
14
|
+
def initialize(config)
|
15
|
+
@config = config
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup_docker
|
19
|
+
docker_host = ENV['DOCKER_HOST']
|
20
|
+
Docker.url = docker_host if docker_host
|
21
|
+
|
22
|
+
puts "Docker url: #{Docker.url}"
|
23
|
+
|
24
|
+
Docker.validate_version!
|
25
|
+
Docker.options[:read_timeout] = 60
|
26
|
+
Docker.options[:chunk_size] = 8
|
27
|
+
end
|
28
|
+
|
29
|
+
def run(targets)
|
30
|
+
puts "Construi version: #{Construi::VERSION}"
|
31
|
+
|
32
|
+
setup_docker
|
33
|
+
|
34
|
+
puts "Current directory: #{Dir.pwd}"
|
35
|
+
|
36
|
+
targets.map {|t| Target.new t, @config.target(t) } .each(&:run)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Construi
|
4
|
+
|
5
|
+
class Target
|
6
|
+
attr_reader :name, :config
|
7
|
+
|
8
|
+
def initialize(name, config)
|
9
|
+
@name = name
|
10
|
+
@config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
def commands
|
14
|
+
@config.commands
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
puts "Running #{name}...".green
|
19
|
+
|
20
|
+
final_image = IntermediateImage.seed(initial_image).reduce(commands) do |image, command|
|
21
|
+
puts
|
22
|
+
puts " > #{command}".green
|
23
|
+
image.run(command, @config.env)
|
24
|
+
end
|
25
|
+
|
26
|
+
final_image.delete
|
27
|
+
end
|
28
|
+
|
29
|
+
def initial_image
|
30
|
+
return Image.from(@config)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
data/lib/construi/version.rb
CHANGED
@@ -0,0 +1,287 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'construi/config'
|
4
|
+
|
5
|
+
RSpec.describe Construi::Config do
|
6
|
+
let(:config_content) { '{}' }
|
7
|
+
|
8
|
+
subject(:config) { Construi::Config.load(config_content) }
|
9
|
+
|
10
|
+
describe '.load_file' do
|
11
|
+
let(:config_file) { Tempfile.new('config.load_file') }
|
12
|
+
let(:config_content) do
|
13
|
+
<<-YAML
|
14
|
+
image: test-image
|
15
|
+
YAML
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
config_file.write(config_content)
|
20
|
+
config_file.close
|
21
|
+
end
|
22
|
+
|
23
|
+
after do
|
24
|
+
config_file.unlink
|
25
|
+
end
|
26
|
+
|
27
|
+
subject { Construi::Config.load_file(config_file) }
|
28
|
+
|
29
|
+
it { is_expected.to_not be(nil) }
|
30
|
+
it { expect(subject.image).to eq('test-image') }
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#image' do
|
34
|
+
let(:config_content) do
|
35
|
+
<<-YAML
|
36
|
+
image: #{image}
|
37
|
+
YAML
|
38
|
+
end
|
39
|
+
|
40
|
+
subject { config.image }
|
41
|
+
|
42
|
+
%w{ test-image:latest lstephen/construi:latest }.each do |image_name|
|
43
|
+
context "when image is #{image_name}" do
|
44
|
+
let(:image) { image_name }
|
45
|
+
it { is_expected.to eq(image) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#build' do
|
51
|
+
let(:config_content) do
|
52
|
+
<<-YAML
|
53
|
+
build: #{build}
|
54
|
+
YAML
|
55
|
+
end
|
56
|
+
|
57
|
+
subject { config.build }
|
58
|
+
|
59
|
+
%w{ . construi/dev etc/docker/ }.each do |build|
|
60
|
+
context "when build is #{build}" do
|
61
|
+
let(:build) { build }
|
62
|
+
it { is_expected.to eq(build) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#env' do
|
68
|
+
subject { config.env }
|
69
|
+
|
70
|
+
context 'when no environment section' do
|
71
|
+
it { is_expected.to eq([]) }
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when explicitly set environment values' do
|
75
|
+
let(:config_content) do
|
76
|
+
<<-YAML
|
77
|
+
environment:
|
78
|
+
- VAR1=VALUE_1
|
79
|
+
- VAR2=VALUE_2
|
80
|
+
YAML
|
81
|
+
end
|
82
|
+
|
83
|
+
it { is_expected.to contain_exactly('VAR1=VALUE_1', 'VAR2=VALUE_2') }
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'when passing through environment varaibles' do
|
87
|
+
before do
|
88
|
+
ENV['VAR1'] = 'VALUE_1'
|
89
|
+
ENV['VAR2'] = 'VALUE_2'
|
90
|
+
end
|
91
|
+
|
92
|
+
let(:config_content) do
|
93
|
+
<<-YAML
|
94
|
+
environment:
|
95
|
+
- VAR1
|
96
|
+
- VAR2
|
97
|
+
YAML
|
98
|
+
end
|
99
|
+
|
100
|
+
it { is_expected.to contain_exactly('VAR1=VALUE_1', 'VAR2=VALUE_2') }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#target' do
|
105
|
+
let(:target) { 'build' }
|
106
|
+
|
107
|
+
subject { config.target target }
|
108
|
+
|
109
|
+
context 'when no target name' do
|
110
|
+
let(:target) { nil }
|
111
|
+
it { is_expected.to be(nil) }
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'when no targets configured' do
|
115
|
+
it { is_expected.to be(nil) }
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'when targets configured' do
|
119
|
+
let(:config_content) do
|
120
|
+
<<-YAML
|
121
|
+
targets:
|
122
|
+
build:
|
123
|
+
- cmd1
|
124
|
+
- cmd2
|
125
|
+
release:
|
126
|
+
- cmd3
|
127
|
+
- cmd4
|
128
|
+
YAML
|
129
|
+
end
|
130
|
+
|
131
|
+
it { is_expected.to_not be(nil) }
|
132
|
+
it { expect(subject.commands).to eq(['cmd1', 'cmd2']) }
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'when single command' do
|
136
|
+
let(:config_content) do
|
137
|
+
<<-YAML
|
138
|
+
targets:
|
139
|
+
build: cmd1
|
140
|
+
YAML
|
141
|
+
end
|
142
|
+
|
143
|
+
it { is_expected.to_not be(nil) }
|
144
|
+
it { expect(subject.commands).to eq(['cmd1']) }
|
145
|
+
end
|
146
|
+
context 'when using run command' do
|
147
|
+
let(:config_content) do
|
148
|
+
<<-YAML
|
149
|
+
targets:
|
150
|
+
build:
|
151
|
+
run:
|
152
|
+
- cmd1
|
153
|
+
- cmd2
|
154
|
+
YAML
|
155
|
+
end
|
156
|
+
|
157
|
+
it { is_expected.to_not be(nil) }
|
158
|
+
it { expect(subject.commands).to eq(['cmd1', 'cmd2']) }
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'when using single run command' do
|
162
|
+
let(:config_content) do
|
163
|
+
<<-YAML
|
164
|
+
targets:
|
165
|
+
build:
|
166
|
+
run: cmd1
|
167
|
+
YAML
|
168
|
+
end
|
169
|
+
|
170
|
+
it { is_expected.to_not be(nil) }
|
171
|
+
it { expect(subject.commands).to eq(['cmd1']) }
|
172
|
+
end
|
173
|
+
|
174
|
+
context 'when no image for target' do
|
175
|
+
let(:config_content) do
|
176
|
+
<<-YAML
|
177
|
+
image: global:image
|
178
|
+
targets:
|
179
|
+
build: cmd1
|
180
|
+
release:
|
181
|
+
image: release:image
|
182
|
+
run:
|
183
|
+
- cmd3
|
184
|
+
- cmd4
|
185
|
+
YAML
|
186
|
+
end
|
187
|
+
|
188
|
+
it { is_expected.to have_attributes(:image => 'global:image', :build => nil) }
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'when image for target' do
|
192
|
+
let(:config_content) do
|
193
|
+
<<-YAML
|
194
|
+
image: global:image
|
195
|
+
targets:
|
196
|
+
build:
|
197
|
+
image: build:image
|
198
|
+
run: cmd1
|
199
|
+
YAML
|
200
|
+
end
|
201
|
+
|
202
|
+
it { is_expected.to have_attributes(:image => 'build:image', :build => nil) }
|
203
|
+
end
|
204
|
+
|
205
|
+
context 'when build for target and image for global' do
|
206
|
+
let(:config_content) do
|
207
|
+
<<-YAML
|
208
|
+
image: global:image
|
209
|
+
targets:
|
210
|
+
build:
|
211
|
+
build: build/build
|
212
|
+
run: cmd1
|
213
|
+
YAML
|
214
|
+
end
|
215
|
+
|
216
|
+
it { is_expected.to have_attributes(:image => nil, :build => 'build/build') }
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe '#files' do
|
221
|
+
let(:host) { '/path/on/host' }
|
222
|
+
let(:container) { '/path/on/container' }
|
223
|
+
let(:permissions) { '0644' }
|
224
|
+
|
225
|
+
subject { config.target('build').files }
|
226
|
+
|
227
|
+
context 'when no files' do
|
228
|
+
let(:config_content) do
|
229
|
+
<<-YAML
|
230
|
+
targets:
|
231
|
+
build: cmd1
|
232
|
+
YAML
|
233
|
+
end
|
234
|
+
|
235
|
+
it { is_expected.to_not be(nil) }
|
236
|
+
it { is_expected.to eq([]) }
|
237
|
+
end
|
238
|
+
|
239
|
+
context 'when global files' do
|
240
|
+
let(:config_content) do
|
241
|
+
<<-YAML
|
242
|
+
files:
|
243
|
+
- #{host}:#{container}
|
244
|
+
- #{host}:#{container}:#{permissions}
|
245
|
+
targets:
|
246
|
+
build: cmd1
|
247
|
+
YAML
|
248
|
+
end
|
249
|
+
|
250
|
+
it { expect(subject.length).to eq(2) }
|
251
|
+
it do
|
252
|
+
expect(subject[0])
|
253
|
+
.to have_attributes :host => host, :container => container, :permissions => nil
|
254
|
+
end
|
255
|
+
it do
|
256
|
+
expect(subject[1])
|
257
|
+
.to have_attributes :host => host, :container => container, :permissions => permissions
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
context 'when target files' do
|
262
|
+
let(:config_content) do
|
263
|
+
<<-YAML
|
264
|
+
files:
|
265
|
+
- #{host}:#{container}
|
266
|
+
targets:
|
267
|
+
build:
|
268
|
+
run: cmd1
|
269
|
+
files:
|
270
|
+
- #{host}:#{container}:#{permissions}
|
271
|
+
YAML
|
272
|
+
end
|
273
|
+
|
274
|
+
it { expect(subject.length).to eq(2) }
|
275
|
+
it do
|
276
|
+
expect(subject[0])
|
277
|
+
.to have_attributes :host => host, :container => container, :permissions => nil
|
278
|
+
end
|
279
|
+
it do
|
280
|
+
expect(subject[1])
|
281
|
+
.to have_attributes :host => host, :container => container, :permissions => permissions
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
RSpec.describe Construi::Container do
|
6
|
+
let!(:docker_container_class) { class_spy(Docker::Container).as_stubbed_const }
|
7
|
+
let(:docker_container) do
|
8
|
+
instance_double(Docker::Container, :id => SecureRandom.hex(16)).as_null_object
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:image_id) { SecureRandom.hex(16) }
|
12
|
+
let(:image) { instance_double(Docker::Image, :id => image_id).as_null_object }
|
13
|
+
|
14
|
+
let(:container) { Construi::Container.wrap docker_container }
|
15
|
+
|
16
|
+
describe '#delete' do
|
17
|
+
subject! { container.delete }
|
18
|
+
|
19
|
+
it { expect(docker_container).to have_received(:delete) }
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#attach_stdout' do
|
23
|
+
subject(:attach_stdout) { -> { container.attach_stdout } }
|
24
|
+
|
25
|
+
context 'when attached succesfully' do
|
26
|
+
before do
|
27
|
+
allow(docker_container)
|
28
|
+
.to receive(:attach)
|
29
|
+
.and_yield('stream', 'msg1')
|
30
|
+
.and_yield('stream', 'msg2')
|
31
|
+
end
|
32
|
+
|
33
|
+
subject! { attach_stdout.call }
|
34
|
+
|
35
|
+
it { expect(docker_container).to have_received(:attach).with(:stream => true, :logs => true) }
|
36
|
+
it { expect($stdout.string).to include("msg1\nmsg2") }
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'when attach times out' do
|
40
|
+
before do
|
41
|
+
allow(docker_container)
|
42
|
+
.to receive(:attach)
|
43
|
+
.and_raise(Docker::Error::TimeoutError.new)
|
44
|
+
end
|
45
|
+
|
46
|
+
subject! { attach_stdout.call }
|
47
|
+
|
48
|
+
it { expect($stdout.string).to include('Failed to attach to stdout'.yellow) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#commit' do
|
53
|
+
before { allow(docker_container).to receive(:commit).and_return image }
|
54
|
+
|
55
|
+
subject! { container.commit }
|
56
|
+
|
57
|
+
it { expect(docker_container).to have_received(:commit) }
|
58
|
+
it { is_expected.to eq(Construi::Image.wrap(image)) }
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#run' do
|
62
|
+
before { allow(docker_container).to receive(:wait).and_return({'StatusCode' => status_code}) }
|
63
|
+
before { allow(docker_container).to receive(:commit).and_return image }
|
64
|
+
|
65
|
+
subject(:run) { -> { container.run } }
|
66
|
+
|
67
|
+
context 'when command succeeds' do
|
68
|
+
let(:status_code) { 0 }
|
69
|
+
|
70
|
+
subject! { run.call }
|
71
|
+
|
72
|
+
it { expect(docker_container).to have_received(:start) }
|
73
|
+
it { expect(docker_container).to have_received(:attach).with(:stream => true, :logs => true) }
|
74
|
+
it { expect(docker_container).to have_received(:wait) }
|
75
|
+
it { expect(docker_container).to have_received(:commit) }
|
76
|
+
it { is_expected.to eq(Construi::Image.wrap(image)) }
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'when command fails' do
|
80
|
+
let(:status_code) { 1 }
|
81
|
+
|
82
|
+
it { expect { run.call }.to raise_error Construi::Container::Error, /status code: 1/}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '.create' do
|
87
|
+
let(:cmd) { 'cmd1 p1 p2' }
|
88
|
+
let(:env) { ['ENV1=VAL1', 'ENV2=VAL2'] }
|
89
|
+
let(:pwd) { '/project/dir' }
|
90
|
+
|
91
|
+
before { allow(docker_container_class).to receive(:create).and_return docker_container }
|
92
|
+
before { allow(Dir).to receive(:pwd).and_return(pwd) }
|
93
|
+
|
94
|
+
subject! { Construi::Container::create(image, cmd, env) }
|
95
|
+
|
96
|
+
it do
|
97
|
+
expect(docker_container_class).to have_received(:create).with( {
|
98
|
+
'Cmd' => ['cmd1', 'p1', 'p2' ],
|
99
|
+
'Image' => image_id,
|
100
|
+
'Env' => env.to_json,
|
101
|
+
'Tty' => false,
|
102
|
+
'WorkingDir' => '/var/workspace',
|
103
|
+
'HostConfig' => { 'Binds' => ["#{pwd}:/var/workspace"] }
|
104
|
+
} )
|
105
|
+
end
|
106
|
+
it { is_expected.to eq(Construi::Container.wrap docker_container) }
|
107
|
+
end
|
108
|
+
|
109
|
+
describe '.run' do
|
110
|
+
let(:cmd) { 'cmd1 p1 p2' }
|
111
|
+
|
112
|
+
before { allow(docker_container_class).to receive(:create).and_return docker_container }
|
113
|
+
before { allow(docker_container).to receive(:wait).and_return({'StatusCode' => 0}) }
|
114
|
+
before { allow(docker_container).to receive(:commit).and_return image }
|
115
|
+
|
116
|
+
subject! { Construi::Container.run(image, cmd, []) }
|
117
|
+
|
118
|
+
it do
|
119
|
+
expect(docker_container_class).to have_received(:create).with(
|
120
|
+
hash_including( {
|
121
|
+
'Cmd' => ['cmd1', 'p1', 'p2'],
|
122
|
+
'Image' => image_id
|
123
|
+
}))
|
124
|
+
end
|
125
|
+
it { is_expected.to eq(Construi::Image.wrap image) }
|
126
|
+
it { expect(docker_container).to have_received(:start) }
|
127
|
+
it { expect(docker_container).to have_received(:commit) }
|
128
|
+
it { expect(docker_container).to have_received(:delete) }
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|