tainers 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.
- data/Gemfile +8 -0
- data/Gemfile.lock +30 -0
- data/bin/tainers +7 -0
- data/lib/tainers.rb +9 -0
- data/lib/tainers/cli.rb +128 -0
- data/lib/tainers/hash.rb +44 -0
- data/lib/tainers/specification.rb +106 -0
- data/spec/commands/common_examples.rb +129 -0
- data/spec/commands/ensure_spec.rb +17 -0
- data/spec/commands/exists_spec.rb +22 -0
- data/spec/commands/name_spec.rb +18 -0
- data/spec/hashing_spec.rb +87 -0
- data/spec/rspec_helper.rb +3 -0
- data/spec/specification_spec.rb +63 -0
- data/spec/specify_spec.rb +79 -0
- metadata +110 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
archive-tar-minitar (0.5.2)
|
5
|
+
diff-lcs (1.2.5)
|
6
|
+
docker-api (1.15.0)
|
7
|
+
archive-tar-minitar
|
8
|
+
excon (>= 0.38.0)
|
9
|
+
json
|
10
|
+
excon (0.41.0)
|
11
|
+
json (1.8.1)
|
12
|
+
rspec (3.1.0)
|
13
|
+
rspec-core (~> 3.1.0)
|
14
|
+
rspec-expectations (~> 3.1.0)
|
15
|
+
rspec-mocks (~> 3.1.0)
|
16
|
+
rspec-core (3.1.7)
|
17
|
+
rspec-support (~> 3.1.0)
|
18
|
+
rspec-expectations (3.1.2)
|
19
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
20
|
+
rspec-support (~> 3.1.0)
|
21
|
+
rspec-mocks (3.1.3)
|
22
|
+
rspec-support (~> 3.1.0)
|
23
|
+
rspec-support (3.1.2)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
docker-api (~> 1.15.0)
|
30
|
+
rspec (~> 3.1.0)
|
data/bin/tainers
ADDED
data/lib/tainers.rb
ADDED
data/lib/tainers/cli.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Tainers
|
5
|
+
module CLI
|
6
|
+
def self.run parameters
|
7
|
+
spec, parameters = parse(parameters)
|
8
|
+
cmd = Command.new(spec)
|
9
|
+
cmd_name = parameters.shift
|
10
|
+
cmd.send("#{cmd_name}_command".to_sym, *parameters)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.parse parameters
|
14
|
+
options = {}
|
15
|
+
options[:spec_source] = from_stdin
|
16
|
+
opt_parser = OptionParser.new do |opts|
|
17
|
+
opts.banner = "Usage: tainers [opts] COMMAND"
|
18
|
+
opts.separator ""
|
19
|
+
opts.separator "Manipulate Tainer-managed containers, taking their specification in JSON (from STDIN by default)."
|
20
|
+
opts.separator ""
|
21
|
+
opts.separator "Specific options:"
|
22
|
+
|
23
|
+
opts.on('-j JSON', '--json JSON', String, "Take container specification from the given JSON parameter.") do |j|
|
24
|
+
options[:spec_source] = from_json(j)
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('-f FILEPATH', '--file FILEPATH', String, "Take container specification from JSON in the given file.") do |f|
|
28
|
+
options[:spec_source] = from_file(f)
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on('-p PREFIX', '--prefix PREFIX', String, "Use PREFIX as container name prefix (overriding whatever is in spec)") do |p|
|
32
|
+
options['prefix'] = p
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on('-s SUFFIX', '--suffix SUFFIX', String, "Use SUFFIX as container name suffix (overriding whatever is in spec)") do |s|
|
36
|
+
options['suffix'] = s
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on('-h', '--help') do
|
40
|
+
print opts
|
41
|
+
exit 0
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.separator ""
|
45
|
+
opts.separator "Commands:"
|
46
|
+
Command.commands.each do |name, help|
|
47
|
+
opts.separator ""
|
48
|
+
opts.separator " #{name}"
|
49
|
+
opts.separator " #{help}" if help.size > 0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Stop on first non-option param
|
54
|
+
non_param = []
|
55
|
+
args = opt_parser.order(parameters) {|p| non_param << p; opt_parser.terminate}
|
56
|
+
spec = options.delete(:spec_source).call
|
57
|
+
spec.update(options)
|
58
|
+
[spec, non_param + args]
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.from_stdin
|
62
|
+
Proc.new do
|
63
|
+
JSON.parse(STDIN.read)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.from_file file
|
68
|
+
Proc.new do
|
69
|
+
File.open(file, "r") do |f|
|
70
|
+
JSON.parse(f.read)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.from_json json
|
76
|
+
Proc.new do
|
77
|
+
JSON.parse json
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Command
|
82
|
+
attr_reader :specification
|
83
|
+
|
84
|
+
def initialize(spec={})
|
85
|
+
@specification = Tainers.specify(spec)
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.commands
|
89
|
+
cmds = instance_methods.collect {|m| m.to_s}.find_all {|m| m.to_s =~ /_command$/}.collect {|n| n.to_s[0..-9] }
|
90
|
+
cmds.collect do |name|
|
91
|
+
helper = "#{name}_help".to_sym
|
92
|
+
if respond_to? helper
|
93
|
+
[name, send(helper)]
|
94
|
+
else
|
95
|
+
[name, '']
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def ensure_command
|
101
|
+
return 0 if specification.ensure
|
102
|
+
255
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.ensure_help
|
106
|
+
"Ensures specified container exists, by name."
|
107
|
+
end
|
108
|
+
|
109
|
+
def exists_command
|
110
|
+
return 0 if specification.exists?
|
111
|
+
1
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.exists_help
|
115
|
+
"Exits with 0 (true) if specified container exists, by name; 1 (false) if not."
|
116
|
+
end
|
117
|
+
|
118
|
+
def name_command
|
119
|
+
STDOUT.print "#{specification.name}\n"
|
120
|
+
0
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.name_help
|
124
|
+
"Prints the specified container's name on stdout."
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/tainers/hash.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Tainers
|
2
|
+
# call-seq:
|
3
|
+
# Tainers.hash(structure) => consistent hash hexdigest
|
4
|
+
#
|
5
|
+
# Produces a consistent hash hexdigest string for the input
|
6
|
+
# structure, handling arrays, hashes, strings, numbers, nesting
|
7
|
+
# of such structures, etc.
|
8
|
+
#
|
9
|
+
# Hash keys are sorted prior to computing the digest, to ensure
|
10
|
+
# that hashes that would be regarded as equal produce the same
|
11
|
+
# digest.
|
12
|
+
#
|
13
|
+
# Tainers.hash({"a" => "foo", "b" => "bar"}) #=> "5427f704439548cae1911616e2bec3b7cc2dd11c"
|
14
|
+
#
|
15
|
+
def self.hash structure
|
16
|
+
str = structured_string(structure)
|
17
|
+
Digest::SHA1.hexdigest str
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.consistent_structure structure
|
23
|
+
if Hash === structure
|
24
|
+
s = structure.keys.sort.inject(["{"]) do |a, key|
|
25
|
+
a << consistent_structure(key)
|
26
|
+
a << consistent_structure(structure[key])
|
27
|
+
a
|
28
|
+
end
|
29
|
+
s << "}"
|
30
|
+
s
|
31
|
+
elsif Array === structure
|
32
|
+
s = structure.collect {|item| consistent_structure item}
|
33
|
+
s.unshift "["
|
34
|
+
s << "]"
|
35
|
+
s
|
36
|
+
else
|
37
|
+
structure
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.structured_string structure
|
42
|
+
consistent_structure(structure).to_json
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'docker'
|
2
|
+
|
3
|
+
module Tainers
|
4
|
+
|
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.
|
16
|
+
#
|
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']
|
21
|
+
|
22
|
+
raise ArgumentError, 'An Image is required' unless valid_image? args['Image']
|
23
|
+
|
24
|
+
# Maketh a copyeth of iteth
|
25
|
+
@args = {}.merge(args)
|
26
|
+
end
|
27
|
+
|
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
|
41
|
+
end
|
42
|
+
|
43
|
+
# The name of the container described by this specification.
|
44
|
+
def name
|
45
|
+
@args['name']
|
46
|
+
end
|
47
|
+
|
48
|
+
# True if the container of the appropriate name already exists. False if not.
|
49
|
+
def exists?
|
50
|
+
! Tainers::API.get_by_name(name).nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def valid_name? name
|
56
|
+
! (name.nil? or name == '')
|
57
|
+
end
|
58
|
+
|
59
|
+
def valid_image? image
|
60
|
+
! (image.nil? or image == '')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module API
|
65
|
+
def self.get_by_name name
|
66
|
+
begin
|
67
|
+
Docker::Container.get(name)
|
68
|
+
rescue Docker::Error::NotFoundError
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.create_or_conflict params
|
74
|
+
begin
|
75
|
+
Docker::Container.create(params.dup)
|
76
|
+
return true
|
77
|
+
rescue Excon::Errors::Conflict
|
78
|
+
return true
|
79
|
+
end
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.specify args={}
|
85
|
+
Specification.new named_parameters_for(args)
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.named_parameters_for params
|
89
|
+
p = params.dup
|
90
|
+
prefix = p.delete('prefix')
|
91
|
+
prefix = if prefix.nil? || prefix == ''
|
92
|
+
'Tainers'
|
93
|
+
else
|
94
|
+
prefix.downcase
|
95
|
+
end
|
96
|
+
suffix = p.delete('suffix')
|
97
|
+
suffix = if suffix.nil?
|
98
|
+
''
|
99
|
+
else
|
100
|
+
"-#{suffix.downcase}"
|
101
|
+
end
|
102
|
+
digest = hash(p)
|
103
|
+
p['name'] = "#{prefix}-#{digest}#{suffix}"
|
104
|
+
p
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'rspec_helper'
|
2
|
+
require 'json'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
shared_examples_for 'prefix and suffix' do |command_group|
|
6
|
+
let(:param_prefix) { "opt-" + double.object_id.to_s }
|
7
|
+
let(:param_suffix) { "opt-" + double.object_id.to_s }
|
8
|
+
|
9
|
+
context 'with --prefix' do
|
10
|
+
before do
|
11
|
+
args << '--prefix'
|
12
|
+
args << param_prefix
|
13
|
+
expected_specification['prefix'] = param_prefix
|
14
|
+
end
|
15
|
+
|
16
|
+
it_behaves_like command_group
|
17
|
+
|
18
|
+
context 'and --suffix' do
|
19
|
+
before do
|
20
|
+
args << '--suffix'
|
21
|
+
args << param_suffix
|
22
|
+
expected_specification['suffix'] = param_suffix
|
23
|
+
end
|
24
|
+
|
25
|
+
it_behaves_like command_group
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'with --suffix' do
|
30
|
+
before do
|
31
|
+
args << '--suffix'
|
32
|
+
args << param_suffix
|
33
|
+
expected_specification['suffix'] = param_suffix
|
34
|
+
end
|
35
|
+
|
36
|
+
it_behaves_like command_group
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
shared_examples_for 'a command' do |command_group|
|
41
|
+
let :provided_specification do
|
42
|
+
{
|
43
|
+
'Image' => double.object_id.to_s,
|
44
|
+
double.to_s => double.to_s,
|
45
|
+
'prefix' => "spec-" + double.object_id.to_s,
|
46
|
+
'suffix' => "spec-" + double.object_id.to_s
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
let :expected_specification do
|
51
|
+
provided_specification.dup
|
52
|
+
end
|
53
|
+
|
54
|
+
let(:args) { [] }
|
55
|
+
|
56
|
+
let(:json) { provided_specification.to_json }
|
57
|
+
|
58
|
+
let(:specification) { double }
|
59
|
+
|
60
|
+
let :command_run do
|
61
|
+
Tainers::CLI.run args
|
62
|
+
end
|
63
|
+
|
64
|
+
before do
|
65
|
+
expect(Tainers).to receive(:specify).with(expected_specification).and_return(specification)
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'given JSON specification in parameters' do
|
69
|
+
context 'via -j' do
|
70
|
+
before do
|
71
|
+
args << '-j'
|
72
|
+
args << json
|
73
|
+
end
|
74
|
+
|
75
|
+
it_behaves_like "prefix and suffix", command_group
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'via --json' do
|
79
|
+
before do
|
80
|
+
args << '--json'
|
81
|
+
args << json
|
82
|
+
end
|
83
|
+
|
84
|
+
it_behaves_like "prefix and suffix", command_group
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'given JSON specification in a file' do
|
89
|
+
let(:file) do
|
90
|
+
f = Tempfile.new('tainers-test-spec')
|
91
|
+
f.write(json)
|
92
|
+
f.close
|
93
|
+
f
|
94
|
+
end
|
95
|
+
|
96
|
+
let(:path) { file.path }
|
97
|
+
|
98
|
+
after do
|
99
|
+
file.unlink
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'via -f' do
|
103
|
+
before do
|
104
|
+
args << '-f'
|
105
|
+
args << path
|
106
|
+
end
|
107
|
+
|
108
|
+
it_behaves_like "prefix and suffix", command_group
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'via --file' do
|
112
|
+
before do
|
113
|
+
args << '--file'
|
114
|
+
args << path
|
115
|
+
end
|
116
|
+
|
117
|
+
it_behaves_like "prefix and suffix", command_group
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'given JSON specification on STDIN' do
|
122
|
+
before do
|
123
|
+
expect(STDIN).to receive(:read).and_return(json)
|
124
|
+
end
|
125
|
+
|
126
|
+
it_behaves_like "prefix and suffix", command_group
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'common_examples'
|
2
|
+
|
3
|
+
shared_examples_for 'ensure command' do
|
4
|
+
before do
|
5
|
+
args << 'ensure'
|
6
|
+
end
|
7
|
+
|
8
|
+
it "ensures container and exits appropriately" do
|
9
|
+
expect(specification).to receive(:ensure).with(no_args).and_return(true)
|
10
|
+
expect(command_run).to eq(0)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'tainers ensure' do
|
15
|
+
it_behaves_like "a command", "ensure command"
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'common_examples'
|
2
|
+
|
3
|
+
shared_examples_for 'exists command' do
|
4
|
+
before do
|
5
|
+
args << 'exists'
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'returns 0 for an existing container' do
|
9
|
+
expect(specification).to receive(:exists?).with(no_args).and_return(true)
|
10
|
+
expect(command_run).to eq(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns 1 for a non-existent container' do
|
14
|
+
expect(specification).to receive(:exists?).with(no_args).and_return(false)
|
15
|
+
expect(command_run).to eq(1)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'tainers exists' do
|
20
|
+
it_behaves_like 'a command', 'exists command'
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'common_examples'
|
2
|
+
|
3
|
+
shared_examples_for 'name command' do
|
4
|
+
before do
|
5
|
+
args << 'name'
|
6
|
+
end
|
7
|
+
|
8
|
+
it "writes the name of the container to stdout" do
|
9
|
+
expect(specification).to receive(:name).with(no_args).and_return(name = "some-name-" + double.object_id.to_s)
|
10
|
+
expect(STDOUT).to receive(:print).with("#{name}\n")
|
11
|
+
expect(command_run).to eq(0)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'tainers name' do
|
16
|
+
it_behaves_like 'a command', 'name command'
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rspec_helper'
|
2
|
+
require 'digest'
|
3
|
+
|
4
|
+
def hashit str
|
5
|
+
Digest::SHA1.hexdigest(str)
|
6
|
+
end
|
7
|
+
|
8
|
+
RSpec.describe 'Tainers.hash' do
|
9
|
+
let(:test_number) { self.object_id }
|
10
|
+
let(:test_string) { "something-#{self.object_id.to_s}-blah" }
|
11
|
+
|
12
|
+
it 'handles a simple string' do
|
13
|
+
expect(Tainers.hash test_string).to eq(hashit('"' + test_string + '"'))
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'handles a simple number' do
|
17
|
+
expect(Tainers.hash test_number).to eq(hashit(test_number.to_s))
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'handles an array of mixed simple type' do
|
21
|
+
expected_ary = ['[', test_string, test_number, ']'].to_json
|
22
|
+
expect(Tainers.hash [test_string, test_number]).to eq(hashit expected_ary)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'handles a hash of mixed simple type' do
|
26
|
+
expected = ["{", "a", test_number, "b", test_string, "c", test_number + 1, "d", test_string + "z", "}"].to_json
|
27
|
+
expect(Tainers.hash "c" => test_number + 1,
|
28
|
+
"a" => test_number,
|
29
|
+
"d" => test_string + "z",
|
30
|
+
"b" => test_string).to eq(hashit expected)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'handles a complex structure' do
|
34
|
+
expected = [
|
35
|
+
"[",
|
36
|
+
test_number,
|
37
|
+
[
|
38
|
+
'{',
|
39
|
+
'first', [
|
40
|
+
'{',
|
41
|
+
1, 'one',
|
42
|
+
2, 'two',
|
43
|
+
'}',
|
44
|
+
],
|
45
|
+
'second', [
|
46
|
+
'[',
|
47
|
+
'a',
|
48
|
+
'b',
|
49
|
+
']',
|
50
|
+
],
|
51
|
+
'}',
|
52
|
+
],
|
53
|
+
test_string,
|
54
|
+
[
|
55
|
+
'{',
|
56
|
+
'fourth', [
|
57
|
+
'[',
|
58
|
+
1000,
|
59
|
+
500,
|
60
|
+
']',
|
61
|
+
],
|
62
|
+
'third', [
|
63
|
+
'{',
|
64
|
+
['[', 'a', 'b', ']'], 'ey',
|
65
|
+
['[', 'b', 'a', ']'], 'bee',
|
66
|
+
'}',
|
67
|
+
],
|
68
|
+
'}',
|
69
|
+
],
|
70
|
+
"]",
|
71
|
+
].to_json
|
72
|
+
struct = [
|
73
|
+
test_number,
|
74
|
+
{
|
75
|
+
'first' => {2 => 'two', 1 => 'one'},
|
76
|
+
'second' => ['a', 'b'],
|
77
|
+
},
|
78
|
+
test_string,
|
79
|
+
{
|
80
|
+
'third' => {['b','a'] => 'bee', ['a','b'] => 'ey'},
|
81
|
+
'fourth' => [1000, 500],
|
82
|
+
},
|
83
|
+
]
|
84
|
+
expect(Tainers.hash struct).to eq(hashit expected)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rspec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Tainers::Specification do
|
4
|
+
it 'requires a name' do
|
5
|
+
expect { Tainers::Specification.new 'Image' => 'foo/image:latest' }.to raise_error(/name is required/)
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'requires an image' do
|
9
|
+
expect { Tainers::Specification.new 'name' => 'something' }.to raise_error(/Image is required/)
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'for a container' do
|
13
|
+
let(:name) { "something-#{object_id.to_s}-foo" }
|
14
|
+
let(:image) { "some.#{object_id.to_s}.repo:5000/some-image/#{object_id.to_s[0..5]}" }
|
15
|
+
let(:container_args) { {'Image' => image, double.to_s => double.to_s } }
|
16
|
+
let(:specification_args) { container_args.merge('name' => name) }
|
17
|
+
|
18
|
+
subject do
|
19
|
+
Tainers::Specification.new specification_args
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'that does not exist' do
|
23
|
+
it 'indicates non-existence' do
|
24
|
+
expect(Docker::Container).to receive(:get).with(name).and_raise(Docker::Error::NotFoundError)
|
25
|
+
expect(subject.exists?).to be false
|
26
|
+
end
|
27
|
+
|
28
|
+
context '#ensure' do
|
29
|
+
before do
|
30
|
+
expect(subject).to receive(:exists?).and_return(false)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'uses Docker::Container for creation' do
|
34
|
+
expect(Docker::Container).to receive(:create).with(specification_args).and_return(container = double)
|
35
|
+
expect(subject.ensure).to be(subject)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'is okay with a conflict result' do
|
39
|
+
expect(Docker::Container).to receive(:create).with(specification_args).and_raise(Excon::Errors::Conflict, "Pickles")
|
40
|
+
expect(subject.ensure).to be(subject)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'does not handle other exceptions' do
|
44
|
+
expect(Docker::Container).to receive(:create).with(specification_args).and_raise(Exception, "You suck.")
|
45
|
+
expect { subject.ensure }.to raise_error("You suck.")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'that exists' do
|
51
|
+
it 'indicates existence' do
|
52
|
+
expect(Docker::Container).to receive(:get).with(name).and_return(double)
|
53
|
+
expect(subject.exists?).to be true
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'does a no-op for #ensure' do
|
57
|
+
expect(subject).to receive(:exists?).and_return(true)
|
58
|
+
expect(Docker::Container).to receive(:create).never
|
59
|
+
expect(subject.ensure).to be(subject)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rspec_helper'
|
2
|
+
|
3
|
+
shared_examples_for 'a named specification' do
|
4
|
+
it 'produces a deterministically named specification' do
|
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)
|
7
|
+
expect(Tainers.specify specify_args).to eq(s)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'Tainers::specify' do
|
12
|
+
let(:prefix) { nil }
|
13
|
+
let(:suffix) { nil }
|
14
|
+
|
15
|
+
let(:base_args) { { double.to_s => double.to_s, double.to_s => double.to_s } }
|
16
|
+
|
17
|
+
let(:specify_args) {
|
18
|
+
[['prefix', prefix], ['suffix', suffix]].inject(base_args.dup) do |a, item|
|
19
|
+
if item[1].nil?
|
20
|
+
a
|
21
|
+
else
|
22
|
+
a.merge(item[0] => item[1])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
}
|
26
|
+
|
27
|
+
let(:hash) { double.object_id.to_s }
|
28
|
+
|
29
|
+
let(:name) {
|
30
|
+
pre = if prefix.nil?
|
31
|
+
'Tainers'
|
32
|
+
else
|
33
|
+
prefix.downcase
|
34
|
+
end
|
35
|
+
pre = 'Tainers' if pre.nil?
|
36
|
+
suf = if suffix.nil?
|
37
|
+
''
|
38
|
+
else
|
39
|
+
'-' + suffix.downcase
|
40
|
+
end
|
41
|
+
"#{pre}-#{hash}#{suf}"
|
42
|
+
}
|
43
|
+
|
44
|
+
context 'with a prefix' do
|
45
|
+
let(:prefix) { "pre#{double.object_id.to_s[0..5]}" }
|
46
|
+
|
47
|
+
it_behaves_like 'a named specification'
|
48
|
+
|
49
|
+
context 'of mixed case' do
|
50
|
+
let(:prefix) { "PrE#{double.object_id.to_s[0..5]}" }
|
51
|
+
|
52
|
+
it_behaves_like 'a named specification'
|
53
|
+
|
54
|
+
context 'and a suffix' do
|
55
|
+
let(:suffix) { "#{double.object_id.to_s[0..5]}sfx" }
|
56
|
+
|
57
|
+
it_behaves_like 'a named specification'
|
58
|
+
|
59
|
+
context 'of mixed case' do
|
60
|
+
let(:suffix) { "#{double.object_id.to_s[0..5]}SfX" }
|
61
|
+
|
62
|
+
it_behaves_like 'a named specification'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'with a suffix' do
|
69
|
+
let(:suffix) { "#{double.object_id.to_s[1..6]}suffy" }
|
70
|
+
|
71
|
+
it_behaves_like 'a named specification'
|
72
|
+
|
73
|
+
context 'of mixed case' do
|
74
|
+
let(:suffix) { "#{double.object_id.to_s[1..6]}sUFFy" }
|
75
|
+
|
76
|
+
it_behaves_like 'a named specification'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tainers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ethan Rowe
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-11-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: docker-api
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.15.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.15.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.7.6
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.7.6
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 3.1.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.1.0
|
62
|
+
description: Config-driven management of docker containers
|
63
|
+
email: ethan@the-rowes.com
|
64
|
+
executables:
|
65
|
+
- tainers
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- lib/tainers/cli.rb
|
70
|
+
- lib/tainers/specification.rb
|
71
|
+
- lib/tainers/hash.rb
|
72
|
+
- lib/tainers.rb
|
73
|
+
- spec/hashing_spec.rb
|
74
|
+
- spec/commands/exists_spec.rb
|
75
|
+
- spec/commands/ensure_spec.rb
|
76
|
+
- spec/commands/common_examples.rb
|
77
|
+
- spec/commands/name_spec.rb
|
78
|
+
- spec/specification_spec.rb
|
79
|
+
- spec/rspec_helper.rb
|
80
|
+
- spec/specify_spec.rb
|
81
|
+
- bin/tainers
|
82
|
+
- Gemfile
|
83
|
+
- Gemfile.lock
|
84
|
+
homepage: http://github.com/ethanrowe/ruby-tainers
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 1.8.23
|
106
|
+
signing_key:
|
107
|
+
specification_version: 3
|
108
|
+
summary: Manage docker containers based on deterministic naming derived from container
|
109
|
+
configuration
|
110
|
+
test_files: []
|