navyrb 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/.gitignore +6 -0
- data/.rspec +1 -0
- data/.simplecov +9 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +87 -0
- data/Guardfile +17 -0
- data/LICENSE.txt +22 -0
- data/README.md +28 -0
- data/Rakefile +2 -0
- data/lib/navy.rb +15 -0
- data/lib/navy/app_container_builder.rb +72 -0
- data/lib/navy/application.rb +63 -0
- data/lib/navy/command_builder.rb +73 -0
- data/lib/navy/configuration.rb +102 -0
- data/lib/navy/container.rb +75 -0
- data/lib/navy/container_building.rb +67 -0
- data/lib/navy/etcd.rb +133 -0
- data/lib/navy/logger.rb +53 -0
- data/lib/navy/router.rb +64 -0
- data/lib/navy/runner.rb +17 -0
- data/lib/navy/task_container_builder.rb +70 -0
- data/lib/navy/version.rb +3 -0
- data/navyrb.gemspec +22 -0
- data/spec/lib/navy/app_container_builder_spec.rb +171 -0
- data/spec/lib/navy/application_spec.rb +104 -0
- data/spec/lib/navy/command_builder_spec.rb +85 -0
- data/spec/lib/navy/configuration_spec.rb +165 -0
- data/spec/lib/navy/container_spec.rb +195 -0
- data/spec/lib/navy/etcd_spec.rb +202 -0
- data/spec/lib/navy/router_spec.rb +69 -0
- data/spec/lib/navy/runner_spec.rb +36 -0
- data/spec/lib/navy/task_container_builder_spec.rb +228 -0
- data/spec/spec_helper.rb +93 -0
- data/spec/support/mock_etcd.rb +24 -0
- metadata +144 -0
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Navy::Container do
|
4
|
+
let(:specification) do
|
5
|
+
{
|
6
|
+
:container_name => 'the_container_name',
|
7
|
+
:name => 'the_app_name',
|
8
|
+
:type => "application"
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:dependencies) do
|
13
|
+
['dep1', 'dep2']
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:etcd) do
|
17
|
+
MockEtcd.new
|
18
|
+
end
|
19
|
+
|
20
|
+
subject do
|
21
|
+
described_class.new :specification => specification,
|
22
|
+
:dependencies => dependencies
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#name" do
|
26
|
+
it "is the container_name in the specification" do
|
27
|
+
expect(subject.name).to eq 'the_container_name'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#app" do
|
32
|
+
it "is the app name in the specification" do
|
33
|
+
expect(subject.app).to eq 'the_app_name'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#daemon?" do
|
38
|
+
context "when the type is application" do
|
39
|
+
before :each do
|
40
|
+
specification[:type] = "application"
|
41
|
+
end
|
42
|
+
|
43
|
+
it "is a daemon" do
|
44
|
+
expect(subject.daemon?).to be true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "when the type is task" do
|
49
|
+
before :each do
|
50
|
+
specification[:type] = "task"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "is a daemon" do
|
54
|
+
expect(subject.daemon?).to be false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#can_be_started?" do
|
60
|
+
context "when there are no dependencies" do
|
61
|
+
let(:dependencies) { [] }
|
62
|
+
it "can be started" do
|
63
|
+
expect(subject.can_be_started?(etcd)).to be true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when the dependencies are not in desired state" do
|
68
|
+
before :each do
|
69
|
+
etcd.setJSON('/navy/containers/dep1/desired', {:state => :desired})
|
70
|
+
etcd.setJSON('/navy/containers/dep1/actual', {:state => :not_desired})
|
71
|
+
end
|
72
|
+
|
73
|
+
it "cannot be started" do
|
74
|
+
expect(subject.can_be_started?(etcd)).to be false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when the dependencies are in desired state" do
|
79
|
+
before :each do
|
80
|
+
etcd.setJSON('/navy/containers/dep1/desired', {:state => :desired})
|
81
|
+
etcd.setJSON('/navy/containers/dep1/actual', {:state => :desired})
|
82
|
+
etcd.setJSON('/navy/containers/dep2/desired', {:state => :desired})
|
83
|
+
etcd.setJSON('/navy/containers/dep2/actual', {:state => :desired})
|
84
|
+
end
|
85
|
+
|
86
|
+
it "can be started" do
|
87
|
+
expect(subject.can_be_started?(etcd)).to be true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#can_never_be_started?" do
|
93
|
+
context "when there are no dependencies" do
|
94
|
+
let(:dependencies) { [] }
|
95
|
+
it "can *always* be started" do
|
96
|
+
expect(subject.can_never_be_started?(etcd)).to be false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "when the dependencies exist" do
|
101
|
+
context "when one of the dependencies is errored" do
|
102
|
+
before :each do
|
103
|
+
etcd.setJSON('/navy/containers/dep1/desired', {:state => :desired})
|
104
|
+
etcd.setJSON('/navy/containers/dep1/actual', {:state => :error})
|
105
|
+
end
|
106
|
+
|
107
|
+
it "can *never* be started" do
|
108
|
+
expect(subject.can_never_be_started?(etcd)).to be true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when the dependencies are in non errored states" do
|
113
|
+
before :each do
|
114
|
+
etcd.setJSON('/navy/containers/dep1/desired', {:state => :desired})
|
115
|
+
etcd.setJSON('/navy/containers/dep1/actual', {:state => :not_error})
|
116
|
+
end
|
117
|
+
|
118
|
+
it "can be *potentially* started" do
|
119
|
+
expect(subject.can_never_be_started?(etcd)).to be false
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "#start" do
|
127
|
+
let(:cmd) { Navy::CommandBuilder.new(subject).build }
|
128
|
+
let(:launched) { [] }
|
129
|
+
let(:success) { true }
|
130
|
+
|
131
|
+
before :each do
|
132
|
+
allow(Navy::Runner).to receive(:launch) do |cmd|
|
133
|
+
launched << cmd
|
134
|
+
success
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
it "generates a command for the container and executes it" do
|
139
|
+
subject.start
|
140
|
+
expect(launched).to eq [cmd]
|
141
|
+
end
|
142
|
+
|
143
|
+
context "when the command succeeds" do
|
144
|
+
it "returns true" do
|
145
|
+
expect(subject.start).to be true
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context "when the command fails" do
|
150
|
+
let(:success) { false }
|
151
|
+
it "returns false" do
|
152
|
+
expect(subject.start).to be false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "when it is a task" do
|
157
|
+
before :each do
|
158
|
+
specification[:type] = "task"
|
159
|
+
specification[:cmds] = ["cmd1", "cmd2"]
|
160
|
+
end
|
161
|
+
|
162
|
+
it "runs each command" do
|
163
|
+
subject.start
|
164
|
+
expect(launched[0].last).to eq "cmd1"
|
165
|
+
expect(launched[1].last).to eq "cmd2"
|
166
|
+
end
|
167
|
+
|
168
|
+
context "when one of the items fails" do
|
169
|
+
let(:success) { false }
|
170
|
+
|
171
|
+
it "returns false and does not run other tasks" do
|
172
|
+
expect(subject.start).to be false
|
173
|
+
expect(launched.length).to eq 1
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
describe "#stop" do
|
180
|
+
let(:cmd) { ["docker rm -f", "the_container_name"] }
|
181
|
+
let(:launched) { [] }
|
182
|
+
|
183
|
+
before :each do
|
184
|
+
allow(Navy::Runner).to receive(:launch) do |cmd|
|
185
|
+
launched << cmd
|
186
|
+
true
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
it "generates a rm command for the container and executes it" do
|
191
|
+
subject.stop
|
192
|
+
expect(launched).to eq [cmd]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'webmock/rspec'
|
3
|
+
|
4
|
+
describe Navy::Etcd do
|
5
|
+
let(:options) do
|
6
|
+
{:host => 'etcdhost', :port => '1234'}
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:example_record) do
|
10
|
+
<<-JSON
|
11
|
+
{
|
12
|
+
"action":"The Action",
|
13
|
+
"node": {
|
14
|
+
"createdIndex": 1,
|
15
|
+
"key": "/akey",
|
16
|
+
"modifiedIndex": 2,
|
17
|
+
"value": "New Value"
|
18
|
+
},
|
19
|
+
"prevNode": {
|
20
|
+
"createdIndex": 1,
|
21
|
+
"key": "/akey",
|
22
|
+
"value": "Prev Value",
|
23
|
+
"modifiedIndex": 3
|
24
|
+
}
|
25
|
+
}
|
26
|
+
JSON
|
27
|
+
end
|
28
|
+
|
29
|
+
let(:example_json_record) do
|
30
|
+
<<-JSON
|
31
|
+
{
|
32
|
+
"action":"The Action",
|
33
|
+
"node": {
|
34
|
+
"createdIndex": 1,
|
35
|
+
"key": "/akey",
|
36
|
+
"modifiedIndex": 2,
|
37
|
+
"value": "{\\"key\\":\\"value\\"}"
|
38
|
+
},
|
39
|
+
"prevNode": {
|
40
|
+
"createdIndex": 1,
|
41
|
+
"key": "/akey",
|
42
|
+
"value": "Prev Value",
|
43
|
+
"modifiedIndex": 3
|
44
|
+
}
|
45
|
+
}
|
46
|
+
JSON
|
47
|
+
end
|
48
|
+
|
49
|
+
let(:example_headers) do
|
50
|
+
{
|
51
|
+
"X-Etcd-Index" => "999"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
subject { described_class.client options }
|
56
|
+
|
57
|
+
describe "Retrieval" do
|
58
|
+
describe "#get" do
|
59
|
+
before :each do
|
60
|
+
@request = stub_request(:get, 'http://etcdhost:1234/v2/keys/some/key').
|
61
|
+
to_return(:body => example_record, :headers => example_headers)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "fetches a response from the given key" do
|
65
|
+
record = subject.get('/some/key')
|
66
|
+
|
67
|
+
#expect(@request).to have_been_made
|
68
|
+
|
69
|
+
expect(record.key).to eq '/akey'
|
70
|
+
expect(record.etcd_index).to be 999
|
71
|
+
expect(record.action).to eq "The Action"
|
72
|
+
expect(record.node.createdIndex).to eq 1
|
73
|
+
expect(record.node.modifiedIndex).to eq 2
|
74
|
+
expect(record.node.value).to eq "New Value"
|
75
|
+
expect(record.prevNode.value).to eq "Prev Value"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#getJSON" do
|
80
|
+
before :each do
|
81
|
+
@request = stub_request(:get, 'http://etcdhost:1234/v2/keys/some/key').
|
82
|
+
to_return(:body => example_json_record, :headers => example_headers)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "fetches and parses as JSON the given jey" do
|
86
|
+
hash = subject.getJSON('/some/key')
|
87
|
+
#expect(@request).to have_been_made
|
88
|
+
|
89
|
+
expect(hash['key']).to eq 'value'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "#watch" do
|
94
|
+
before :each do
|
95
|
+
@request = stub_request(:get, 'http://etcdhost:1234/v2/keys/some/key?wait=true').
|
96
|
+
to_return(:body => example_record, :headers => example_headers)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "fetches with a wait" do
|
100
|
+
record = subject.watch('/some/key')
|
101
|
+
#expect(@request).to have_been_made
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "#ls" do
|
106
|
+
let(:exampledir) do
|
107
|
+
{
|
108
|
+
:node => {
|
109
|
+
:nodes => [
|
110
|
+
{:key => '/some/dir/item1'},
|
111
|
+
{:key => '/some/dir/item2'}
|
112
|
+
]
|
113
|
+
}
|
114
|
+
}.to_json
|
115
|
+
end
|
116
|
+
|
117
|
+
before :each do
|
118
|
+
@request = stub_request(:get, 'http://etcdhost:1234/v2/keys/some/dir').
|
119
|
+
to_return(:body => exampledir)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "returns the keys in the given directory" do
|
123
|
+
keys = subject.ls('/some/dir')
|
124
|
+
#expect(@request).to have_been_made
|
125
|
+
expect(keys).to include '/some/dir/item1'
|
126
|
+
expect(keys).to include '/some/dir/item2'
|
127
|
+
expect(keys.length).to eq 2
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "Storage" do
|
134
|
+
describe "#setJSON" do
|
135
|
+
let(:data) do
|
136
|
+
data = {:some => :json, :data => [:here]}
|
137
|
+
end
|
138
|
+
|
139
|
+
before :each do
|
140
|
+
@request = stub_request(:put, 'http://etcdhost:1234/v2/keys/some/json/key').
|
141
|
+
with(:body => {:value => data.to_json})
|
142
|
+
end
|
143
|
+
|
144
|
+
it "stores JSON encoded value at given key" do
|
145
|
+
subject.setJSON('/some/json/key', data)
|
146
|
+
#expect(@request).to have_been_made
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "#delete" do
|
151
|
+
before :each do
|
152
|
+
@request = stub_request(:delete, 'http://etcdhost:1234/v2/keys/some/key?with=param').to_return(:status => 202)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "deletes the specified key" do
|
156
|
+
result = subject.delete('/some/key', :with => :param)
|
157
|
+
expect(@request).to have_been_made
|
158
|
+
expect(result).to be true
|
159
|
+
end
|
160
|
+
|
161
|
+
context "when the key is missing" do
|
162
|
+
before :each do
|
163
|
+
@request = stub_request(:delete, 'http://etcdhost:1234/v2/keys/some/key?with=param').to_return(:status => 404)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "returns false" do
|
167
|
+
result = subject.delete('/some/key', :with => :param)
|
168
|
+
expect(result).to be false
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe "#set" do
|
174
|
+
let(:data) { "some data " }
|
175
|
+
|
176
|
+
before :each do
|
177
|
+
@request = stub_request(:put, 'http://etcdhost:1234/v2/keys/some/data/value').
|
178
|
+
with(:body => {:value => data })
|
179
|
+
end
|
180
|
+
|
181
|
+
it "stores the string at the given value" do
|
182
|
+
subject.set('/some/data/value', data)
|
183
|
+
#expect(@request).to have_been_made
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "#queueJSON" do
|
188
|
+
let(:data) { {:some => :json } }
|
189
|
+
|
190
|
+
before :each do
|
191
|
+
@request = stub_request(:post, 'http://etcdhost:1234/v2/keys/some/queue').
|
192
|
+
with(:body => {:value => data.to_json })
|
193
|
+
end
|
194
|
+
|
195
|
+
it "stores the string at the given value" do
|
196
|
+
subject.queueJSON('/some/queue', data)
|
197
|
+
expect(@request).to have_been_made
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Navy::Router do
|
4
|
+
class ExampleHandler
|
5
|
+
def self.handled
|
6
|
+
@handled ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def handle_set(params, request)
|
10
|
+
self.class.handled[:set] = params, request
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle_delete(params, request)
|
14
|
+
self.class.handled[:delete] = params, request
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:handler1) { Class.new(ExampleHandler) }
|
20
|
+
let(:handler2) { Class.new(ExampleHandler) }
|
21
|
+
|
22
|
+
subject do
|
23
|
+
described_class.new do |r|
|
24
|
+
r.route '^/a/route$', handler1
|
25
|
+
r.route '^/a/:pattern/route$', handler2
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "routes through to defined handlers" do
|
30
|
+
request = double :key => '/a/route',
|
31
|
+
:action => :set
|
32
|
+
|
33
|
+
subject.route(request)
|
34
|
+
expect(handler1.handled[:set]).to eq [{}, request]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "matches placeholder patters" do
|
38
|
+
request = double :key => '/a/matching/route',
|
39
|
+
:action => :delete
|
40
|
+
|
41
|
+
subject.route(request)
|
42
|
+
expect(handler2.handled[:delete]).to eq [{'pattern' => 'matching'}, request]
|
43
|
+
end
|
44
|
+
|
45
|
+
it "gracefully handles unknown paths" do
|
46
|
+
request = double :key => '/unkonwn/path',
|
47
|
+
:action => :delete
|
48
|
+
expect { subject.route(request) }.to_not raise_error
|
49
|
+
end
|
50
|
+
|
51
|
+
it "gracefully handle unmapped action" do
|
52
|
+
request = double :key => '/a/route',
|
53
|
+
:action => :unmapped
|
54
|
+
expect { subject.route(request) }.to_not raise_error
|
55
|
+
end
|
56
|
+
|
57
|
+
it "passes down global options into the params" do
|
58
|
+
options = {:foo => :bar}
|
59
|
+
router = described_class.new(options) do |r|
|
60
|
+
r.route 'example', handler1
|
61
|
+
end
|
62
|
+
|
63
|
+
request = double :key => 'example', :action => :set
|
64
|
+
|
65
|
+
router.route(request)
|
66
|
+
|
67
|
+
expect(handler1.handled[:set][0]).to eq({:foo => :bar})
|
68
|
+
end
|
69
|
+
end
|