dockistrano 0.0.1

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,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