foreman_openbolt 1.2.0 → 1.3.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 +4 -4
- data/README.md +1 -0
- data/Rakefile +32 -0
- data/app/controllers/api/v2/openbolt_jobs_controller.rb +77 -0
- data/app/controllers/api/v2/openbolt_tasks_controller.rb +83 -0
- data/app/controllers/concerns/foreman_openbolt/common.rb +121 -0
- data/app/controllers/concerns/foreman_openbolt/jobs.rb +84 -0
- data/app/controllers/concerns/foreman_openbolt/tasks.rb +140 -0
- data/app/controllers/foreman_openbolt/task_controller.rb +12 -291
- data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +4 -2
- data/app/lib/actions/foreman_openbolt/poll_task_status.rb +48 -29
- data/app/models/foreman_openbolt/task_job.rb +9 -2
- data/config/routes.rb +39 -4
- data/db/migrate/20260824000000_remove_view_smart_proxies_openbolt_permission.rb +18 -0
- data/lib/foreman_openbolt/engine.rb +15 -7
- data/lib/foreman_openbolt/version.rb +1 -1
- data/lib/proxy_api/openbolt.rb +64 -13
- data/package.json +1 -1
- data/test/acceptance/acceptance_helper.rb +13 -0
- data/test/acceptance/api_acceptance_helper.rb +109 -0
- data/test/acceptance/tests/api/authn_authz_test.rb +82 -0
- data/test/acceptance/tests/api/jobs_test.rb +77 -0
- data/test/acceptance/tests/api/launch_test.rb +79 -0
- data/test/acceptance/tests/api/proxy_tasks_test.rb +51 -0
- data/test/unit/controllers/api/v2/openbolt_jobs_controller_test.rb +251 -0
- data/test/unit/controllers/api/v2/openbolt_tasks_controller_test.rb +438 -0
- data/test/unit/controllers/task_controller_test.rb +197 -39
- data/test/unit/docker/Dockerfile +1 -0
- data/test/unit/lib/actions/poll_task_status_test.rb +112 -34
- data/test/unit/lib/proxy_api/openbolt_test.rb +121 -6
- data/test/unit/models/task_job_test.rb +11 -0
- data/webpack/src/Components/LaunchTask/__tests__/LaunchTask.test.js +44 -1
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +3 -0
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useTasksData.test.js +5 -4
- data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +1 -1
- data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +4 -1
- data/webpack/src/Components/LaunchTask/index.js +21 -6
- data/webpack/src/Components/TaskExecution/__tests__/TaskExecution.test.js +1 -1
- data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +8 -8
- data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +3 -3
- data/webpack/src/Components/TaskHistory/__tests__/TaskHistory.test.js +6 -6
- data/webpack/src/Components/TaskHistory/index.js +3 -3
- metadata +17 -4
|
@@ -16,34 +16,35 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
16
16
|
WebMock.reset!
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
context '
|
|
19
|
+
context 'tasks' do
|
|
20
20
|
test 'returns tasks from proxy as JSON' do
|
|
21
21
|
tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
22
22
|
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
23
23
|
.to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
24
24
|
|
|
25
|
-
get :
|
|
25
|
+
get :tasks, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
26
26
|
assert_response :success
|
|
27
27
|
assert_equal tasks, JSON.parse(response.body)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
test 'returns error when
|
|
31
|
-
get :
|
|
30
|
+
test 'returns error when smart_proxy_id is missing' do
|
|
31
|
+
get :tasks, session: @session
|
|
32
32
|
assert_response :bad_request
|
|
33
33
|
body = JSON.parse(response.body)
|
|
34
|
-
assert_match(/Smart Proxy ID is required/, body['error'])
|
|
34
|
+
assert_match(/Smart Proxy ID is required/, body['error']['message'])
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
test 'returns error when proxy not found' do
|
|
38
|
-
get :
|
|
38
|
+
get :tasks, params: { smart_proxy_id: -1 }, session: @session
|
|
39
39
|
assert_response :not_found
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
test 'returns
|
|
42
|
+
test 'returns bad_gateway when proxy is unreachable' do
|
|
43
43
|
stub_request(:get, "#{@proxy.url}/openbolt/tasks").to_timeout
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
# Transport failures wrap as ProxyException and render 502.
|
|
46
|
+
get :tasks, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
47
|
+
assert_response :bad_gateway
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -53,7 +54,7 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
53
54
|
stub_request(:get, "#{@proxy.url}/openbolt/tasks/reload")
|
|
54
55
|
.to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
post :reload_tasks, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
57
58
|
assert_response :success
|
|
58
59
|
assert_equal tasks, JSON.parse(response.body)
|
|
59
60
|
end
|
|
@@ -72,16 +73,17 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
72
73
|
|
|
73
74
|
test 'launches task and returns job_id' do
|
|
74
75
|
post :launch_task, params: {
|
|
75
|
-
|
|
76
|
+
smart_proxy_id: @proxy.id,
|
|
76
77
|
task_name: 'mymod::install',
|
|
77
78
|
targets: 'host1.example.com',
|
|
78
|
-
|
|
79
|
+
parameters: { 'name' => 'nginx' },
|
|
79
80
|
options: { 'transport' => 'ssh' },
|
|
80
81
|
}, session: @session
|
|
81
82
|
|
|
82
83
|
assert_response :success
|
|
83
84
|
body = JSON.parse(response.body)
|
|
84
85
|
assert_equal 'launched-job-1', body['job_id']
|
|
86
|
+
assert_equal 'task', body['kind']
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
test 'schedules polling after creating task' do
|
|
@@ -92,7 +94,7 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
92
94
|
)
|
|
93
95
|
|
|
94
96
|
post :launch_task, params: {
|
|
95
|
-
|
|
97
|
+
smart_proxy_id: @proxy.id,
|
|
96
98
|
task_name: 'mymod::install',
|
|
97
99
|
targets: 'host1.example.com',
|
|
98
100
|
}, session: @session
|
|
@@ -103,7 +105,7 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
103
105
|
test 'creates a TaskJob record' do
|
|
104
106
|
assert_difference('ForemanOpenbolt::TaskJob.count', 1) do
|
|
105
107
|
post :launch_task, params: {
|
|
106
|
-
|
|
108
|
+
smart_proxy_id: @proxy.id,
|
|
107
109
|
task_name: 'mymod::install',
|
|
108
110
|
targets: 'host1.example.com,host2.example.com',
|
|
109
111
|
}, session: @session
|
|
@@ -116,15 +118,15 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
test 'returns error when task_name is missing' do
|
|
119
|
-
post :launch_task, params: {
|
|
121
|
+
post :launch_task, params: { smart_proxy_id: @proxy.id, targets: 'host1' }, session: @session
|
|
120
122
|
assert_response :bad_request
|
|
121
|
-
assert_match(/
|
|
123
|
+
assert_match(/Task name and targets cannot be empty/, JSON.parse(response.body)['error']['message'])
|
|
122
124
|
end
|
|
123
125
|
|
|
124
126
|
test 'returns error when targets is missing' do
|
|
125
|
-
post :launch_task, params: {
|
|
127
|
+
post :launch_task, params: { smart_proxy_id: @proxy.id, task_name: 'test::task' }, session: @session
|
|
126
128
|
assert_response :bad_request
|
|
127
|
-
assert_match(/targets/, JSON.parse(response.body)['error'])
|
|
129
|
+
assert_match(/Task name and targets cannot be empty/, JSON.parse(response.body)['error']['message'])
|
|
128
130
|
end
|
|
129
131
|
|
|
130
132
|
test 'returns error when proxy returns error in response' do
|
|
@@ -133,12 +135,12 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
133
135
|
headers: { 'Content-Type' => 'application/json' })
|
|
134
136
|
|
|
135
137
|
post :launch_task, params: {
|
|
136
|
-
|
|
138
|
+
smart_proxy_id: @proxy.id,
|
|
137
139
|
task_name: 'missing::task',
|
|
138
140
|
targets: 'host1',
|
|
139
141
|
}, session: @session
|
|
140
142
|
assert_response :bad_request
|
|
141
|
-
assert_match(/Task execution failed/, JSON.parse(response.body)['error'])
|
|
143
|
+
assert_match(/Task execution failed/, JSON.parse(response.body)['error']['message'])
|
|
142
144
|
end
|
|
143
145
|
|
|
144
146
|
test 'returns bad_gateway when proxy returns invalid JSON' do
|
|
@@ -147,12 +149,12 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
147
149
|
headers: { 'Content-Type' => 'application/json' })
|
|
148
150
|
|
|
149
151
|
post :launch_task, params: {
|
|
150
|
-
|
|
152
|
+
smart_proxy_id: @proxy.id,
|
|
151
153
|
task_name: 'mymod::install',
|
|
152
154
|
targets: 'host1',
|
|
153
155
|
}, session: @session
|
|
154
156
|
assert_response :bad_gateway
|
|
155
|
-
assert_match(/Smart Proxy error/, JSON.parse(response.body)['error'])
|
|
157
|
+
assert_match(/Smart Proxy error/, JSON.parse(response.body)['error']['message'])
|
|
156
158
|
end
|
|
157
159
|
|
|
158
160
|
test 'returns error when proxy returns no job ID' do
|
|
@@ -161,12 +163,55 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
161
163
|
headers: { 'Content-Type' => 'application/json' })
|
|
162
164
|
|
|
163
165
|
post :launch_task, params: {
|
|
164
|
-
|
|
166
|
+
smart_proxy_id: @proxy.id,
|
|
165
167
|
task_name: 'test::task',
|
|
166
168
|
targets: 'host1',
|
|
167
169
|
}, session: @session
|
|
168
170
|
assert_response :bad_request
|
|
169
|
-
assert_match(/No job ID returned/, JSON.parse(response.body)['error'])
|
|
171
|
+
assert_match(/No job ID returned/, JSON.parse(response.body)['error']['message'])
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
test 'rejects whitespace-only task_name and targets' do
|
|
175
|
+
post :launch_task, params: {
|
|
176
|
+
smart_proxy_id: @proxy.id,
|
|
177
|
+
task_name: ' ',
|
|
178
|
+
targets: ' ',
|
|
179
|
+
}, session: @session
|
|
180
|
+
assert_response :bad_request
|
|
181
|
+
assert_match(/Task name and targets cannot be empty/,
|
|
182
|
+
JSON.parse(response.body)['error']['message'])
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
test 'returns 500 with error shape when TaskJob persistence fails' do
|
|
186
|
+
ForemanOpenbolt::TaskJob.stubs(:create_from_execution!).raises(
|
|
187
|
+
ActiveRecord::RecordInvalid.new(ForemanOpenbolt::TaskJob.new)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
post :launch_task, params: {
|
|
191
|
+
smart_proxy_id: @proxy.id,
|
|
192
|
+
task_name: 'mymod::install',
|
|
193
|
+
targets: 'host1.example.com',
|
|
194
|
+
}, session: @session
|
|
195
|
+
|
|
196
|
+
assert_response :internal_server_error
|
|
197
|
+
body = JSON.parse(response.body)
|
|
198
|
+
assert_match(/launched-job-1/, body['error']['message'])
|
|
199
|
+
assert_match(/could not.*record/i, body['error']['message'])
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
test 'returns 500 with error shape when async polling cannot be scheduled' do
|
|
203
|
+
ForemanTasks.stubs(:async_task).raises(StandardError, 'dynflow down')
|
|
204
|
+
|
|
205
|
+
post :launch_task, params: {
|
|
206
|
+
smart_proxy_id: @proxy.id,
|
|
207
|
+
task_name: 'mymod::install',
|
|
208
|
+
targets: 'host1.example.com',
|
|
209
|
+
}, session: @session
|
|
210
|
+
|
|
211
|
+
assert_response :internal_server_error
|
|
212
|
+
body = JSON.parse(response.body)
|
|
213
|
+
assert_match(/launched-job-1/, body['error']['message'])
|
|
214
|
+
assert_match(/background polling could not be scheduled/i, body['error']['message'])
|
|
170
215
|
end
|
|
171
216
|
end
|
|
172
217
|
|
|
@@ -179,10 +224,11 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
179
224
|
|
|
180
225
|
body = JSON.parse(response.body)
|
|
181
226
|
assert_equal 'running', body['status']
|
|
182
|
-
assert_equal job.task_name, body['
|
|
227
|
+
assert_equal job.task_name, body['name']
|
|
183
228
|
assert_equal job.targets, body['targets']
|
|
184
229
|
assert_equal @proxy.id, body['smart_proxy']['id']
|
|
185
230
|
assert_equal @proxy.name, body['smart_proxy']['name']
|
|
231
|
+
assert_equal 'task', body['kind']
|
|
186
232
|
end
|
|
187
233
|
|
|
188
234
|
test 'returns error when job_id is missing' do
|
|
@@ -209,6 +255,7 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
209
255
|
assert_equal job.result, body['value']
|
|
210
256
|
assert_equal job.log, body['log']
|
|
211
257
|
assert_equal job.command, body['command']
|
|
258
|
+
assert_equal 'task', body['kind']
|
|
212
259
|
end
|
|
213
260
|
|
|
214
261
|
test 'returns not_found when job does not exist' do
|
|
@@ -232,7 +279,7 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
232
279
|
Setting['openbolt_password'] = 'real-secret-password'
|
|
233
280
|
|
|
234
281
|
post :launch_task, params: {
|
|
235
|
-
|
|
282
|
+
smart_proxy_id: @proxy.id,
|
|
236
283
|
task_name: 'mymod::install',
|
|
237
284
|
targets: 'host1.example.com',
|
|
238
285
|
options: { 'password' => '[Use saved encrypted default]', 'transport' => 'ssh' },
|
|
@@ -252,19 +299,19 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
252
299
|
|
|
253
300
|
test 'returns error when encrypted placeholder used for nonexistent setting' do
|
|
254
301
|
post :launch_task, params: {
|
|
255
|
-
|
|
302
|
+
smart_proxy_id: @proxy.id,
|
|
256
303
|
task_name: 'mymod::install',
|
|
257
304
|
targets: 'host1.example.com',
|
|
258
305
|
options: { 'nonexistent-option' => '[Use saved encrypted default]' },
|
|
259
306
|
}, session: @session
|
|
260
307
|
|
|
261
308
|
assert_response :bad_request
|
|
262
|
-
assert_match(/No saved value for encrypted option/, JSON.parse(response.body)['error'])
|
|
309
|
+
assert_match(/No saved value for encrypted option/, JSON.parse(response.body)['error']['message'])
|
|
263
310
|
end
|
|
264
311
|
|
|
265
312
|
test 'passes non-encrypted options through unchanged' do
|
|
266
313
|
post :launch_task, params: {
|
|
267
|
-
|
|
314
|
+
smart_proxy_id: @proxy.id,
|
|
268
315
|
task_name: 'mymod::install',
|
|
269
316
|
targets: 'host1.example.com',
|
|
270
317
|
options: { 'transport' => 'ssh', 'user' => 'admin' },
|
|
@@ -277,7 +324,7 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
277
324
|
end
|
|
278
325
|
end
|
|
279
326
|
|
|
280
|
-
context '
|
|
327
|
+
context 'task_options with settings defaults' do
|
|
281
328
|
test 'merges Foreman setting defaults into proxy options' do
|
|
282
329
|
Setting['openbolt_transport'] = 'winrm'
|
|
283
330
|
Setting['openbolt_user'] = 'admin'
|
|
@@ -290,7 +337,7 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
290
337
|
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
291
338
|
.to_return(status: 200, body: proxy_options.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
292
339
|
|
|
293
|
-
get :
|
|
340
|
+
get :task_options, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
294
341
|
assert_response :success
|
|
295
342
|
|
|
296
343
|
body = JSON.parse(response.body)
|
|
@@ -308,15 +355,52 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
308
355
|
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
309
356
|
.to_return(status: 200, body: proxy_options.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
310
357
|
|
|
311
|
-
get :
|
|
358
|
+
get :task_options, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
312
359
|
assert_response :success
|
|
313
360
|
|
|
314
361
|
body = JSON.parse(response.body)
|
|
315
362
|
assert_equal '[Use saved encrypted default]', body['password']['default']
|
|
316
363
|
end
|
|
364
|
+
|
|
365
|
+
test 'omits default when a non-encrypted setting is set to empty string' do
|
|
366
|
+
# An empty-string setting must not override the proxy's real default.
|
|
367
|
+
Setting['openbolt_user'] = ''
|
|
368
|
+
|
|
369
|
+
proxy_options = {
|
|
370
|
+
'user' => { 'type' => 'string', 'default' => 'bolt' },
|
|
371
|
+
}
|
|
372
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
373
|
+
.to_return(status: 200, body: proxy_options.to_json,
|
|
374
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
375
|
+
|
|
376
|
+
get :task_options, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
377
|
+
assert_response :success
|
|
378
|
+
|
|
379
|
+
body = JSON.parse(response.body)
|
|
380
|
+
# Proxy default ('bolt') stays untouched. Empty Foreman setting doesn't override.
|
|
381
|
+
assert_equal 'bolt', body['user']['default']
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
test 'omits encrypted placeholder when an encrypted setting is set to empty string' do
|
|
385
|
+
Setting['openbolt_password'] = ''
|
|
386
|
+
|
|
387
|
+
proxy_options = {
|
|
388
|
+
'password' => { 'type' => 'string', 'sensitive' => true },
|
|
389
|
+
}
|
|
390
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
391
|
+
.to_return(status: 200, body: proxy_options.to_json,
|
|
392
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
393
|
+
|
|
394
|
+
get :task_options, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
395
|
+
assert_response :success
|
|
396
|
+
|
|
397
|
+
body = JSON.parse(response.body)
|
|
398
|
+
assert_not body['password'].key?('default'),
|
|
399
|
+
"expected 'password' to have no 'default' key when the setting is empty"
|
|
400
|
+
end
|
|
317
401
|
end
|
|
318
402
|
|
|
319
|
-
context '
|
|
403
|
+
context 'task_options with Choria settings defaults' do
|
|
320
404
|
# Mirrors the real GET /openbolt/tasks/options response for Choria
|
|
321
405
|
# (see smart_proxy_openbolt/lib/smart_proxy_openbolt/main.rb OPENBOLT_OPTIONS).
|
|
322
406
|
def self.choria_proxy_options
|
|
@@ -356,7 +440,7 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
356
440
|
.to_return(status: 200, body: self.class.choria_proxy_options.to_json,
|
|
357
441
|
headers: { 'Content-Type' => 'application/json' })
|
|
358
442
|
|
|
359
|
-
get :
|
|
443
|
+
get :task_options, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
360
444
|
assert_response :success
|
|
361
445
|
|
|
362
446
|
body = JSON.parse(response.body)
|
|
@@ -380,7 +464,7 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
380
464
|
.to_return(status: 200, body: self.class.choria_proxy_options.to_json,
|
|
381
465
|
headers: { 'Content-Type' => 'application/json' })
|
|
382
466
|
|
|
383
|
-
get :
|
|
467
|
+
get :task_options, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
384
468
|
assert_response :success
|
|
385
469
|
|
|
386
470
|
body = JSON.parse(response.body)
|
|
@@ -393,11 +477,11 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
393
477
|
end
|
|
394
478
|
end
|
|
395
479
|
|
|
396
|
-
context '
|
|
480
|
+
context 'jobs' do
|
|
397
481
|
test 'returns paginated task history' do
|
|
398
482
|
3.times { FactoryBot.create(:task_job, smart_proxy: @proxy) }
|
|
399
483
|
|
|
400
|
-
get :
|
|
484
|
+
get :jobs, params: { page: 1, per_page: 2 }, session: @session
|
|
401
485
|
assert_response :success
|
|
402
486
|
|
|
403
487
|
body = JSON.parse(response.body)
|
|
@@ -405,10 +489,11 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
405
489
|
assert_equal 3, body['total']
|
|
406
490
|
assert_equal 1, body['page']
|
|
407
491
|
assert_equal 2, body['per_page']
|
|
492
|
+
assert(body['results'].all? { |row| row['kind'] == 'task' })
|
|
408
493
|
end
|
|
409
494
|
|
|
410
495
|
test 'caps per_page at 100' do
|
|
411
|
-
get :
|
|
496
|
+
get :jobs, params: { per_page: 200 }, session: @session
|
|
412
497
|
assert_response :success
|
|
413
498
|
|
|
414
499
|
body = JSON.parse(response.body)
|
|
@@ -416,11 +501,84 @@ class TaskControllerTest < ActionController::TestCase
|
|
|
416
501
|
end
|
|
417
502
|
|
|
418
503
|
test 'defaults per_page to 20' do
|
|
419
|
-
get :
|
|
504
|
+
get :jobs, session: @session
|
|
420
505
|
assert_response :success
|
|
421
506
|
|
|
422
507
|
body = JSON.parse(response.body)
|
|
423
508
|
assert_equal 20, body['per_page']
|
|
424
509
|
end
|
|
510
|
+
|
|
511
|
+
test 'floors per_page at 1 when zero is requested' do
|
|
512
|
+
get :jobs, params: { per_page: 0 }, session: @session
|
|
513
|
+
assert_response :success
|
|
514
|
+
|
|
515
|
+
body = JSON.parse(response.body)
|
|
516
|
+
assert_equal 1, body['per_page']
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
test 'floors per_page at 1 when a negative value is requested' do
|
|
520
|
+
get :jobs, params: { per_page: -5 }, session: @session
|
|
521
|
+
assert_response :success
|
|
522
|
+
|
|
523
|
+
body = JSON.parse(response.body)
|
|
524
|
+
assert_equal 1, body['per_page']
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
test 'floors page at 1 when zero is requested' do
|
|
528
|
+
get :jobs, params: { page: 0 }, session: @session
|
|
529
|
+
assert_response :success
|
|
530
|
+
|
|
531
|
+
body = JSON.parse(response.body)
|
|
532
|
+
assert_equal 1, body['page']
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
test "per_page='all' returns every job in a single page" do
|
|
536
|
+
4.times { FactoryBot.create(:task_job, smart_proxy: @proxy) }
|
|
537
|
+
|
|
538
|
+
get :jobs, params: { per_page: 'all' }, session: @session
|
|
539
|
+
assert_response :success
|
|
540
|
+
|
|
541
|
+
body = JSON.parse(response.body)
|
|
542
|
+
assert_equal 4, body['results'].length
|
|
543
|
+
assert_equal 4, body['per_page']
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
test "per_page='all' on empty DB does not 500" do
|
|
547
|
+
# Guards against will_paginate rejecting per_page: 0 when the table
|
|
548
|
+
# is empty. paginated_task_jobs floors the count at 1.
|
|
549
|
+
get :jobs, params: { per_page: 'all' }, session: @session
|
|
550
|
+
assert_response :success
|
|
551
|
+
|
|
552
|
+
body = JSON.parse(response.body)
|
|
553
|
+
assert_equal [], body['results']
|
|
554
|
+
assert_equal 0, body['total']
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
context 'unexpected errors' do
|
|
559
|
+
test 'renders a generic 500 without the exception message' do
|
|
560
|
+
ForemanOpenbolt::TaskJob.stubs(:includes).raises(StandardError, 'secret internals')
|
|
561
|
+
|
|
562
|
+
get :jobs, session: @session
|
|
563
|
+
assert_response :internal_server_error
|
|
564
|
+
|
|
565
|
+
body = JSON.parse(response.body)
|
|
566
|
+
assert_equal 'Internal server error', body['error']['message']
|
|
567
|
+
assert_no_match(/secret internals/, response.body)
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
context 'authorization' do
|
|
572
|
+
test 'forbids unprivileged users from launching tasks' do
|
|
573
|
+
unprivileged = FactoryBot.create(:user)
|
|
574
|
+
User.current = unprivileged
|
|
575
|
+
|
|
576
|
+
post :launch_task, params: {
|
|
577
|
+
smart_proxy_id: @proxy.id,
|
|
578
|
+
task_name: 'mymod::install',
|
|
579
|
+
targets: 'host1.example.com',
|
|
580
|
+
}, session: set_session_user(unprivileged)
|
|
581
|
+
assert_response :forbidden
|
|
582
|
+
end
|
|
425
583
|
end
|
|
426
584
|
end
|
data/test/unit/docker/Dockerfile
CHANGED
|
@@ -5,38 +5,6 @@ require 'test_plugin_helper'
|
|
|
5
5
|
class PollTaskStatusTest < ForemanOpenbolt::PluginTestCase
|
|
6
6
|
include Dynflow::Testing
|
|
7
7
|
|
|
8
|
-
context 'extract_proxy_error' do
|
|
9
|
-
setup do
|
|
10
|
-
@action = create_action(Actions::ForemanOpenbolt::PollTaskStatus)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
test 'returns nil for nil response' do
|
|
14
|
-
assert_nil @action.extract_proxy_error(nil)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
test 'returns nil when no error key' do
|
|
18
|
-
assert_nil @action.extract_proxy_error({ 'status' => 'running' })
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
test 'returns nil for empty error string' do
|
|
22
|
-
assert_nil @action.extract_proxy_error({ 'error' => '' })
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
test 'returns string error directly' do
|
|
26
|
-
assert_equal 'something broke', @action.extract_proxy_error({ 'error' => 'something broke' })
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
test 'returns message from hash error' do
|
|
30
|
-
assert_equal 'detailed error', @action.extract_proxy_error({ 'error' => { 'message' => 'detailed error' } })
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
test 'returns stringified hash when no message key' do
|
|
34
|
-
error_hash = { 'code' => 500 }
|
|
35
|
-
result = @action.extract_proxy_error({ 'error' => error_hash })
|
|
36
|
-
assert_equal error_hash.to_s, result
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
8
|
context 'plan' do
|
|
41
9
|
test 'stores job_id and proxy_id in input' do
|
|
42
10
|
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, 'job-123', 42)
|
|
@@ -103,7 +71,7 @@ class PollTaskStatusTest < ForemanOpenbolt::PluginTestCase
|
|
|
103
71
|
assert_equal 'exception', @job.reload.status
|
|
104
72
|
end
|
|
105
73
|
|
|
106
|
-
test 'marks exception immediately on proxy application error' do
|
|
74
|
+
test 'marks exception immediately on proxy application error from job_status' do
|
|
107
75
|
status_stub = stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
108
76
|
.to_return(status: 200, body: { 'error' => { 'message' => 'Job not found: test-job' } }.to_json,
|
|
109
77
|
headers: { 'Content-Type' => 'application/json' })
|
|
@@ -115,6 +83,71 @@ class PollTaskStatusTest < ForemanOpenbolt::PluginTestCase
|
|
|
115
83
|
assert_equal 'exception', @job.reload.status
|
|
116
84
|
end
|
|
117
85
|
|
|
86
|
+
test 'marks exception immediately on proxy application error from job_result' do
|
|
87
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
88
|
+
.to_return(status: 200, body: { 'status' => 'success' }.to_json,
|
|
89
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
90
|
+
result_stub = stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/result")
|
|
91
|
+
.to_return(status: 200,
|
|
92
|
+
body: { 'error' => { 'message' => 'Result file not found for job' } }.to_json,
|
|
93
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
94
|
+
|
|
95
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
96
|
+
run_action(action)
|
|
97
|
+
|
|
98
|
+
assert_requested(result_stub)
|
|
99
|
+
assert_equal 'exception', @job.reload.status
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
test 'does not strand the job when result fetch fails transiently after completion' do
|
|
103
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
104
|
+
.to_return(status: 200, body: { 'status' => 'success' }.to_json,
|
|
105
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
106
|
+
result_stub = stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/result")
|
|
107
|
+
.to_return(status: 500, body: 'Internal Server Error')
|
|
108
|
+
|
|
109
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
110
|
+
run = run_action(action)
|
|
111
|
+
|
|
112
|
+
assert_requested(result_stub)
|
|
113
|
+
@job.reload
|
|
114
|
+
assert_equal 'running', @job.status
|
|
115
|
+
assert_nil @job.result
|
|
116
|
+
assert_equal 1, run.input[:retry_count]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
test 'marks exception when the result fetch keeps failing past the retry limit after completion' do
|
|
120
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
121
|
+
.to_return(status: 200, body: { 'status' => 'success' }.to_json,
|
|
122
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
123
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/result")
|
|
124
|
+
.to_return(status: 500, body: 'Internal Server Error')
|
|
125
|
+
|
|
126
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
127
|
+
action.input[:retry_count] = Actions::ForemanOpenbolt::PollTaskStatus::RETRY_LIMIT
|
|
128
|
+
run_action(action)
|
|
129
|
+
|
|
130
|
+
@job.reload
|
|
131
|
+
assert_equal 'exception', @job.status
|
|
132
|
+
assert_nil @job.result
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
test 'records completed status when proxy returns a blank result body' do
|
|
136
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
137
|
+
.to_return(status: 200, body: { 'status' => 'success' }.to_json,
|
|
138
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
139
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/result")
|
|
140
|
+
.to_return(status: 200, body: {}.to_json,
|
|
141
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
142
|
+
|
|
143
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
144
|
+
run_action(action)
|
|
145
|
+
|
|
146
|
+
@job.reload
|
|
147
|
+
assert_equal 'success', @job.status
|
|
148
|
+
assert_nil @job.result
|
|
149
|
+
end
|
|
150
|
+
|
|
118
151
|
test 'marks exception immediately when proxy response has no status' do
|
|
119
152
|
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
120
153
|
.to_return(status: 200, body: { 'unexpected' => 'data' }.to_json,
|
|
@@ -131,11 +164,56 @@ class PollTaskStatusTest < ForemanOpenbolt::PluginTestCase
|
|
|
131
164
|
.to_return(status: 500, body: 'Internal Server Error')
|
|
132
165
|
|
|
133
166
|
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
134
|
-
# Set retry count to just above the limit so the next error triggers exhaustion
|
|
135
167
|
action.input[:retry_count] = Actions::ForemanOpenbolt::PollTaskStatus::RETRY_LIMIT
|
|
136
168
|
run_action(action)
|
|
137
169
|
|
|
138
170
|
assert_equal 'exception', @job.reload.status
|
|
139
171
|
end
|
|
172
|
+
|
|
173
|
+
test 'does not raise when exception flip itself fails after retry exhaustion' do
|
|
174
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
175
|
+
.to_return(status: 500, body: 'Internal Server Error')
|
|
176
|
+
::ForemanOpenbolt::TaskJob.any_instance.stubs(:update!).raises(
|
|
177
|
+
ActiveRecord::RecordInvalid.new(ForemanOpenbolt::TaskJob.new)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
181
|
+
action.input[:retry_count] = Actions::ForemanOpenbolt::PollTaskStatus::RETRY_LIMIT
|
|
182
|
+
|
|
183
|
+
assert_nothing_raised { run_action(action) }
|
|
184
|
+
assert_equal 'running', @job.reload.status
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
test 'does not raise when exception flip itself fails after proxy not found' do
|
|
188
|
+
::ForemanOpenbolt::TaskJob.any_instance.stubs(:update!).raises(
|
|
189
|
+
ActiveRecord::RecordInvalid.new(ForemanOpenbolt::TaskJob.new)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, -1)
|
|
193
|
+
|
|
194
|
+
assert_nothing_raised { run_action(action) }
|
|
195
|
+
assert_equal 'running', @job.reload.status
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
test 'does not raise when exception flip itself fails after proxy-reported error' do
|
|
199
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
200
|
+
.to_return(status: 200,
|
|
201
|
+
body: { 'error' => { 'message' => 'Job not found on proxy' } }.to_json,
|
|
202
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
203
|
+
::ForemanOpenbolt::TaskJob.any_instance.stubs(:update!).raises(
|
|
204
|
+
ActiveRecord::RecordInvalid.new(ForemanOpenbolt::TaskJob.new)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
208
|
+
action.input[:retry_count] = 5
|
|
209
|
+
|
|
210
|
+
run = nil
|
|
211
|
+
assert_nothing_raised do
|
|
212
|
+
run = run_action(action)
|
|
213
|
+
end
|
|
214
|
+
# The stubbed update! never wrote 'exception'.
|
|
215
|
+
assert_equal 'running', @job.reload.status
|
|
216
|
+
assert_equal 5, run.input[:retry_count]
|
|
217
|
+
end
|
|
140
218
|
end
|
|
141
219
|
end
|