dockistrano 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,536 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dockistrano::Service do
4
+
5
+ subject { described_class.new(config, "default") }
6
+ let(:config) {{
7
+ "default" => {
8
+ "image_name" => "image",
9
+ "tag" => "tag",
10
+ "registry" => "index.provide.dev:5000"
11
+ }
12
+ }}
13
+
14
+ context ".factory" do
15
+ it "initializes a service for the current directory" do
16
+ service = described_class.factory("spec/fixtures/project_1")
17
+ expect(service.config).to eq({
18
+ "registry" => "my.registry.com:5000",
19
+ "image_name" => "project_1",
20
+ "dependencies" => {
21
+ "redis" => "git@github.com:username/redis.git",
22
+ "memcached" => "git@github.com:username/memcached.git"
23
+ },
24
+ "environment" => {}
25
+ })
26
+ end
27
+ end
28
+
29
+ context "#config=" do
30
+ subject { described_class.new({ "default" => {} }, "default") }
31
+
32
+ it "uses the name of the git repository as the name of the image" do
33
+ expect(Dockistrano::Git).to receive(:repository_name).and_return("repo_name")
34
+ subject.config = {}
35
+ expect(subject.image_name).to eq("repo_name")
36
+ end
37
+
38
+ it "uses the git branch as the tag" do
39
+ expect(Dockistrano::Git).to receive(:branch).and_return("develop")
40
+ subject.config = {}
41
+ expect(subject.tag).to eq("develop")
42
+ end
43
+ end
44
+
45
+ context "#environment=" do
46
+ let(:config) {{
47
+ "default" => { "test_command" => "default" },
48
+ "test" => { "test_command" => "test" }
49
+ }}
50
+
51
+ it "sets the config to the new environment" do
52
+ subject.environment = "test"
53
+ expect(subject.config).to eq({ "test_command" => "test", "environment" => {} })
54
+ end
55
+
56
+ it "raises an error when the environment is not found" do
57
+ expect {
58
+ subject.environment = "foobar"
59
+ }.to raise_error(Dockistrano::Service::EnvironmentNotFoundInConfiguration)
60
+ end
61
+ end
62
+
63
+ context "#registry_instance" do
64
+ it "returns an instance of the registry" do
65
+ expect(Dockistrano::Registry).to receive(:new).and_return(instance = double)
66
+ expect(subject.registry_instance).to eq(instance)
67
+ end
68
+ end
69
+
70
+ context "#image_id" do
71
+ it "returns the image id of the current container" do
72
+ expect(Dockistrano::Docker).to receive(:image_id).with(subject.full_image_name).and_return(123456789)
73
+ expect(subject.image_id).to eq(123456789)
74
+ end
75
+
76
+ it "returns nil when the image is not found" do
77
+ expect(Dockistrano::Docker).to receive(:image_id).with(subject.full_image_name).and_raise(Dockistrano::Docker::ImageNotFound)
78
+ expect(subject.image_id).to be_nil
79
+ end
80
+ end
81
+
82
+ context "#full_image_name" do
83
+ let(:config) {{
84
+ "default" => {
85
+ "image_name" => "image",
86
+ "tag" => "tag",
87
+ "registry" => "index.provider.dev:5000"
88
+ }
89
+ }}
90
+
91
+ it "returns the name with registry and tag" do
92
+ expect(subject.full_image_name).to eq("index.provider.dev:5000/image:tag")
93
+ end
94
+ end
95
+
96
+ context "#build" do
97
+ it "builds the container" do
98
+ allow(subject).to receive(:image_id).and_return(123, 456)
99
+ allow(subject).to receive(:full_image_name).and_return("full_image_name")
100
+ expect(Dockistrano::Docker).to receive(:build).with("full_image_name")
101
+ expect(subject.build).to be_true
102
+ end
103
+
104
+ it "returns false when the build didn't change the image id" do
105
+ allow(subject).to receive(:image_id).and_return(123, 123)
106
+ allow(subject).to receive(:full_image_name).and_return("full_image_name")
107
+ expect(Dockistrano::Docker).to receive(:build).with("full_image_name")
108
+ expect(subject.build).to be_false
109
+ end
110
+ end
111
+
112
+ context "#test" do
113
+ before do
114
+ allow(subject).to receive(:full_image_name).and_return("full_image_name")
115
+ allow(subject).to receive(:test_command).and_return("rake test")
116
+ allow(subject).to receive(:checked_environment_variables).and_return(environment_variables)
117
+ allow(subject).to receive(:volumes).and_return(volumes)
118
+ end
119
+
120
+ let(:environment_variables) { double }
121
+ let(:volumes) { double }
122
+
123
+ it "tests the container" do
124
+ expect(subject).to receive(:ensure_backing_services)
125
+ expect(Dockistrano::Docker).to receive(:exec).with("full_image_name", command: "rake test", e: environment_variables, v: volumes).and_return(true)
126
+ expect(subject.test).to be_true
127
+ end
128
+
129
+ it "returns true when no test command is provided" do
130
+ allow(subject).to receive(:test_command).and_return("")
131
+ expect(subject.test).to be_true
132
+ end
133
+ end
134
+
135
+ context "#ensure_backing_services" do
136
+ before do
137
+ allow(subject).to receive(:backing_services).and_return({ "service_1" => service_1, "service_2" => service_2 })
138
+ end
139
+ let(:service_1) { double }
140
+ let(:service_2) { double }
141
+
142
+ it "starts services that are not running" do
143
+ expect(service_1).to receive(:running?).and_return(true)
144
+ expect(service_2).to receive(:running?).and_return(false)
145
+
146
+ expect(service_2).to receive(:start)
147
+ subject.ensure_backing_services
148
+ end
149
+
150
+ it "doesn't start services that are already running" do
151
+ expect(service_1).to receive(:running?).and_return(true)
152
+ expect(service_2).to receive(:running?).and_return(true)
153
+
154
+ expect(service_1).to_not receive(:start)
155
+ expect(service_2).to_not receive(:start)
156
+ subject.ensure_backing_services
157
+ end
158
+ end
159
+
160
+ context "#stop" do
161
+ let(:hipache) { double }
162
+
163
+ before do
164
+ allow(Dockistrano::Docker).to receive(:stop_all_containers_from_image)
165
+ allow(Dockistrano::Hipache).to receive(:new).and_return(hipache)
166
+ end
167
+
168
+ it "stops the container" do
169
+ expect(Dockistrano::Docker).to receive(:stop_all_containers_from_image).with(subject.full_image_name)
170
+ subject.stop
171
+ end
172
+
173
+ it "unregisters a host from Hipache" do
174
+ allow(subject).to receive(:host).and_return("hostname.dev")
175
+ expect(subject).to receive(:ip_address).and_return("33.33.33.33")
176
+ expect(subject).to receive(:port).and_return("3000")
177
+ expect(hipache).to receive(:unregister).with(subject.image_name, "hostname.dev", "33.33.33.33", "3000")
178
+ subject.stop
179
+ end
180
+
181
+ it "unregisters multiple hosts from Hipache" do
182
+ allow(subject).to receive(:host).and_return({ "hostname.dev" => "8000" })
183
+ expect(subject).to receive(:ip_address).and_return("33.33.33.33")
184
+ expect(hipache).to receive(:unregister).with(subject.image_name, "hostname.dev", "33.33.33.33", "8000")
185
+ subject.stop
186
+ end
187
+ end
188
+
189
+ context "#running?" do
190
+ before do
191
+ allow(subject).to receive(:full_image_name).and_return("image_name:tag")
192
+ end
193
+
194
+ it "returns true when the service is running" do
195
+ expect(Dockistrano::Docker).to receive(:running_container_id).with("image_name:tag").and_return("423c138040f1")
196
+ expect(subject.running?).to eq("423c138040f1")
197
+ end
198
+
199
+ it "returns false when the service is not running" do
200
+ expect(Dockistrano::Docker).to receive(:running_container_id).with("image_name:tag").and_return(nil)
201
+ expect(subject.running?).to eq(nil)
202
+ end
203
+ end
204
+
205
+ context "#pull_backing_services" do
206
+ before do
207
+ allow(subject).to receive(:backing_services).and_return({ "service_1" => service_1, "service_2" => service_2 })
208
+ end
209
+ let(:service_1) { double }
210
+ let(:service_2) { double }
211
+
212
+ it "pulls each backing service" do
213
+ expect(service_1).to receive(:pull)
214
+ expect(service_2).to receive(:pull)
215
+ subject.pull_backing_services
216
+ end
217
+ end
218
+
219
+ context "#pull" do
220
+ before do
221
+ allow(subject).to receive(:registry).and_return("registry.net:5000")
222
+ allow(subject).to receive(:image_name).and_return("image_name")
223
+ allow(subject).to receive(:tag_with_fallback).and_return("latest")
224
+ end
225
+
226
+ it "pulls the service's container" do
227
+ expect(Dockistrano::Docker).to receive(:pull).with("registry.net:5000/image_name", "latest")
228
+ subject.pull
229
+ end
230
+ end
231
+
232
+ context "#push" do
233
+ it "pushes the current container to the registry" do
234
+ allow(subject).to receive(:registry).and_return("registry.net:5000")
235
+ allow(subject).to receive(:image_name).and_return("image_name")
236
+ allow(subject).to receive(:tag).and_return("tag")
237
+ expect(Dockistrano::Docker).to receive(:push).with("registry.net:5000/image_name", "tag")
238
+ subject.push
239
+ end
240
+ end
241
+
242
+ context "#start" do
243
+ let(:environment) { double }
244
+ let(:volumes) { double }
245
+ let(:ports) { double }
246
+ let(:hipache) { double }
247
+
248
+ before do
249
+ allow(Dockistrano::Hipache).to receive(:new).and_return(hipache)
250
+ allow(subject).to receive(:ensure_backing_services)
251
+ allow(subject).to receive(:create_data_directories)
252
+ allow(subject).to receive(:checked_environment_variables).and_return(environment)
253
+ allow(subject).to receive(:volumes).and_return(volumes)
254
+ allow(subject).to receive(:ports).and_return(ports)
255
+ allow(Dockistrano::Docker).to receive(:run)
256
+ end
257
+
258
+ it "ensures backing services are running" do
259
+ expect(subject).to receive(:ensure_backing_services)
260
+ subject.start
261
+ end
262
+
263
+ it "creates data directories" do
264
+ expect(subject).to receive(:create_data_directories)
265
+ subject.start
266
+ end
267
+
268
+ it "starts additional container when additional commands are configured" do
269
+ allow(subject).to receive(:additional_commands).and_return({ "worker" => "sidekiq start" })
270
+ expect(Dockistrano::Docker).to receive(:run).with(subject.full_image_name, e: environment, v: volumes, p: ports, d: true, command: "sidekiq start")
271
+ subject.start
272
+ end
273
+
274
+ it "starts the container with the default command, providing env variables and volumes" do
275
+ expect(Dockistrano::Docker).to receive(:run).with(subject.full_image_name, e: environment, v: volumes, p: ports, d: true)
276
+ subject.start
277
+ end
278
+
279
+ it "registers a host with Hipache" do
280
+ allow(subject).to receive(:host).and_return("hostname.dev")
281
+ expect(subject).to receive(:ip_address).and_return("33.33.33.33")
282
+ expect(subject).to receive(:port).and_return("3000")
283
+ expect(hipache).to receive(:register).with(subject.image_name, "hostname.dev", "33.33.33.33", "3000")
284
+ subject.start
285
+ end
286
+
287
+ it "registers multiple hosts with Hipache" do
288
+ allow(subject).to receive(:host).and_return({ "hostname.dev" => "8000" })
289
+ expect(subject).to receive(:ip_address).and_return("33.33.33.33")
290
+ expect(hipache).to receive(:register).with(subject.image_name, "hostname.dev", "33.33.33.33", "8000")
291
+ subject.start
292
+ end
293
+ end
294
+
295
+ context "#run" do
296
+ it "runs the command inside the container" do
297
+ allow(subject).to receive(:environment_variables).and_return(environment = double)
298
+ allow(subject).to receive(:volumes).and_return(volumes = double)
299
+ allow(subject).to receive(:ports).and_return(ports = double)
300
+ expect(Dockistrano::Docker).to receive(:run).with(subject.full_image_name, e: environment, v: volumes, p: ports, command: "foobar")
301
+ subject.run("foobar")
302
+ end
303
+ end
304
+
305
+ context "#exec" do
306
+ it "executes the command inside the container" do
307
+ allow(subject).to receive(:environment_variables).and_return(environment = double)
308
+ allow(subject).to receive(:volumes).and_return(volumes = double)
309
+ allow(subject).to receive(:ports).and_return(ports = double)
310
+ expect(subject).to receive(:create_data_directories)
311
+ expect(Dockistrano::Docker).to receive(:exec).with(subject.full_image_name, e: environment, v: volumes, p: ports, command: "foobar")
312
+ subject.exec("foobar")
313
+ end
314
+ end
315
+
316
+ context "#console" do
317
+ it "runs the command inside the container" do
318
+ allow(subject).to receive(:environment_variables).and_return(environment = double)
319
+ allow(subject).to receive(:volumes).and_return(volumes = double)
320
+ allow(subject).to receive(:ports).and_return(ports = double)
321
+ expect(subject).to receive(:create_data_directories)
322
+ expect(Dockistrano::Docker).to receive(:console).with(subject.full_image_name, e: environment, v: volumes, p: ports, command: "foobar")
323
+ subject.console("foobar")
324
+ end
325
+ end
326
+
327
+ context "#backing_services" do
328
+ let(:service) { double }
329
+
330
+ before do
331
+ allow(subject).to receive(:dependencies).and_return({"postgresql" => {}})
332
+ end
333
+
334
+ it "returns a hash with backing services" do
335
+ expect(Dockistrano::ServiceDependency).to receive(:factory).with(subject, "postgresql", {}).and_return(service)
336
+ expect(subject.backing_services).to eq({ "postgresql" => service })
337
+ end
338
+ end
339
+
340
+ context "#environment_variables" do
341
+ let(:backing_service){
342
+ double(
343
+ ip_address: "172.0.0.1",
344
+ port: "1245",
345
+ backing_service_env: { database: "dockistrano_development" },
346
+ environment_variables: { "DATABASE_URL" => "postgres://postgres@$POSTGRESQL_IP/$POSTGRESQL_DATABASE"}
347
+ )
348
+ }
349
+
350
+ before do
351
+ allow(subject).to receive(:backing_services).and_return({ "postgresql" => backing_service })
352
+ end
353
+
354
+ it "includes environment variables from the current containers configuration" do
355
+ subject.config = { "environment" => { "rails_env" => "test" } }
356
+ expect(subject.environment_variables).to include("RAILS_ENV")
357
+ expect(subject.environment_variables["RAILS_ENV"]).to eq("test")
358
+ end
359
+
360
+ it "includes environment variables with the ip and port of each backing service" do
361
+ expect(subject.environment_variables).to include("POSTGRESQL_IP")
362
+ expect(subject.environment_variables).to include("POSTGRESQL_PORT")
363
+ end
364
+
365
+ it "includes variables for the backing service provided in the local configuration" do
366
+ expect(subject.environment_variables).to include("POSTGRESQL_DATABASE")
367
+ expect(subject.environment_variables["POSTGRESQL_DATABASE"]).to eq("dockistrano_development")
368
+ end
369
+
370
+ it "includes environment variables from each backing service" do
371
+ expect(backing_service).to receive(:environment_variables).and_return({ "SUB_ENV" => "some_value" })
372
+ expect(subject.environment_variables["SUB_ENV"]).to eq("some_value")
373
+ end
374
+
375
+ it "includes environment variables that are provided by the service" do
376
+ expect(subject).to receive(:provides_env).and_return({ "BUNDLE_PATH" => "/bundle" })
377
+ expect(subject.environment_variables["BUNDLE_PATH"]).to eq("/bundle")
378
+ end
379
+
380
+ it "interpolates environment variables with present values" do
381
+ expect(subject.environment_variables["DATABASE_URL"]).to eq("postgres://postgres@172.0.0.1/dockistrano_development")
382
+ end
383
+ end
384
+
385
+ context "#volumes" do
386
+ it "includes a default data volume" do
387
+ expect(subject.volumes).to include("/dockistrano/image/data:/dockistrano/data")
388
+ end
389
+
390
+ it "includes a source mount when configured" do
391
+ allow(subject).to receive(:mount_src).and_return("/home/app")
392
+ expect(subject.volumes).to include("/dockistrano/image/src:/home/app")
393
+ end
394
+ end
395
+
396
+ context "#directories_required_on_host"
397
+
398
+ context "#available_tags" do
399
+ it "returns a list of available tags for the current service" do
400
+ expect(subject).to receive(:registry_instance).and_return(registry = double)
401
+ expect(registry).to receive(:tags_for_image).with(subject.full_image_name).and_return(["develop", "master"])
402
+ expect(subject.available_tags).to eq(["develop", "master"])
403
+ end
404
+
405
+ it "returns an empty list when the repository is not found in the registry" do
406
+ expect(subject).to receive(:registry_instance).and_return(registry = double)
407
+ expect(registry).to receive(:tags_for_image).with(subject.full_image_name).and_raise(Dockistrano::Registry::RepositoryNotFoundInRegistry)
408
+ expect(subject.available_tags).to eq([])
409
+ end
410
+ end
411
+
412
+ context "#tag_with_fallback" do
413
+ it "uses the feature branch when available" do
414
+ allow(subject).to receive(:tag).and_return("feature_branch")
415
+ allow(subject).to receive(:available_tags).and_return(["develop", "master", "feature_branch"])
416
+ expect(subject.tag_with_fallback).to eq("feature_branch")
417
+ end
418
+
419
+ it "uses the develop branch when available" do
420
+ allow(subject).to receive(:tag).and_return("feature_branch")
421
+ allow(subject).to receive(:available_tags).and_return(["develop", "master", "latest"])
422
+ expect(subject.tag_with_fallback).to eq("develop")
423
+ end
424
+
425
+ it "uses the master branch when available" do
426
+ allow(subject).to receive(:tag).and_return("feature_branch")
427
+ allow(subject).to receive(:available_tags).and_return(["master", "latest"])
428
+ expect(subject.tag_with_fallback).to eq("master")
429
+ end
430
+
431
+ it "uses the latest branch when available" do
432
+ allow(subject).to receive(:tag).and_return("feature_branch")
433
+ allow(subject).to receive(:available_tags).and_return(["latest"])
434
+ expect(subject.tag_with_fallback).to eq("latest")
435
+ end
436
+
437
+ it "raises an error when no tags are found" do
438
+ allow(subject).to receive(:tag).and_return("feature_branch")
439
+ allow(subject).to receive(:available_tags).and_return(["foobar", "test"])
440
+ expect { subject.tag_with_fallback }.to raise_error(Dockistrano::Service::NoTagFoundForImage)
441
+ end
442
+ end
443
+
444
+ context "#ip_address" do
445
+ it "returns the ip address of the running container" do
446
+ allow(subject).to receive(:running?).and_return(true)
447
+ allow(subject).to receive(:container_settings).and_return({ "NetworkSettings" => { "IPAddress" => "33.33.33.10" }})
448
+ expect(subject.ip_address).to eq("33.33.33.10")
449
+ end
450
+
451
+ it "returns nil when the container is not running" do
452
+ allow(subject).to receive(:running?).and_return(false)
453
+ expect(subject.ip_address).to be_nil
454
+ end
455
+ end
456
+
457
+ context "#port" do
458
+ it "returns the ip address of the running container" do
459
+ allow(subject).to receive(:running?).and_return(true)
460
+ allow(subject).to receive(:container_settings).and_return({ "NetworkSettings" => { "PortMapping" => { "Tcp" => { "8000" => "80" } } } })
461
+ expect(subject.port).to eq("8000")
462
+ end
463
+
464
+ it "returns nil when the container is not running" do
465
+ allow(subject).to receive(:running?).and_return(false)
466
+ expect(subject.port).to be_nil
467
+ end
468
+ end
469
+
470
+ context "#ports" do
471
+ it "returns a string representation of the port mappings" do
472
+ subject.config = { "ports" => { "1234" => "5678" } }
473
+ expect(subject.ports).to eq(["1234:5678"])
474
+ end
475
+ end
476
+
477
+ context "#attach" do
478
+ it "attaches to the output of the container" do
479
+ expect(Dockistrano::Docker).to receive(:attach).with("123456")
480
+ expect(Dockistrano::Docker).to receive(:running_container_id).with(subject.full_image_name).and_return("123456")
481
+ subject.attach
482
+ end
483
+ end
484
+
485
+ context "#logs" do
486
+ it "returns the logs of the last run of the container" do
487
+ expect(Dockistrano::Docker).to receive(:logs).with("123456")
488
+ expect(Dockistrano::Docker).to receive(:last_run_container_id).with(subject.full_image_name).and_return("123456")
489
+ subject.logs
490
+ end
491
+ end
492
+
493
+ context "#create_data_directories" do
494
+ it "creates directories in the mounted data folder" do
495
+ allow(subject).to receive(:data_directories).and_return(["logs"])
496
+ allow(subject).to receive(:volumes).and_return(volumes = double)
497
+ allow(subject).to receive(:environment_variables).and_return(environment_variables = double)
498
+ allow(Dockistrano::Docker).to receive(:inspect_image).with(subject.full_image_name).and_return({ "container_config" => { "User" => "app" }})
499
+
500
+ expect(Dockistrano::Docker).to receive(:run).with(subject.full_image_name,
501
+ v: volumes,
502
+ e: environment_variables,
503
+ u: "root",
504
+ command: "/bin/bash -c 'mkdir -p /dockistrano/data/logs; chown app:app /dockistrano/data/logs'"
505
+ )
506
+
507
+ subject.create_data_directories
508
+ end
509
+ end
510
+
511
+ context "#newer_version_available?" do
512
+ let(:registry_instance) { double }
513
+
514
+ before do
515
+ allow(subject).to receive(:registry_instance).and_return(registry_instance)
516
+ end
517
+
518
+ it "returns true when a newer version of the image is available" do
519
+ expect(subject).to receive(:image_id).and_return("1")
520
+ expect(registry_instance).to receive(:latest_id_for_image).with(subject.image_name, subject.tag).and_return("2")
521
+ expect(subject.newer_version_available?).to be_true
522
+ end
523
+
524
+ it "returns false when no registry image id is found" do
525
+ expect(registry_instance).to receive(:latest_id_for_image).with(subject.image_name, subject.tag).and_return(nil)
526
+ expect(subject.newer_version_available?).to be_false
527
+ end
528
+
529
+ it "returns false when the registry image id is equal to the local id" do
530
+ expect(subject).to receive(:image_id).and_return("1")
531
+ expect(registry_instance).to receive(:latest_id_for_image).with(subject.image_name, subject.tag).and_return("1")
532
+ expect(subject.newer_version_available?).to be_false
533
+ end
534
+ end
535
+
536
+ end
File without changes
@@ -0,0 +1,8 @@
1
+ ---
2
+ default:
3
+ registry: my.registry.com:5000
4
+ image_name: project_1
5
+ dependencies:
6
+ redis: git@github.com:username/redis.git
7
+ memcached: git@github.com:username/memcached.git
8
+
@@ -0,0 +1,21 @@
1
+ require 'rspec'
2
+ require 'dockistrano'
3
+ require 'webmock/rspec'
4
+
5
+ RSpec.configure do
6
+
7
+ # Capture method from Thor's own spec_helper
8
+ def capture(stream)
9
+ begin
10
+ stream = stream.to_s
11
+ eval "$#{stream} = StringIO.new"
12
+ yield
13
+ result = eval("$#{stream}").string
14
+ ensure
15
+ eval("$#{stream} = #{stream.upcase}")
16
+ end
17
+
18
+ result
19
+ end
20
+
21
+ end