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