izanami 0.14.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.
- checksums.yaml +15 -0
- data/.env.development +6 -0
- data/.gitignore +23 -0
- data/Gemfile +8 -0
- data/LICENSE +20 -0
- data/README.md +133 -0
- data/Rakefile +34 -0
- data/assets/css/app.css +1 -0
- data/assets/css/bootstrap.css +6805 -0
- data/assets/css/bootstrap.min.css +9 -0
- data/assets/js/app.js +47 -0
- data/assets/js/bootstrap.js +1999 -0
- data/assets/js/bootstrap.min.js +6 -0
- data/assets/js/jquery-2.0.3.min.js +6 -0
- data/bin/izanami +14 -0
- data/bin/izanami-app +14 -0
- data/bin/izanami-watchdog +12 -0
- data/izanami.gemspec +30 -0
- data/lib/izanami/app.rb +165 -0
- data/lib/izanami/channel.rb +133 -0
- data/lib/izanami/mapper.rb +64 -0
- data/lib/izanami/mappers/command.rb +161 -0
- data/lib/izanami/version.rb +3 -0
- data/lib/izanami/worker.rb +59 -0
- data/lib/izanami/workers/command.rb +82 -0
- data/lib/izanami/workers/watchdog.rb +65 -0
- data/lib/izanami.rb +5 -0
- data/spec/helper.rb +12 -0
- data/spec/izanami/app_spec.rb +195 -0
- data/spec/izanami/channel/input_spec.rb +38 -0
- data/spec/izanami/channel_spec.rb +66 -0
- data/spec/izanami/mapper_spec.rb +48 -0
- data/spec/izanami/mappers/command_spec.rb +154 -0
- data/spec/izanami/workers/command_spec.rb +122 -0
- data/spec/sandbox/Capfile +4 -0
- data/spec/sandbox/Gemfile +3 -0
- data/views/_commands_panel.erb +12 -0
- data/views/command.erb +10 -0
- data/views/home.erb +44 -0
- data/views/layout.erb +32 -0
- metadata +207 -0
@@ -0,0 +1,195 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
require 'izanami/app'
|
3
|
+
|
4
|
+
describe Izanami::App do
|
5
|
+
include Rack::Test::Methods
|
6
|
+
|
7
|
+
def app
|
8
|
+
Izanami::App
|
9
|
+
end
|
10
|
+
|
11
|
+
def worker
|
12
|
+
fork { yield }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Simulate Worker::Command
|
16
|
+
def publish(redis_url, id)
|
17
|
+
redis = Redis.new(url: redis_url)
|
18
|
+
|
19
|
+
# wait for a subscriber to connect to a channel
|
20
|
+
loop do
|
21
|
+
subscribers = redis.info['pubsub_channels']
|
22
|
+
break if subscribers != '0'
|
23
|
+
end
|
24
|
+
|
25
|
+
key = "izanami:commands:#{id}"
|
26
|
+
channel = "izanami:commands:#{id}:channel"
|
27
|
+
output = 'PONG'
|
28
|
+
status = 'success'
|
29
|
+
|
30
|
+
redis.publish channel, output
|
31
|
+
redis.multi do
|
32
|
+
redis.publish channel, '--EOC--'
|
33
|
+
redis.hset key, 'status', status
|
34
|
+
redis.hset key, 'output', output
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def save_commands(redis, commands)
|
39
|
+
commands.each do |command|
|
40
|
+
# set all the values in redis in one step
|
41
|
+
redis.hmset "izanami:commands:#{command['id']}", *command.to_a.flatten
|
42
|
+
redis.sadd "izanami:commands:ids", command['id']
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
let!(:redis_url) { ENV['IZANAMI_REDIS_URL'] }
|
47
|
+
let(:redis) { Redis.new(url: redis_url) }
|
48
|
+
|
49
|
+
let(:command_a) do
|
50
|
+
{
|
51
|
+
'tasks' => 'ping',
|
52
|
+
'id' => '20130816101418',
|
53
|
+
'output' => 'PONG',
|
54
|
+
'status' => 'success',
|
55
|
+
}
|
56
|
+
end
|
57
|
+
let(:command_b) do
|
58
|
+
{
|
59
|
+
'tasks' => 'ping2',
|
60
|
+
'id' => '20130815121400',
|
61
|
+
'output' => 'PONG2',
|
62
|
+
'status' => 'fail',
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
let(:commands) { [command_a, command_b] }
|
67
|
+
let(:user) { ENV['IZANAMI_USER'] }
|
68
|
+
let(:password) { ENV['IZANAMI_PASSWORD'] }
|
69
|
+
|
70
|
+
before { authorize(user, password) }
|
71
|
+
|
72
|
+
after { redis.flushdb }
|
73
|
+
|
74
|
+
describe 'GET /' do
|
75
|
+
context 'when there are no command logs' do
|
76
|
+
before { redis.flushdb }
|
77
|
+
|
78
|
+
it 'shows the homepage' do
|
79
|
+
get '/'
|
80
|
+
|
81
|
+
last_response.should be_ok
|
82
|
+
last_response.body.should include('Command history')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'when there are command logs' do
|
87
|
+
before { save_commands(redis, commands) }
|
88
|
+
|
89
|
+
it 'shows the homepage with the commands' do
|
90
|
+
get '/'
|
91
|
+
|
92
|
+
last_response.should be_ok
|
93
|
+
last_response.body.should include('Command history')
|
94
|
+
commands.each do |command|
|
95
|
+
last_response.body.should include(command['id'])
|
96
|
+
last_response.body.should include(command['tasks'])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe 'GET /commands' do
|
103
|
+
it 'redirects to home page' do
|
104
|
+
get '/commands'
|
105
|
+
|
106
|
+
last_response.should be_redirect
|
107
|
+
last_response.original_headers['Location'].should == 'http://example.org/'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'DELETE /commands' do
|
112
|
+
before { save_commands(redis, commands) }
|
113
|
+
|
114
|
+
it 'removes all commands' do
|
115
|
+
delete '/commands'
|
116
|
+
last_response.should be_redirect
|
117
|
+
|
118
|
+
follow_redirect!
|
119
|
+
|
120
|
+
last_response.should be_ok
|
121
|
+
last_response.body.should include('Command history')
|
122
|
+
commands.each do |command|
|
123
|
+
last_response.body.should_not include(command['id'])
|
124
|
+
last_response.body.should_not include(command['tasks'])
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe 'POST /commands' do
|
130
|
+
before do
|
131
|
+
# we don't want to `defer` any worker here.
|
132
|
+
Process.should_receive(:fork).and_return(123)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'performs the commands' do
|
136
|
+
post '/commands', 'tasks' => 'ping'
|
137
|
+
last_response.should be_redirect
|
138
|
+
|
139
|
+
follow_redirect!
|
140
|
+
last_response.should be_ok
|
141
|
+
last_response.body.should include('ping')
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe 'GET /commands/:id' do
|
146
|
+
let(:id) { command_a['id'] }
|
147
|
+
let(:tasks) { command_a['tasks'] }
|
148
|
+
|
149
|
+
before { save_commands(redis, commands) }
|
150
|
+
|
151
|
+
it 'shows the command' do
|
152
|
+
get "/commands/#{id}"
|
153
|
+
|
154
|
+
last_response.should be_ok
|
155
|
+
last_response.body.should include(tasks)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe 'GET /commands/:id/log' do
|
160
|
+
context 'when the output is already saved' do
|
161
|
+
let(:id) { command_a['id'] }
|
162
|
+
let(:output) { command_a['output'] }
|
163
|
+
|
164
|
+
before { save_commands(redis, commands) }
|
165
|
+
|
166
|
+
it 'gets the log as an event-stream' do
|
167
|
+
get "/commands/#{id}/log"
|
168
|
+
|
169
|
+
last_response.should be_ok
|
170
|
+
last_response.content_type.should include('text/event-stream')
|
171
|
+
last_response.body.should include("data: #{output}\n\n")
|
172
|
+
last_response.body.should include("data: CLOSE\n\n")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context 'when the output is been generated' do
|
177
|
+
let(:command) { { 'id' => '20130716150200', 'tasks' => 'ping' } }
|
178
|
+
let(:id) { command['id'] }
|
179
|
+
|
180
|
+
before { save_commands(redis, [command]) }
|
181
|
+
after { Process.wait(@pid) if @pid }
|
182
|
+
|
183
|
+
it 'gets the log as an event-stream' do
|
184
|
+
@pid = worker { publish(redis_url, id) }
|
185
|
+
|
186
|
+
get "/commands/#{id}/log"
|
187
|
+
|
188
|
+
last_response.should be_ok
|
189
|
+
last_response.content_type.should include('text/event-stream')
|
190
|
+
last_response.body.should include("data: PONG\n\n")
|
191
|
+
last_response.body.should include("data: CLOSE\n\n")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative '../../helper'
|
2
|
+
require 'izanami/channel'
|
3
|
+
|
4
|
+
describe Izanami::Channel::Input do
|
5
|
+
let(:mapper) { double('mapper') }
|
6
|
+
let(:id) { '123' }
|
7
|
+
|
8
|
+
subject { Izanami::Channel::Input.new(mapper, id) }
|
9
|
+
|
10
|
+
describe '#<<' do
|
11
|
+
let(:payload_a) { 'ping' }
|
12
|
+
let(:payload_b) { 'pong' }
|
13
|
+
|
14
|
+
before do
|
15
|
+
mapper.should_receive(:publish).with(id, payload_a)
|
16
|
+
mapper.should_receive(:publish).with(id, payload_b)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'publishes the content and keeps it' do
|
20
|
+
subject << payload_a
|
21
|
+
subject << payload_b
|
22
|
+
|
23
|
+
subject.to_s.
|
24
|
+
should == "#{payload_a}#{Izanami::Channel::SEPARATOR}#{payload_b}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#close' do
|
29
|
+
before do
|
30
|
+
mapper.should_receive(:update).with(id, 'output', '')
|
31
|
+
mapper.should_receive(:publish).with(id, Izanami::Channel::EOC)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'closes the channel, stores and returns the content' do
|
35
|
+
subject.close.should == ''
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
require 'izanami/channel'
|
3
|
+
|
4
|
+
module Mock
|
5
|
+
class On < Struct.new(:buffer)
|
6
|
+
def message
|
7
|
+
buffer.each do |output|
|
8
|
+
yield 'channel-id', output
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
shared_examples 'Channel#read' do
|
15
|
+
it 'reads the command output' do
|
16
|
+
output = []
|
17
|
+
subject.read(id) { |line| output << line }
|
18
|
+
|
19
|
+
output.should == expected
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Izanami::Channel do
|
24
|
+
let(:mapper) { double('mapper') }
|
25
|
+
let(:id) { '123' }
|
26
|
+
|
27
|
+
subject { Izanami::Channel.new(mapper) }
|
28
|
+
|
29
|
+
describe '#write' do
|
30
|
+
it 'initiates a channel output instance' do
|
31
|
+
subject.write(id).should be_instance_of(Izanami::Channel::Input)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#read' do
|
36
|
+
let(:expected) { %w[ping pong] }
|
37
|
+
|
38
|
+
context 'when the command has an output' do
|
39
|
+
let(:command) do
|
40
|
+
{
|
41
|
+
'id' => id,
|
42
|
+
'output' => "ping#{Izanami::Channel::SEPARATOR}pong"
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
before do
|
47
|
+
mapper.should_receive(:find).with(id).and_return(command)
|
48
|
+
end
|
49
|
+
|
50
|
+
it_should_behave_like 'Channel#read'
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when the command is been executed' do
|
54
|
+
let(:command) { { 'id' => id, } }
|
55
|
+
let(:on) { Mock::On.new(%W[ping pong #{Izanami::Channel::EOC}]) }
|
56
|
+
|
57
|
+
before do
|
58
|
+
mapper.should_receive(:find).with(id).and_return(command)
|
59
|
+
mapper.should_receive(:subscribe).with(id).and_yield(on)
|
60
|
+
mapper.should_receive(:unsubscribe).with(id)
|
61
|
+
end
|
62
|
+
|
63
|
+
it_should_behave_like 'Channel#read'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
require 'izanami/mapper'
|
3
|
+
|
4
|
+
describe Izanami::Mapper do
|
5
|
+
let(:redis) { double('redis') }
|
6
|
+
let(:options) { { redis: redis } }
|
7
|
+
|
8
|
+
subject { Izanami::Mapper.new(options) }
|
9
|
+
|
10
|
+
describe '#redis' do
|
11
|
+
context 'with redis instance' do
|
12
|
+
it 'uses the given redis instance' do
|
13
|
+
subject.redis.should == redis
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with redis options' do
|
18
|
+
let(:redis_url) { ENV['IZANAMI_REDIS_URL'] }
|
19
|
+
let(:options) { { url: redis_url } }
|
20
|
+
|
21
|
+
it 'initializes a new redis instance' do
|
22
|
+
subject.redis.should be_instance_of(Redis)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#namespace' do
|
28
|
+
context 'with a namespace' do
|
29
|
+
let(:options) { { namespace: 'spec' } }
|
30
|
+
|
31
|
+
it 'uses that namespace' do
|
32
|
+
subject.namespace.should == 'spec'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'without a namespace' do
|
37
|
+
it 'uses the default namespace' do
|
38
|
+
subject.namespace.should == 'izanami'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#client' do
|
44
|
+
it 'uses redis namespace as a proxy' do
|
45
|
+
subject.client.should be_instance_of(Redis::Namespace)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require_relative '../../helper'
|
2
|
+
require 'izanami/mappers/command'
|
3
|
+
|
4
|
+
describe Izanami::Mappers do
|
5
|
+
let(:redis_url) { ENV['IZANAMI_REDIS_URL'] }
|
6
|
+
let(:redis) { Redis.new(url: redis_url) }
|
7
|
+
|
8
|
+
let(:options) { { redis: redis } }
|
9
|
+
|
10
|
+
let(:commands) do
|
11
|
+
[
|
12
|
+
{ 'id' => '20130804160130', 'tasks' => 'ping', },
|
13
|
+
{ 'id' => '20130804150130', 'tasks' => 'check', },
|
14
|
+
{ 'id' => '20130804140130', 'tasks' => 'deploy', },
|
15
|
+
{ 'id' => '20130803160130', 'tasks' => 'reboot', },
|
16
|
+
{ 'id' => '20130802160130', 'tasks' => 'ping', },
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
subject { Izanami::Mappers::Command.new(options) }
|
21
|
+
|
22
|
+
describe '#namespace' do
|
23
|
+
context 'with a namespace' do
|
24
|
+
let(:options) { { redis: redis, namespace: 'spec' } }
|
25
|
+
|
26
|
+
it 'uses that namespace' do
|
27
|
+
subject.namespace.should == 'spec:commands'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'without a namespace' do
|
32
|
+
it 'uses the default namespace' do
|
33
|
+
subject.namespace.should == 'izanami:commands'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#ttl' do
|
39
|
+
context 'with a ttl option' do
|
40
|
+
let(:options) { { redis: redis, ttl: 1 } }
|
41
|
+
|
42
|
+
it 'uses that ttl' do
|
43
|
+
subject.ttl.should == 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'without a ttl option' do
|
48
|
+
let(:options) { { redis: redis } }
|
49
|
+
|
50
|
+
it 'uses the default ttl' do
|
51
|
+
subject.ttl.should == 604800
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'redis interaction' do
|
57
|
+
before do
|
58
|
+
commands.each do |command|
|
59
|
+
subject.save command
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
after { redis.flushdb }
|
64
|
+
|
65
|
+
describe '#find' do
|
66
|
+
let(:command) { { 'id' => '1234', 'tasks' => 'test' } }
|
67
|
+
|
68
|
+
context 'when the command exists' do
|
69
|
+
before { subject.save(command) }
|
70
|
+
|
71
|
+
it 'retrieves the content' do
|
72
|
+
subject.find(command['id']).should == command
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context "when the command doesn't exist" do
|
77
|
+
it 'returns nil' do
|
78
|
+
subject.find(command['id']).should be_nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#take' do
|
84
|
+
it 'takes the elements sorted by their ids' do
|
85
|
+
subject.take(3).should == commands.take(3)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#count' do
|
90
|
+
it 'counts all the records' do
|
91
|
+
subject.count.should == commands.count
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe '#delete_all' do
|
96
|
+
it 'deletes all the commands stored in redis' do
|
97
|
+
subject.delete_all
|
98
|
+
|
99
|
+
subject.count.should == 0
|
100
|
+
commands.each do |command|
|
101
|
+
subject.exists?(command['id']).should be_false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '#delete' do
|
107
|
+
let(:ids) { commands.take(2).map { |c| c['id'] } }
|
108
|
+
|
109
|
+
it 'deletes the ids stored in redis' do
|
110
|
+
subject.delete(ids)
|
111
|
+
|
112
|
+
ids.each do |id|
|
113
|
+
subject.exists?(id).should be_false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#save' do
|
119
|
+
context 'with an id' do
|
120
|
+
let(:command) { { 'id' => '123', 'tasks' => 'ping' } }
|
121
|
+
|
122
|
+
it 'saves the record with the given key' do
|
123
|
+
subject.save(command)
|
124
|
+
|
125
|
+
subject.find(command['id']).should == command
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'without an id' do
|
130
|
+
let(:command) { { 'tasks' => 'ping' } }
|
131
|
+
|
132
|
+
it 'saves the record with a new key' do
|
133
|
+
hash = subject.save(command)
|
134
|
+
|
135
|
+
hash['id'].should_not be_nil
|
136
|
+
subject.find(hash['id'])['tasks'].should == command['tasks']
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe '#update' do
|
142
|
+
let(:command) { { 'id' => '123', 'tasks' => 'ping' } }
|
143
|
+
|
144
|
+
before { subject.save(command) }
|
145
|
+
|
146
|
+
it 'updates the record with the given key' do
|
147
|
+
subject.update('123', 'tasks', 'pong')
|
148
|
+
|
149
|
+
subject.find('123').should_not == command
|
150
|
+
subject.find('123').should == { 'id' => '123', 'tasks' => 'pong' }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require_relative '../../helper'
|
2
|
+
require 'izanami/workers/command'
|
3
|
+
require 'izanami/mappers/command'
|
4
|
+
|
5
|
+
shared_context 'mapper' do
|
6
|
+
let(:mapper) { Izanami::Mappers::Command.new(redis_options) }
|
7
|
+
|
8
|
+
before { mapper.client.flushdb }
|
9
|
+
after { mapper.client.flushdb }
|
10
|
+
end
|
11
|
+
|
12
|
+
shared_examples '#handle_streams' do
|
13
|
+
include_context 'mapper'
|
14
|
+
|
15
|
+
let(:stdin) { StringIO.new }
|
16
|
+
let(:stdout) { StringIO.new(output) }
|
17
|
+
let(:thread) { Thread.new { double('status', success?: status) } }
|
18
|
+
|
19
|
+
it 'handles the shell command IO objects and stores the result' do
|
20
|
+
subject.handle_streams(stdin, stdout, thread)
|
21
|
+
|
22
|
+
c = mapper.find(command['id'])
|
23
|
+
c['output'].should == output
|
24
|
+
c['status'].should == status_string
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe Izanami::Workers::Command do
|
29
|
+
describe 'worker class methods' do
|
30
|
+
subject { Izanami::Workers::Command }
|
31
|
+
|
32
|
+
describe '.defer' do
|
33
|
+
it { should respond_to(:defer) }
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '.call' do
|
37
|
+
it { should respond_to(:call) }
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '.run' do
|
41
|
+
it { should respond_to(:run) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'worker instance methods' do
|
46
|
+
let(:command) { { 'id' => '123', 'tasks' => 'ping' } }
|
47
|
+
let(:redis_options) { { url: ENV['IZANAMI_REDIS_URL'] } }
|
48
|
+
subject { Izanami::Workers::Command.new(redis_options, command) }
|
49
|
+
|
50
|
+
describe '#defer' do
|
51
|
+
it { should respond_to(:defer) }
|
52
|
+
|
53
|
+
context 'calling #defer' do
|
54
|
+
let(:pid) { 123 }
|
55
|
+
|
56
|
+
it 'forks the process to detach the execution' do
|
57
|
+
Process.should_receive(:fork).and_return(pid)
|
58
|
+
Process.should_receive(:detach).with(pid)
|
59
|
+
subject.defer.should == pid
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#call' do
|
65
|
+
it { should respond_to(:call) }
|
66
|
+
|
67
|
+
context 'calling #call' do
|
68
|
+
include_context 'mapper'
|
69
|
+
|
70
|
+
it 'executes the `cap` command' do
|
71
|
+
subject.call
|
72
|
+
|
73
|
+
c = mapper.find(command['id'])
|
74
|
+
c['output'].should include('PONG')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '#run' do
|
80
|
+
it { should respond_to(:run) }
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#shell_env' do
|
84
|
+
before { ENV['BUNDLE_IZANAMI_STUB'] = 'spec' }
|
85
|
+
|
86
|
+
it 'cleans all bundler environment variables' do
|
87
|
+
subject.shell_env['BUNDLE_IZANAMI_STUB'].should be_nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '#shell_options' do
|
92
|
+
it 'changes the dir for with the sandbox' do
|
93
|
+
subject.shell_options[:chdir].should == ENV['IZANAMI_SANDBOX']
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '#cmd' do
|
98
|
+
it 'calls the `cap` command using `bundler`' do
|
99
|
+
subject.cmd.should == "bundle exec cap #{command['tasks']}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#handle_streams' do
|
104
|
+
|
105
|
+
context 'with successful status' do
|
106
|
+
let(:output) { " * 2013-08-14 15:47:06 executing `ping'`\nPONG" }
|
107
|
+
let(:status) { true }
|
108
|
+
let(:status_string) { 'success' }
|
109
|
+
|
110
|
+
it_should_behave_like '#handle_streams'
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'with failed status' do
|
114
|
+
let(:output) { "the task `ping' does not exist" }
|
115
|
+
let(:status) { false }
|
116
|
+
let(:status_string) { 'fail' }
|
117
|
+
|
118
|
+
it_should_behave_like '#handle_streams'
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<div class="panel panel-<%= type %>">
|
2
|
+
<div class="panel-heading"><%= title %></div>
|
3
|
+
|
4
|
+
<ul class="list-group">
|
5
|
+
<% commands.each do |command| %>
|
6
|
+
<li class="list-group-item">
|
7
|
+
<% time = id_to_time(command['id']) %>
|
8
|
+
<abbr title="<%= time %>" class="initialism"><%= abbr_time(time) %></abbr> <a href="<%= "/commands/#{command['id']}" %>"><%= command['tasks'] %></a>
|
9
|
+
</li>
|
10
|
+
<% end %>
|
11
|
+
</ul>
|
12
|
+
</div>
|
data/views/command.erb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
<div class="row">
|
2
|
+
<div class="col-lg-12">
|
3
|
+
<h2><%= command['tasks'] %> <span class="label label-<%= label_status(command['status']) %>"><%= format_status(command['status']) %></span></h2>
|
4
|
+
<p>Command started at <%= id_to_time(command['id']) %></p>
|
5
|
+
</div>
|
6
|
+
</div>
|
7
|
+
|
8
|
+
<div class="row">
|
9
|
+
<pre id="log" data-id="<%= command['id'] %>" class="col-lg-12"></pre>
|
10
|
+
</div>
|