tainers 0.0.2 → 0.1.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.
@@ -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