construi 0.30.0 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+
@@ -1,3 +1,3 @@
1
1
  module Construi
2
- VERSION = "0.30.0"
2
+ VERSION = "0.31.0"
3
3
  end
@@ -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
+