tainers 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
@@ -1,89 +1,167 @@
|
|
1
1
|
require 'docker'
|
2
2
|
|
3
3
|
module Tainers
|
4
|
+
module Specification
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
# While this can be used directly, it is not intended for direct instantiation,
|
9
|
-
# instead designed to be used via Tainers::specify, which provides name determination
|
10
|
-
# logic for organizing containers by their configuration.
|
11
|
-
class Specification
|
12
|
-
|
13
|
-
# Creates a new container specification that uses the same parameters supported
|
14
|
-
# by the Docker::Container::create singleton method. These parameters align
|
15
|
-
# with that of the docker daemon remote API.
|
6
|
+
# An object representing a container configuration (a "specification"), with
|
7
|
+
# methods for checking and/or ensuring existence (by name).
|
16
8
|
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
|
9
|
+
# While this can be used directly, it is not intended for direct instantiation,
|
10
|
+
# instead designed to be used via Tainers::specify, which provides name determination
|
11
|
+
# logic for organizing containers by their configuration.
|
12
|
+
class Bare
|
13
|
+
|
14
|
+
# Creates a new container specification that uses the same parameters supported
|
15
|
+
# by the Docker::Container::create singleton method. These parameters align
|
16
|
+
# with that of the docker daemon remote API.
|
17
|
+
#
|
18
|
+
# Note that it requires a container name, and an Image. Without an Image, there's
|
19
|
+
# nothing to build. The name is essential to the purpose of the entire Tainers project.
|
20
|
+
def initialize args={}
|
21
|
+
raise ArgumentError, 'A name is required' unless valid_name? args['name']
|
22
|
+
|
23
|
+
raise ArgumentError, 'An Image is required' unless valid_image? args['Image']
|
24
|
+
|
25
|
+
# Maketh a copyeth of iteth
|
26
|
+
@args = {}.merge(args)
|
27
|
+
end
|
21
28
|
|
22
|
-
|
29
|
+
# Ensures that the container named by this specification exists, creating the
|
30
|
+
# container if necessary.
|
31
|
+
#
|
32
|
+
# Note that this only ensures that a container with the proper name exists; it does not
|
33
|
+
# ensure that the existing container has a matching configuration.
|
34
|
+
#
|
35
|
+
# Returns true if the container reliably exists (it has been shown to exist, or was
|
36
|
+
# successfully created, or failed to create due to a name conflict). All other cases
|
37
|
+
# should result in exceptions.
|
38
|
+
def ensure
|
39
|
+
return self if exists?
|
40
|
+
return self if Tainers::API.create_or_conflict(@args)
|
41
|
+
return nil
|
42
|
+
end
|
23
43
|
|
24
|
-
#
|
25
|
-
|
26
|
-
|
44
|
+
# Creates the container named by this specification, if it does
|
45
|
+
# not already exist.
|
46
|
+
#
|
47
|
+
# Returns true (self, actually) if the invocation resulted in the
|
48
|
+
# creation of a new container; false otherwise.
|
49
|
+
#
|
50
|
+
# A false condition could result from:
|
51
|
+
# - The container already existing
|
52
|
+
# - The container being simultaneously created by another
|
53
|
+
# actor, with your invocation losing the race.
|
54
|
+
#
|
55
|
+
# A failure to create due to operational or semantic issues
|
56
|
+
# should result in an exception. Therefore, any non-exceptional
|
57
|
+
# case should mean that a container of the expected name exists,
|
58
|
+
# though in the false result case there is no firm guarantee
|
59
|
+
# that the existing container has the requested configuration e.
|
60
|
+
def create
|
61
|
+
return false if exists?
|
62
|
+
return self if Tainers::API.create(@args)
|
63
|
+
false
|
64
|
+
end
|
27
65
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
66
|
+
# The name of the container described by this specification.
|
67
|
+
def name
|
68
|
+
@args['name']
|
69
|
+
end
|
70
|
+
|
71
|
+
# The image of the container described by this specification.
|
72
|
+
# Note that this is a string (a tag or image ID) and not a more
|
73
|
+
# complex object.
|
74
|
+
def image
|
75
|
+
@args['Image']
|
76
|
+
end
|
77
|
+
|
78
|
+
# True if the container of the appropriate name already exists. False if not.
|
79
|
+
def exists?
|
80
|
+
! Tainers::API.get_by_name(name).nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def valid_name? name
|
86
|
+
! (name.nil? or name == '')
|
87
|
+
end
|
88
|
+
|
89
|
+
def valid_image? image
|
90
|
+
! (image.nil? or image == '')
|
91
|
+
end
|
92
|
+
end # class Bare
|
93
|
+
|
94
|
+
module Delegator
|
95
|
+
def self.delegates(method_name)
|
96
|
+
method_name = method_name.to_sym
|
97
|
+
define_method(method_name) do |*args|
|
98
|
+
chain.send(method_name, *args)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def initialize(chain)
|
103
|
+
@chain = chain
|
104
|
+
end
|
105
|
+
|
106
|
+
attr_reader :chain
|
107
|
+
|
108
|
+
delegates :create
|
109
|
+
delegates :ensure
|
110
|
+
delegates :exists?
|
111
|
+
delegates :image
|
112
|
+
delegates :name
|
41
113
|
end
|
42
114
|
|
43
|
-
#
|
44
|
-
#
|
115
|
+
# Tainer specification that automatically pulls the image
|
116
|
+
# as needed prior to container creation operations.
|
45
117
|
#
|
46
|
-
#
|
47
|
-
# creation of a new container; false otherwise.
|
118
|
+
# Wrap it around a bare specification to use it:
|
48
119
|
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
@args['name']
|
68
|
-
end
|
120
|
+
# t1 = Tainers::Specification::Bare.new('Image' =>' foo')
|
121
|
+
# t2 = Tainers::Specification::ImagePuller.new(t1)
|
122
|
+
# # This doesn't pull image "foo"
|
123
|
+
# t1.ensure
|
124
|
+
# # But this will, if necessary
|
125
|
+
# t2.ensure
|
126
|
+
#
|
127
|
+
# Note that the #ensure and #create methods have the pulling
|
128
|
+
# behavior; no others do.
|
129
|
+
class ImagePuller
|
130
|
+
include Delegator
|
131
|
+
|
132
|
+
def self.ensure_image(image)
|
133
|
+
if ! Tainers::API.image_exists?(image)
|
134
|
+
Tainers::API.pull_image image
|
135
|
+
end
|
136
|
+
true
|
137
|
+
end
|
69
138
|
|
70
|
-
|
71
|
-
|
72
|
-
|
139
|
+
def self.pulls_and_delegates(method_name)
|
140
|
+
method_name = method_name.to_sym
|
141
|
+
define_method(method_name) do |*args|
|
142
|
+
self.class.ensure_image(chain.image)
|
143
|
+
chain.send(method_name, *args)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
pulls_and_delegates :create
|
148
|
+
pulls_and_delegates :ensure
|
73
149
|
end
|
150
|
+
end # module Specification
|
74
151
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
152
|
+
module API
|
153
|
+
def self.image_exists? name
|
154
|
+
begin
|
155
|
+
return true if Docker::Image.get(name)
|
156
|
+
rescue Docker::Error::NotFoundError
|
157
|
+
return false
|
158
|
+
end
|
79
159
|
end
|
80
160
|
|
81
|
-
def
|
82
|
-
|
161
|
+
def self.pull_image name
|
162
|
+
Docker::Image.create('fromImage' => name)
|
83
163
|
end
|
84
|
-
end
|
85
164
|
|
86
|
-
module API
|
87
165
|
def self.get_by_name name
|
88
166
|
begin
|
89
167
|
Docker::Container.get(name)
|
@@ -105,10 +183,20 @@ module Tainers
|
|
105
183
|
create params
|
106
184
|
true
|
107
185
|
end
|
108
|
-
end
|
186
|
+
end # module API
|
109
187
|
|
188
|
+
# Returns an image-pulling container specification
|
189
|
+
# from the given parameters.
|
190
|
+
#
|
191
|
+
# Enforces the naming conventions such that the
|
192
|
+
# name for the container will have prefix, suffix,
|
193
|
+
# and spec-derived hash as documented elsewhere.
|
194
|
+
#
|
195
|
+
# The result will be an instance of Tainers::Specification::ImagePuller.
|
110
196
|
def self.specify args={}
|
111
|
-
Specification.new
|
197
|
+
Specification::ImagePuller.new(
|
198
|
+
Specification::Bare.new named_parameters_for(args)
|
199
|
+
)
|
112
200
|
end
|
113
201
|
|
114
202
|
def self.named_parameters_for params
|
@@ -22,13 +22,13 @@ shared_examples_for 'container creator' do
|
|
22
22
|
end
|
23
23
|
|
24
24
|
|
25
|
-
RSpec.describe Tainers::Specification do
|
25
|
+
RSpec.describe Tainers::Specification::Bare do
|
26
26
|
it 'requires a name' do
|
27
|
-
expect { Tainers::Specification.new 'Image' => 'foo/image:latest' }.to raise_error(/name is required/)
|
27
|
+
expect { Tainers::Specification::Bare.new 'Image' => 'foo/image:latest' }.to raise_error(/name is required/)
|
28
28
|
end
|
29
29
|
|
30
30
|
it 'requires an image' do
|
31
|
-
expect { Tainers::Specification.new 'name' => 'something' }.to raise_error(/Image is required/)
|
31
|
+
expect { Tainers::Specification::Bare.new 'name' => 'something' }.to raise_error(/Image is required/)
|
32
32
|
end
|
33
33
|
|
34
34
|
context 'for a container' do
|
@@ -38,7 +38,15 @@ RSpec.describe Tainers::Specification do
|
|
38
38
|
let(:specification_args) { container_args.merge('name' => name) }
|
39
39
|
|
40
40
|
subject do
|
41
|
-
Tainers::Specification.new specification_args
|
41
|
+
Tainers::Specification::Bare.new specification_args
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'has a name' do
|
45
|
+
expect(subject.name).to equal(name)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'has an image' do
|
49
|
+
expect(subject.image).to equal(image)
|
42
50
|
end
|
43
51
|
|
44
52
|
context 'that does not exist' do
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'rspec_helper'
|
2
|
+
|
3
|
+
shared_examples_for 'specification delegator' do |method|
|
4
|
+
let(:callable) do
|
5
|
+
subject.method(method.to_sym)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "passes through to underlying method" do
|
9
|
+
expect(wrapped).to receive(method.to_sym).with(no_args).and_return(result = double)
|
10
|
+
expect(callable.call).to be(result)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "passes arguments through to underlying method" do
|
14
|
+
args = [double, double, double]
|
15
|
+
expect(wrapped).to receive(method.to_sym).with(*args).and_return(result = double)
|
16
|
+
expect(callable.call(*args)).to be(result)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
shared_examples_for 'pulling delegator' do |method|
|
21
|
+
let(:callable) do
|
22
|
+
subject.method(method.to_sym)
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:image) do
|
26
|
+
"image-#{double.to_s}-foo"
|
27
|
+
end
|
28
|
+
|
29
|
+
before do
|
30
|
+
allow(wrapped).to receive(:image).with(no_args).and_return(image)
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "with no existing image" do
|
34
|
+
before do
|
35
|
+
expect(Docker::Image).to receive(:get).with(image).and_raise(Docker::Error::NotFoundError)
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'and successful pull' do
|
39
|
+
let(:api_image) { double }
|
40
|
+
|
41
|
+
before do
|
42
|
+
expect(Docker::Image).to receive(:create).with('fromImage' => image).and_return(api_image)
|
43
|
+
end
|
44
|
+
|
45
|
+
it_behaves_like 'specification delegator', method
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'and failed pull' do
|
49
|
+
before do
|
50
|
+
# Just a general exception on this rather than a specific type;
|
51
|
+
# the docker-api gem doesn't give a pretty exception here.
|
52
|
+
expect(Docker::Image).to receive(:create).with('fromImage' => image).and_raise("Pull failed!")
|
53
|
+
expect(wrapped).to receive(method.to_sym).never
|
54
|
+
end
|
55
|
+
|
56
|
+
it "propagates the pull failure exception" do
|
57
|
+
expect { callable.call }.to raise_error("Pull failed!")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "with an existing image" do
|
63
|
+
before do
|
64
|
+
expect(Docker::Image).to receive(:get).with(image).and_return(double)
|
65
|
+
expect(Docker::Image).to receive(:create).never
|
66
|
+
end
|
67
|
+
|
68
|
+
it_behaves_like 'specification delegator', method
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe Tainers::Specification::ImagePuller do
|
73
|
+
let(:wrapped) { double }
|
74
|
+
|
75
|
+
subject { Tainers::Specification::ImagePuller.new wrapped }
|
76
|
+
|
77
|
+
describe '#image method' do
|
78
|
+
it_behaves_like 'specification delegator', :image
|
79
|
+
end
|
80
|
+
|
81
|
+
describe '#name method' do
|
82
|
+
it_behaves_like 'specification delegator', :name
|
83
|
+
end
|
84
|
+
|
85
|
+
describe '#exists? method' do
|
86
|
+
it_behaves_like 'specification delegator', :exists?
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#create method' do
|
90
|
+
it_behaves_like 'pulling delegator', :create
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '#ensure method' do
|
94
|
+
it_behaves_like 'pulling delegator', :ensure
|
95
|
+
end
|
96
|
+
end
|
data/spec/specify_spec.rb
CHANGED
@@ -3,7 +3,10 @@ require 'rspec_helper'
|
|
3
3
|
shared_examples_for 'a named specification' do
|
4
4
|
it 'produces a deterministically named specification' do
|
5
5
|
expect(Tainers).to receive(:hash).with(base_args.dup).and_return(hash)
|
6
|
-
|
6
|
+
# Assemble the bare specification from the parameters.
|
7
|
+
expect(Tainers::Specification::Bare).to receive(:new).with(base_args.dup.update('name' => name)).and_return(bare = double)
|
8
|
+
# And wrap it with image puller behavior so the easy case is for images to get automatically pulled down as needed.
|
9
|
+
expect(Tainers::Specification::ImagePuller).to receive(:new).with(bare).and_return(s = double)
|
7
10
|
expect(Tainers.specify specify_args).to eq(s)
|
8
11
|
end
|
9
12
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tainers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -71,12 +71,13 @@ files:
|
|
71
71
|
- lib/tainers/hash.rb
|
72
72
|
- lib/tainers.rb
|
73
73
|
- spec/hashing_spec.rb
|
74
|
+
- spec/container_specifications/bare_spec.rb
|
75
|
+
- spec/container_specifications/image_puller_spec.rb
|
74
76
|
- spec/commands/exists_spec.rb
|
75
77
|
- spec/commands/ensure_spec.rb
|
76
78
|
- spec/commands/common_examples.rb
|
77
79
|
- spec/commands/create_spec.rb
|
78
80
|
- spec/commands/name_spec.rb
|
79
|
-
- spec/specification_spec.rb
|
80
81
|
- spec/rspec_helper.rb
|
81
82
|
- spec/specify_spec.rb
|
82
83
|
- bin/tainers
|