izanami 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|