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
- # An object representing a container configuration (a "specification"), with
6
- # methods for checking and/or ensuring existence (by name).
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
- # Note that it requires a container name, and an Image. Without an Image, there's
18
- # nothing to build. The name is essential to the purpose of the entire Tainers project.
19
- def initialize args={}
20
- raise ArgumentError, 'A name is required' unless valid_name? args['name']
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
- raise ArgumentError, 'An Image is required' unless valid_image? args['Image']
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
- # Maketh a copyeth of iteth
25
- @args = {}.merge(args)
26
- end
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
- # Ensures that the container named by this specification exists, creating the
29
- # container if necessary.
30
- #
31
- # Note that this only ensures that a container with the proper name exists; it does not
32
- # ensure that the existing container has a matching configuration.
33
- #
34
- # Returns true if the container reliably exists (it has been shown to exist, or was
35
- # successfully created, or failed to create due to a name conflict). All other cases
36
- # should result in exceptions.
37
- def ensure
38
- return self if exists?
39
- return self if Tainers::API.create_or_conflict(@args)
40
- return nil
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
- # Creates the container named by this specification, if it does
44
- # not already exist.
115
+ # Tainer specification that automatically pulls the image
116
+ # as needed prior to container creation operations.
45
117
  #
46
- # Returns true (self, actually) if the invocation resulted in the
47
- # creation of a new container; false otherwise.
118
+ # Wrap it around a bare specification to use it:
48
119
  #
49
- # A false condition could result from:
50
- # - The container already existing
51
- # - The container being simultaneously created by another
52
- # actor, with your invocation losing the race.
53
- #
54
- # A failure to create due to operational or semantic issues
55
- # should result in an exception. Therefore, any non-exceptional
56
- # case should mean that a container of the expected name exists,
57
- # though in the false result case there is no firm guarantee
58
- # that the existing container has the requested configuration e.
59
- def create
60
- return false if exists?
61
- return self if Tainers::API.create(@args)
62
- false
63
- end
64
-
65
- # The name of the container described by this specification.
66
- def name
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
- # True if the container of the appropriate name already exists. False if not.
71
- def exists?
72
- ! Tainers::API.get_by_name(name).nil?
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
- private
76
-
77
- def valid_name? name
78
- ! (name.nil? or name == '')
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 valid_image? image
82
- ! (image.nil? or image == '')
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 named_parameters_for(args)
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
- expect(Tainers::Specification).to receive(:new).with(base_args.dup.update('name' => name)).and_return(s = double)
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.2
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