foreman_openbolt 1.1.0 → 1.1.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.
- checksums.yaml +4 -4
- data/README.md +9 -15
- data/Rakefile +0 -0
- data/lib/foreman_openbolt/version.rb +1 -1
- data/package.json +1 -1
- data/test/acceptance/acceptance_helper.rb +146 -0
- data/test/acceptance/docker/docker-compose.yml +69 -0
- data/test/acceptance/docker/foreman/Dockerfile +45 -0
- data/test/acceptance/docker/foreman/entrypoint.sh +26 -0
- data/test/acceptance/docker/target/Dockerfile +29 -0
- data/test/acceptance/docker/target/entrypoint.sh +11 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.json +30 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.sh +16 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/echo.json +13 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/echo.sh +3 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.json +8 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.sh +3 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.json +8 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.sh +2 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.json +14 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.sh +3 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.json +13 -0
- data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.sh +9 -0
- data/test/acceptance/fixtures/openbolt.yml +7 -0
- data/test/acceptance/tests/error_handling_test.rb +40 -0
- data/test/acceptance/tests/host_selector_test.rb +31 -0
- data/test/acceptance/tests/launch_task_test.rb +96 -0
- data/test/acceptance/tests/parameter_table_test.rb +61 -0
- data/test/acceptance/tests/settings_test.rb +95 -0
- data/test/acceptance/tests/ssh_options_test.rb +77 -0
- data/test/acceptance/tests/task_execution_test.rb +40 -0
- data/test/acceptance/tests/task_history_test.rb +84 -0
- data/test/acceptance/tests/transport_options_test.rb +121 -0
- data/test/test_plugin_helper.rb +17 -0
- data/test/unit/controllers/task_controller_test.rb +351 -0
- data/test/unit/docker/Dockerfile +47 -0
- data/test/unit/docker/docker-compose.yml +33 -0
- data/test/unit/docker/entrypoint.sh +4 -0
- data/test/unit/factories/foreman_openbolt_factories.rb +39 -0
- data/test/unit/lib/actions/cleanup_proxy_artifacts_test.rb +51 -0
- data/test/unit/lib/actions/poll_task_status_test.rb +141 -0
- data/test/unit/lib/proxy_api/openbolt_test.rb +174 -0
- data/test/unit/models/task_job_test.rb +278 -0
- metadata +39 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
class TaskControllerTest < ActionController::TestCase
|
|
6
|
+
tests ForemanOpenbolt::TaskController
|
|
7
|
+
|
|
8
|
+
setup do
|
|
9
|
+
@routes = ForemanOpenbolt::Engine.routes
|
|
10
|
+
@proxy = FactoryBot.create(:smart_proxy)
|
|
11
|
+
@session = set_session_user
|
|
12
|
+
WebMock.reset!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
teardown do
|
|
16
|
+
WebMock.reset!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context 'fetch_tasks' do
|
|
20
|
+
test 'returns tasks from proxy as JSON' do
|
|
21
|
+
tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
22
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
23
|
+
.to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
24
|
+
|
|
25
|
+
get :fetch_tasks, params: { proxy_id: @proxy.id }, session: @session
|
|
26
|
+
assert_response :success
|
|
27
|
+
assert_equal tasks, JSON.parse(response.body)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test 'returns error when proxy_id is missing' do
|
|
31
|
+
get :fetch_tasks, session: @session
|
|
32
|
+
assert_response :bad_request
|
|
33
|
+
body = JSON.parse(response.body)
|
|
34
|
+
assert_match(/Smart Proxy ID is required/, body['error'])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
test 'returns error when proxy not found' do
|
|
38
|
+
get :fetch_tasks, params: { proxy_id: -1 }, session: @session
|
|
39
|
+
assert_response :not_found
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
test 'returns internal_server_error when proxy is unreachable' do
|
|
43
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks").to_timeout
|
|
44
|
+
|
|
45
|
+
get :fetch_tasks, params: { proxy_id: @proxy.id }, session: @session
|
|
46
|
+
assert_response :internal_server_error
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context 'reload_tasks' do
|
|
51
|
+
test 'returns reloaded tasks from proxy' do
|
|
52
|
+
tasks = { 'new::task' => {} }
|
|
53
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/reload")
|
|
54
|
+
.to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
55
|
+
|
|
56
|
+
get :reload_tasks, params: { proxy_id: @proxy.id }, session: @session
|
|
57
|
+
assert_response :success
|
|
58
|
+
assert_equal tasks, JSON.parse(response.body)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context 'launch_task' do
|
|
63
|
+
setup do
|
|
64
|
+
@tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
65
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
66
|
+
.to_return(status: 200, body: @tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
67
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
68
|
+
.to_return(status: 200, body: { 'id' => 'launched-job-1' }.to_json,
|
|
69
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
70
|
+
ForemanTasks.stubs(:async_task)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
test 'launches task and returns job_id' do
|
|
74
|
+
post :launch_task, params: {
|
|
75
|
+
proxy_id: @proxy.id,
|
|
76
|
+
task_name: 'mymod::install',
|
|
77
|
+
targets: 'host1.example.com',
|
|
78
|
+
params: { 'name' => 'nginx' },
|
|
79
|
+
options: { 'transport' => 'ssh' },
|
|
80
|
+
}, session: @session
|
|
81
|
+
|
|
82
|
+
assert_response :success
|
|
83
|
+
body = JSON.parse(response.body)
|
|
84
|
+
assert_equal 'launched-job-1', body['job_id']
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
test 'schedules polling after creating task' do
|
|
88
|
+
ForemanTasks.expects(:async_task).with(
|
|
89
|
+
Actions::ForemanOpenbolt::PollTaskStatus,
|
|
90
|
+
'launched-job-1',
|
|
91
|
+
@proxy.id
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
post :launch_task, params: {
|
|
95
|
+
proxy_id: @proxy.id,
|
|
96
|
+
task_name: 'mymod::install',
|
|
97
|
+
targets: 'host1.example.com',
|
|
98
|
+
}, session: @session
|
|
99
|
+
|
|
100
|
+
assert_response :success
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
test 'creates a TaskJob record' do
|
|
104
|
+
assert_difference('ForemanOpenbolt::TaskJob.count', 1) do
|
|
105
|
+
post :launch_task, params: {
|
|
106
|
+
proxy_id: @proxy.id,
|
|
107
|
+
task_name: 'mymod::install',
|
|
108
|
+
targets: 'host1.example.com,host2.example.com',
|
|
109
|
+
}, session: @session
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
job = ForemanOpenbolt::TaskJob.last
|
|
113
|
+
assert_equal 'mymod::install', job.task_name
|
|
114
|
+
assert_equal %w[host1.example.com host2.example.com], job.targets
|
|
115
|
+
assert_equal 'pending', job.status
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
test 'returns error when task_name is missing' do
|
|
119
|
+
post :launch_task, params: { proxy_id: @proxy.id, targets: 'host1' }, session: @session
|
|
120
|
+
assert_response :bad_request
|
|
121
|
+
assert_match(/task_name/, JSON.parse(response.body)['error'])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
test 'returns error when targets is missing' do
|
|
125
|
+
post :launch_task, params: { proxy_id: @proxy.id, task_name: 'test::task' }, session: @session
|
|
126
|
+
assert_response :bad_request
|
|
127
|
+
assert_match(/targets/, JSON.parse(response.body)['error'])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
test 'returns error when proxy returns error in response' do
|
|
131
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
132
|
+
.to_return(status: 200, body: { 'error' => 'Task not found' }.to_json,
|
|
133
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
134
|
+
|
|
135
|
+
post :launch_task, params: {
|
|
136
|
+
proxy_id: @proxy.id,
|
|
137
|
+
task_name: 'missing::task',
|
|
138
|
+
targets: 'host1',
|
|
139
|
+
}, session: @session
|
|
140
|
+
assert_response :bad_request
|
|
141
|
+
assert_match(/Task execution failed/, JSON.parse(response.body)['error'])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
test 'returns error when proxy returns no job ID' do
|
|
145
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
146
|
+
.to_return(status: 200, body: { 'status' => 'ok' }.to_json,
|
|
147
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
148
|
+
|
|
149
|
+
post :launch_task, params: {
|
|
150
|
+
proxy_id: @proxy.id,
|
|
151
|
+
task_name: 'test::task',
|
|
152
|
+
targets: 'host1',
|
|
153
|
+
}, session: @session
|
|
154
|
+
assert_response :bad_request
|
|
155
|
+
assert_match(/No job ID returned/, JSON.parse(response.body)['error'])
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'job_status' do
|
|
160
|
+
test 'returns serialized task job' do
|
|
161
|
+
job = FactoryBot.create(:task_job, :running, smart_proxy: @proxy)
|
|
162
|
+
|
|
163
|
+
get :job_status, params: { job_id: job.job_id }, session: @session
|
|
164
|
+
assert_response :success
|
|
165
|
+
|
|
166
|
+
body = JSON.parse(response.body)
|
|
167
|
+
assert_equal 'running', body['status']
|
|
168
|
+
assert_equal job.task_name, body['task_name']
|
|
169
|
+
assert_equal job.targets, body['targets']
|
|
170
|
+
assert_equal @proxy.id, body['smart_proxy']['id']
|
|
171
|
+
assert_equal @proxy.name, body['smart_proxy']['name']
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
test 'returns error when job_id is missing' do
|
|
175
|
+
get :job_status, session: @session
|
|
176
|
+
assert_response :bad_request
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
test 'returns not_found when job does not exist' do
|
|
180
|
+
get :job_status, params: { job_id: 'nonexistent' }, session: @session
|
|
181
|
+
assert_response :not_found
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
context 'job_result' do
|
|
186
|
+
test 'returns result and log for completed job' do
|
|
187
|
+
ForemanTasks.stubs(:async_task)
|
|
188
|
+
job = FactoryBot.create(:task_job, :success, smart_proxy: @proxy)
|
|
189
|
+
|
|
190
|
+
get :job_result, params: { job_id: job.job_id }, session: @session
|
|
191
|
+
assert_response :success
|
|
192
|
+
|
|
193
|
+
body = JSON.parse(response.body)
|
|
194
|
+
assert_equal 'success', body['status']
|
|
195
|
+
assert_equal job.result, body['value']
|
|
196
|
+
assert_equal job.log, body['log']
|
|
197
|
+
assert_equal job.command, body['command']
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
test 'returns not_found when job does not exist' do
|
|
201
|
+
get :job_result, params: { job_id: 'nonexistent' }, session: @session
|
|
202
|
+
assert_response :not_found
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
context 'launch_task with encrypted options' do
|
|
207
|
+
setup do
|
|
208
|
+
@tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
209
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
210
|
+
.to_return(status: 200, body: @tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
211
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
212
|
+
.to_return(status: 200, body: { 'id' => 'encrypted-job-1' }.to_json,
|
|
213
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
214
|
+
ForemanTasks.stubs(:async_task)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
test 'replaces encrypted placeholder with real setting value before sending to proxy' do
|
|
218
|
+
Setting['openbolt_password'] = 'real-secret-password'
|
|
219
|
+
|
|
220
|
+
post :launch_task, params: {
|
|
221
|
+
proxy_id: @proxy.id,
|
|
222
|
+
task_name: 'mymod::install',
|
|
223
|
+
targets: 'host1.example.com',
|
|
224
|
+
options: { 'password' => '[Use saved encrypted default]', 'transport' => 'ssh' },
|
|
225
|
+
}, session: @session
|
|
226
|
+
|
|
227
|
+
assert_response :success
|
|
228
|
+
|
|
229
|
+
# Verify the real value was sent to the proxy (not the placeholder)
|
|
230
|
+
proxy_request = WebMock::RequestRegistry.instance
|
|
231
|
+
.requested_signatures
|
|
232
|
+
.hash
|
|
233
|
+
.keys
|
|
234
|
+
.find { |sig| sig.uri.path == '/openbolt/launch/task' }
|
|
235
|
+
sent_body = JSON.parse(proxy_request.body)
|
|
236
|
+
assert_equal 'real-secret-password', sent_body['options']['password']
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
test 'scrubs encrypted options before storing in database' do
|
|
240
|
+
Setting['openbolt_password'] = 'real-secret-password'
|
|
241
|
+
|
|
242
|
+
post :launch_task, params: {
|
|
243
|
+
proxy_id: @proxy.id,
|
|
244
|
+
task_name: 'mymod::install',
|
|
245
|
+
targets: 'host1.example.com',
|
|
246
|
+
options: { 'password' => '[Use saved encrypted default]', 'transport' => 'ssh' },
|
|
247
|
+
}, session: @session
|
|
248
|
+
|
|
249
|
+
assert_response :success
|
|
250
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'encrypted-job-1')
|
|
251
|
+
assert_equal '*****', job.openbolt_options['password']
|
|
252
|
+
assert_equal 'ssh', job.openbolt_options['transport']
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
test 'returns error when encrypted placeholder used for nonexistent setting' do
|
|
256
|
+
post :launch_task, params: {
|
|
257
|
+
proxy_id: @proxy.id,
|
|
258
|
+
task_name: 'mymod::install',
|
|
259
|
+
targets: 'host1.example.com',
|
|
260
|
+
options: { 'nonexistent-option' => '[Use saved encrypted default]' },
|
|
261
|
+
}, session: @session
|
|
262
|
+
|
|
263
|
+
assert_response :bad_request
|
|
264
|
+
assert_match(/No saved value for encrypted option/, JSON.parse(response.body)['error'])
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
test 'passes non-encrypted options through unchanged' do
|
|
268
|
+
post :launch_task, params: {
|
|
269
|
+
proxy_id: @proxy.id,
|
|
270
|
+
task_name: 'mymod::install',
|
|
271
|
+
targets: 'host1.example.com',
|
|
272
|
+
options: { 'transport' => 'ssh', 'user' => 'admin' },
|
|
273
|
+
}, session: @session
|
|
274
|
+
|
|
275
|
+
assert_response :success
|
|
276
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'encrypted-job-1')
|
|
277
|
+
assert_equal 'ssh', job.openbolt_options['transport']
|
|
278
|
+
assert_equal 'admin', job.openbolt_options['user']
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
context 'fetch_openbolt_options with settings defaults' do
|
|
283
|
+
test 'merges Foreman setting defaults into proxy options' do
|
|
284
|
+
Setting['openbolt_transport'] = 'winrm'
|
|
285
|
+
Setting['openbolt_user'] = 'admin'
|
|
286
|
+
|
|
287
|
+
proxy_options = {
|
|
288
|
+
'transport' => { 'type' => %w[ssh winrm] },
|
|
289
|
+
'user' => { 'type' => 'string' },
|
|
290
|
+
'verbose' => { 'type' => 'boolean' },
|
|
291
|
+
}
|
|
292
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
293
|
+
.to_return(status: 200, body: proxy_options.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
294
|
+
|
|
295
|
+
get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
|
|
296
|
+
assert_response :success
|
|
297
|
+
|
|
298
|
+
body = JSON.parse(response.body)
|
|
299
|
+
assert_equal 'winrm', body['transport']['default']
|
|
300
|
+
assert_equal 'admin', body['user']['default']
|
|
301
|
+
assert_equal false, body['verbose']['default']
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
test 'shows encrypted placeholder instead of real value for encrypted settings' do
|
|
305
|
+
Setting['openbolt_password'] = 'secret-value'
|
|
306
|
+
|
|
307
|
+
proxy_options = {
|
|
308
|
+
'password' => { 'type' => 'string', 'sensitive' => true },
|
|
309
|
+
}
|
|
310
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
311
|
+
.to_return(status: 200, body: proxy_options.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
312
|
+
|
|
313
|
+
get :fetch_openbolt_options, params: { proxy_id: @proxy.id }, session: @session
|
|
314
|
+
assert_response :success
|
|
315
|
+
|
|
316
|
+
body = JSON.parse(response.body)
|
|
317
|
+
assert_equal '[Use saved encrypted default]', body['password']['default']
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
context 'fetch_task_history' do
|
|
322
|
+
test 'returns paginated task history' do
|
|
323
|
+
3.times { FactoryBot.create(:task_job, smart_proxy: @proxy) }
|
|
324
|
+
|
|
325
|
+
get :fetch_task_history, params: { page: 1, per_page: 2 }, session: @session
|
|
326
|
+
assert_response :success
|
|
327
|
+
|
|
328
|
+
body = JSON.parse(response.body)
|
|
329
|
+
assert_equal 2, body['results'].length
|
|
330
|
+
assert_equal 3, body['total']
|
|
331
|
+
assert_equal 1, body['page']
|
|
332
|
+
assert_equal 2, body['per_page']
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
test 'caps per_page at 100' do
|
|
336
|
+
get :fetch_task_history, params: { per_page: 200 }, session: @session
|
|
337
|
+
assert_response :success
|
|
338
|
+
|
|
339
|
+
body = JSON.parse(response.body)
|
|
340
|
+
assert_equal 100, body['per_page']
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
test 'defaults per_page to 20' do
|
|
344
|
+
get :fetch_task_history, session: @session
|
|
345
|
+
assert_response :success
|
|
346
|
+
|
|
347
|
+
body = JSON.parse(response.body)
|
|
348
|
+
assert_equal 20, body['per_page']
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Base test image: Foreman + system deps on Rocky Linux 9.
|
|
2
|
+
# This image is stable for a given Foreman version and does not
|
|
3
|
+
# depend on any foreman_openbolt code. All plugin wiring happens
|
|
4
|
+
# at runtime via the rake test:up task.
|
|
5
|
+
#
|
|
6
|
+
# Build:
|
|
7
|
+
# FOREMAN_BRANCH=3.18-stable docker compose -f test/unit/docker/docker-compose.yml build test
|
|
8
|
+
|
|
9
|
+
FROM rockylinux:9
|
|
10
|
+
|
|
11
|
+
# System packages -- cached across all Foreman versions.
|
|
12
|
+
RUN dnf module enable -y nodejs:22 postgresql:16 && \
|
|
13
|
+
dnf install -y \
|
|
14
|
+
ruby ruby-devel rubygem-bundler \
|
|
15
|
+
gcc gcc-c++ make \
|
|
16
|
+
postgresql-devel postgresql \
|
|
17
|
+
libffi-devel \
|
|
18
|
+
nodejs npm \
|
|
19
|
+
git-core \
|
|
20
|
+
redhat-rpm-config && \
|
|
21
|
+
dnf clean all
|
|
22
|
+
|
|
23
|
+
ARG FOREMAN_BRANCH=3.18-stable
|
|
24
|
+
|
|
25
|
+
# Clone Foreman at the specified branch.
|
|
26
|
+
RUN git init /opt/foreman
|
|
27
|
+
|
|
28
|
+
WORKDIR /opt/foreman
|
|
29
|
+
|
|
30
|
+
RUN git remote add origin https://github.com/theforeman/foreman.git && \
|
|
31
|
+
git fetch --depth=1 origin ${FOREMAN_BRANCH} && \
|
|
32
|
+
git checkout FETCH_HEAD
|
|
33
|
+
|
|
34
|
+
# Configure database for the test container.
|
|
35
|
+
RUN printf "test:\n adapter: postgresql\n host: db\n port: 5432\n username: foreman\n password: foreman\n database: foreman_test\n encoding: utf8\n pool: 5\n" \
|
|
36
|
+
> config/database.yml
|
|
37
|
+
|
|
38
|
+
# Install Foreman's own gems (without the plugin). This is the
|
|
39
|
+
# expensive layer but only changes when the Foreman version changes.
|
|
40
|
+
ENV BUNDLE_WITHOUT="release console journald libvirt ec2 openstack vmware telemetry"
|
|
41
|
+
ENV BUNDLE_PATH="/opt/bundle"
|
|
42
|
+
RUN bundle install --jobs=4
|
|
43
|
+
|
|
44
|
+
COPY test/unit/docker/entrypoint.sh /entrypoint.sh
|
|
45
|
+
RUN chmod +x /entrypoint.sh
|
|
46
|
+
|
|
47
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: foreman-openbolt-unit
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
db:
|
|
5
|
+
image: postgres:16-alpine
|
|
6
|
+
environment:
|
|
7
|
+
POSTGRES_USER: foreman
|
|
8
|
+
POSTGRES_PASSWORD: foreman
|
|
9
|
+
POSTGRES_DB: foreman_test
|
|
10
|
+
tmpfs: /var/lib/postgresql/data
|
|
11
|
+
healthcheck:
|
|
12
|
+
test: ["CMD-SHELL", "pg_isready -U foreman"]
|
|
13
|
+
interval: 2s
|
|
14
|
+
timeout: 5s
|
|
15
|
+
retries: 10
|
|
16
|
+
|
|
17
|
+
test:
|
|
18
|
+
image: foreman-openbolt-test:${FOREMAN_BRANCH:-3.18-stable}
|
|
19
|
+
build:
|
|
20
|
+
context: ../../..
|
|
21
|
+
dockerfile: test/unit/docker/Dockerfile
|
|
22
|
+
args:
|
|
23
|
+
FOREMAN_BRANCH: ${FOREMAN_BRANCH:-3.18-stable}
|
|
24
|
+
depends_on:
|
|
25
|
+
db:
|
|
26
|
+
condition: service_healthy
|
|
27
|
+
environment:
|
|
28
|
+
RAILS_ENV: test
|
|
29
|
+
DATABASE_URL: postgresql://foreman:foreman@db:5432/foreman_test
|
|
30
|
+
volumes:
|
|
31
|
+
- ../../..:/opt/foreman_openbolt
|
|
32
|
+
working_dir: /opt/foreman
|
|
33
|
+
command: ["tail", "-f", "/dev/null"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FactoryBot.define do
|
|
4
|
+
factory :task_job, class: 'ForemanOpenbolt::TaskJob' do
|
|
5
|
+
sequence(:job_id) { |n| "test-job-#{n}" }
|
|
6
|
+
association :smart_proxy
|
|
7
|
+
task_name { 'mymod::install' }
|
|
8
|
+
task_description { 'Install a package on the target host' }
|
|
9
|
+
status { 'pending' }
|
|
10
|
+
targets { ['host1.example.com', 'host2.example.com'] }
|
|
11
|
+
submitted_at { Time.current }
|
|
12
|
+
task_parameters { { 'name' => 'nginx' } }
|
|
13
|
+
openbolt_options { { 'transport' => 'ssh' } }
|
|
14
|
+
|
|
15
|
+
trait :running do
|
|
16
|
+
status { 'running' }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
trait :success do
|
|
20
|
+
status { 'success' }
|
|
21
|
+
completed_at { Time.current }
|
|
22
|
+
result { { 'items' => [{ 'target' => 'host1.example.com', 'status' => 'success' }] } }
|
|
23
|
+
log { 'Started: task mymod::install\nFinished: success' }
|
|
24
|
+
command { 'bolt task run mymod::install --targets host1.example.com' }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
trait :failure do
|
|
28
|
+
status { 'failure' }
|
|
29
|
+
completed_at { Time.current }
|
|
30
|
+
result { { 'items' => [{ 'target' => 'host1.example.com', 'status' => 'failure' }] } }
|
|
31
|
+
log { 'Started: task mymod::install\nFinished: failure' }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
trait :exception do
|
|
35
|
+
status { 'exception' }
|
|
36
|
+
completed_at { Time.current }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
class CleanupProxyArtifactsTest < ForemanOpenbolt::PluginTestCase
|
|
6
|
+
include Dynflow::Testing
|
|
7
|
+
|
|
8
|
+
context 'plan' do
|
|
9
|
+
test 'stores proxy_id and job_id in input' do
|
|
10
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, 42, 'job-123')
|
|
11
|
+
assert_equal 42, action.input[:proxy_id]
|
|
12
|
+
assert_equal 'job-123', action.input[:job_id]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context 'rescue_strategy' do
|
|
17
|
+
test 'uses Skip rescue strategy' do
|
|
18
|
+
action = create_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts)
|
|
19
|
+
assert_equal Dynflow::Action::Rescue::Skip, action.rescue_strategy
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context 'run' do
|
|
24
|
+
test 'calls delete_job_artifacts when proxy exists' do
|
|
25
|
+
proxy = FactoryBot.create(:smart_proxy)
|
|
26
|
+
stub = stub_request(:delete, "#{proxy.url}/openbolt/job/job-456/artifacts")
|
|
27
|
+
.to_return(status: 200, body: '{"deleted": true}', headers: { 'Content-Type' => 'application/json' })
|
|
28
|
+
|
|
29
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, proxy.id, 'job-456')
|
|
30
|
+
run_action(action)
|
|
31
|
+
|
|
32
|
+
assert_requested(stub)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
test 'does not call API when proxy not found' do
|
|
36
|
+
stub = stub_request(:delete, %r{openbolt/job})
|
|
37
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, -1, 'job-456')
|
|
38
|
+
run_action(action)
|
|
39
|
+
assert_not_requested(stub)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
test 'does not raise when API call fails' do
|
|
43
|
+
proxy = FactoryBot.create(:smart_proxy)
|
|
44
|
+
stub_request(:delete, "#{proxy.url}/openbolt/job/job-456/artifacts")
|
|
45
|
+
.to_return(status: 500, body: 'Internal Server Error')
|
|
46
|
+
|
|
47
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::CleanupProxyArtifacts, proxy.id, 'job-456')
|
|
48
|
+
assert_nothing_raised { run_action(action) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
class PollTaskStatusTest < ForemanOpenbolt::PluginTestCase
|
|
6
|
+
include Dynflow::Testing
|
|
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
|
+
context 'plan' do
|
|
41
|
+
test 'stores job_id and proxy_id in input' do
|
|
42
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, 'job-123', 42)
|
|
43
|
+
assert_equal 'job-123', action.input[:job_id]
|
|
44
|
+
assert_equal 42, action.input[:proxy_id]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context 'rescue_strategy' do
|
|
49
|
+
test 'uses Skip rescue strategy' do
|
|
50
|
+
action = create_action(Actions::ForemanOpenbolt::PollTaskStatus)
|
|
51
|
+
assert_equal Dynflow::Action::Rescue::Skip, action.rescue_strategy
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context 'poll_and_reschedule' do
|
|
56
|
+
setup do
|
|
57
|
+
@proxy = FactoryBot.create(:smart_proxy)
|
|
58
|
+
@job = FactoryBot.create(:task_job, :running, smart_proxy: @proxy)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
test 'polls proxy and keeps status when unchanged' do
|
|
62
|
+
status_stub = stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
63
|
+
.to_return(status: 200, body: { 'status' => 'running' }.to_json,
|
|
64
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
65
|
+
|
|
66
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
67
|
+
run_action(action)
|
|
68
|
+
|
|
69
|
+
assert_requested(status_stub)
|
|
70
|
+
assert_equal 'running', @job.reload.status
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
test 'fetches result when job completes' do
|
|
74
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
75
|
+
.to_return(status: 200, body: { 'status' => 'success' }.to_json,
|
|
76
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
77
|
+
|
|
78
|
+
result_body = { 'status' => 'success', 'value' => { 'items' => [] }, 'log' => 'done',
|
|
79
|
+
'command' => 'bolt task run test' }
|
|
80
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/result")
|
|
81
|
+
.to_return(status: 200, body: result_body.to_json,
|
|
82
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
83
|
+
|
|
84
|
+
ForemanTasks.stubs(:async_task)
|
|
85
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
86
|
+
run_action(action)
|
|
87
|
+
|
|
88
|
+
@job.reload
|
|
89
|
+
assert_equal 'success', @job.status
|
|
90
|
+
assert_equal({ 'items' => [] }, @job.result)
|
|
91
|
+
assert_equal 'done', @job.log
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
test 'finishes cleanly when task job not found' do
|
|
95
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, 'nonexistent-id', @proxy.id)
|
|
96
|
+
assert_nothing_raised { run_action(action) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
test 'marks exception when proxy not found' do
|
|
100
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, -1)
|
|
101
|
+
run_action(action)
|
|
102
|
+
|
|
103
|
+
assert_equal 'exception', @job.reload.status
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
test 'marks exception immediately on proxy application error' do
|
|
107
|
+
status_stub = stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
108
|
+
.to_return(status: 200, body: { 'error' => { 'message' => 'Job not found: test-job' } }.to_json,
|
|
109
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
110
|
+
|
|
111
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
112
|
+
run_action(action)
|
|
113
|
+
|
|
114
|
+
assert_requested(status_stub)
|
|
115
|
+
assert_equal 'exception', @job.reload.status
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
test 'marks exception immediately when proxy response has no status' do
|
|
119
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
120
|
+
.to_return(status: 200, body: { 'unexpected' => 'data' }.to_json,
|
|
121
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
122
|
+
|
|
123
|
+
action = create_and_plan_action(Actions::ForemanOpenbolt::PollTaskStatus, @job.job_id, @proxy.id)
|
|
124
|
+
run_action(action)
|
|
125
|
+
|
|
126
|
+
assert_equal 'exception', @job.reload.status
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
test 'marks job as exception after exhausting retry limit' do
|
|
130
|
+
stub_request(:get, "#{@proxy.url}/openbolt/job/#{@job.job_id}/status")
|
|
131
|
+
.to_return(status: 500, body: 'Internal Server Error')
|
|
132
|
+
|
|
133
|
+
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
|
+
action.input[:retry_count] = Actions::ForemanOpenbolt::PollTaskStatus::RETRY_LIMIT
|
|
136
|
+
run_action(action)
|
|
137
|
+
|
|
138
|
+
assert_equal 'exception', @job.reload.status
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|