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
@@ -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
|
+
|