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
|
@@ -29,11 +29,11 @@ class ProxyApiOpenboltTest < ForemanOpenbolt::PluginTestCase
|
|
|
29
29
|
assert_equal tasks, result
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
test '
|
|
32
|
+
test 'wraps proxy HTTP errors as ProxyException' do
|
|
33
33
|
stub_request(:get, "#{PROXY_URL}/openbolt/tasks")
|
|
34
34
|
.to_return(status: 500, body: 'Internal Server Error')
|
|
35
35
|
|
|
36
|
-
assert_raises(
|
|
36
|
+
assert_raises(ProxyAPI::ProxyException) { @api.fetch_tasks }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
test 'raises ProxyException on unparseable response body' do
|
|
@@ -159,16 +159,131 @@ class ProxyApiOpenboltTest < ForemanOpenbolt::PluginTestCase
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
context 'connection errors' do
|
|
162
|
-
test '
|
|
162
|
+
test 'wraps timeouts as ProxyException' do
|
|
163
163
|
stub_request(:get, "#{PROXY_URL}/openbolt/tasks").to_timeout
|
|
164
164
|
|
|
165
|
-
assert_raises(
|
|
165
|
+
assert_raises(ProxyAPI::ProxyException) { @api.fetch_tasks }
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
-
test '
|
|
168
|
+
test 'wraps connection refused as ProxyException' do
|
|
169
169
|
stub_request(:get, "#{PROXY_URL}/openbolt/tasks").to_raise(Errno::ECONNREFUSED)
|
|
170
170
|
|
|
171
|
-
assert_raises(
|
|
171
|
+
assert_raises(ProxyAPI::ProxyException) { @api.fetch_tasks }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
test 'wraps SocketError as ProxyException' do
|
|
175
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/tasks").to_raise(SocketError.new('host not found'))
|
|
176
|
+
|
|
177
|
+
assert_raises(ProxyAPI::ProxyException) { @api.fetch_tasks }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
test 'wraps SSL errors as ProxyException' do
|
|
181
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/tasks")
|
|
182
|
+
.to_raise(OpenSSL::SSL::SSLError.new('bad cert'))
|
|
183
|
+
|
|
184
|
+
assert_raises(ProxyAPI::ProxyException) { @api.fetch_tasks }
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The proxy reports domain errors as HTTP 200 with {"error": {"message":
|
|
189
|
+
# ...}}. ProxyReportedError makes those loud everywhere except launch_task,
|
|
190
|
+
# which passes the envelope through so the caller renders a 400.
|
|
191
|
+
context 'proxy-reported errors (200 + error envelope)' do
|
|
192
|
+
error_body = { 'error' => { 'message' => 'Job not found: bogus' } }.to_json
|
|
193
|
+
|
|
194
|
+
test 'fetch_tasks raises ProxyReportedError' do
|
|
195
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/tasks")
|
|
196
|
+
.to_return(status: 200, body: error_body, headers: { 'Content-Type' => 'application/json' })
|
|
197
|
+
|
|
198
|
+
error = assert_raises(ProxyAPI::Openbolt::ProxyReportedError) { @api.fetch_tasks }
|
|
199
|
+
assert_match(/Job not found: bogus/, error.message)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
test 'reload_tasks raises ProxyReportedError' do
|
|
203
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/tasks/reload")
|
|
204
|
+
.to_return(status: 200, body: error_body, headers: { 'Content-Type' => 'application/json' })
|
|
205
|
+
|
|
206
|
+
assert_raises(ProxyAPI::Openbolt::ProxyReportedError) { @api.reload_tasks }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
test 'openbolt_options raises ProxyReportedError' do
|
|
210
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/tasks/options")
|
|
211
|
+
.to_return(status: 200, body: error_body, headers: { 'Content-Type' => 'application/json' })
|
|
212
|
+
|
|
213
|
+
assert_raises(ProxyAPI::Openbolt::ProxyReportedError) { @api.openbolt_options }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
test 'job_status raises ProxyReportedError' do
|
|
217
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/job/test-123/status")
|
|
218
|
+
.to_return(status: 200, body: error_body, headers: { 'Content-Type' => 'application/json' })
|
|
219
|
+
|
|
220
|
+
assert_raises(ProxyAPI::Openbolt::ProxyReportedError) { @api.job_status(job_id: 'test-123') }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
test 'job_result raises ProxyReportedError' do
|
|
224
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/job/test-123/result")
|
|
225
|
+
.to_return(status: 200, body: error_body, headers: { 'Content-Type' => 'application/json' })
|
|
226
|
+
|
|
227
|
+
assert_raises(ProxyAPI::Openbolt::ProxyReportedError) { @api.job_result(job_id: 'test-123') }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
test 'delete_job_artifacts raises ProxyReportedError' do
|
|
231
|
+
stub_request(:delete, "#{PROXY_URL}/openbolt/job/test-123/artifacts")
|
|
232
|
+
.to_return(status: 200, body: error_body, headers: { 'Content-Type' => 'application/json' })
|
|
233
|
+
|
|
234
|
+
assert_raises(ProxyAPI::Openbolt::ProxyReportedError) { @api.delete_job_artifacts(job_id: 'test-123') }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
test 'launch_task returns the error envelope unchanged' do
|
|
238
|
+
stub_request(:post, "#{PROXY_URL}/openbolt/launch/task")
|
|
239
|
+
.to_return(status: 200, body: error_body, headers: { 'Content-Type' => 'application/json' })
|
|
240
|
+
|
|
241
|
+
result = @api.launch_task(name: 'bogus', targets: 'host1', parameters: {}, options: {})
|
|
242
|
+
assert_equal('Job not found: bogus', result['error']['message'])
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
test 'launch_task still wraps transport errors' do
|
|
246
|
+
stub_request(:post, "#{PROXY_URL}/openbolt/launch/task").to_timeout
|
|
247
|
+
|
|
248
|
+
assert_raises(ProxyAPI::ProxyException) do
|
|
249
|
+
@api.launch_task(name: 'mymod::install', targets: 'host1', parameters: {}, options: {})
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
test 'ProxyReportedError is a ProxyException so existing rescue_from chains catch it' do
|
|
254
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/tasks")
|
|
255
|
+
.to_return(status: 200, body: error_body, headers: { 'Content-Type' => 'application/json' })
|
|
256
|
+
|
|
257
|
+
assert_raises(ProxyAPI::ProxyException) { @api.fetch_tasks }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
test 'a task named error does not raise ProxyReportedError' do
|
|
261
|
+
tasks = {
|
|
262
|
+
'error' => { 'description' => 'Raise an error on the target' },
|
|
263
|
+
'mymod::install' => { 'description' => 'Install a package' },
|
|
264
|
+
}
|
|
265
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/tasks")
|
|
266
|
+
.to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
267
|
+
|
|
268
|
+
assert_equal tasks, @api.fetch_tasks
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
test 'top-level error key with a string value is not treated as an envelope' do
|
|
272
|
+
body = { 'error' => 'Plain string error' }
|
|
273
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/tasks")
|
|
274
|
+
.to_return(status: 200, body: body.to_json,
|
|
275
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
276
|
+
|
|
277
|
+
assert_equal body, @api.fetch_tasks
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
test 'error hash without a message key is not treated as an envelope' do
|
|
281
|
+
body = { 'error' => { 'code' => 42 } }
|
|
282
|
+
stub_request(:get, "#{PROXY_URL}/openbolt/tasks")
|
|
283
|
+
.to_return(status: 200, body: body.to_json,
|
|
284
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
285
|
+
|
|
286
|
+
assert_equal body, @api.fetch_tasks
|
|
172
287
|
end
|
|
173
288
|
end
|
|
174
289
|
end
|
|
@@ -200,6 +200,17 @@ class TaskJobTest < ForemanOpenbolt::PluginTestCase
|
|
|
200
200
|
end
|
|
201
201
|
assert_equal({ 'items' => [] }, job.reload.result)
|
|
202
202
|
end
|
|
203
|
+
|
|
204
|
+
test 'cleanup_proxy_artifacts schedules Dynflow action when log is saved without result' do
|
|
205
|
+
job = FactoryBot.create(:task_job, status: 'success', completed_at: Time.current)
|
|
206
|
+
ForemanTasks.expects(:async_task).with(
|
|
207
|
+
::Actions::ForemanOpenbolt::CleanupProxyArtifacts,
|
|
208
|
+
job.smart_proxy_id,
|
|
209
|
+
job.job_id
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
job.update!(log: 'task finished')
|
|
213
|
+
end
|
|
203
214
|
end
|
|
204
215
|
|
|
205
216
|
context 'create_from_execution!' do
|
|
@@ -4,7 +4,7 @@ import { Provider } from 'react-redux';
|
|
|
4
4
|
import { createStore } from 'redux';
|
|
5
5
|
import { MemoryRouter } from 'react-router-dom';
|
|
6
6
|
import { API } from 'foremanReact/redux/API';
|
|
7
|
-
import LaunchTask from '../index';
|
|
7
|
+
import LaunchTask, { buildLaunchPayload } from '../index';
|
|
8
8
|
|
|
9
9
|
// Mock HostSelector to avoid Apollo/GraphQL dependency chain
|
|
10
10
|
jest.mock('../HostSelector', () => {
|
|
@@ -81,3 +81,46 @@ describe('LaunchTask', () => {
|
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
describe('buildLaunchPayload', () => {
|
|
86
|
+
test('passes "parameters" key through correctly', () => {
|
|
87
|
+
const body = buildLaunchPayload({
|
|
88
|
+
proxyId: 1,
|
|
89
|
+
taskName: 'mymod::install',
|
|
90
|
+
targets: ['host1.example.com'],
|
|
91
|
+
parameters: { name: 'nginx', version: '1.21' },
|
|
92
|
+
options: { transport: 'ssh' },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(body).toHaveProperty('parameters', {
|
|
96
|
+
name: 'nginx',
|
|
97
|
+
version: '1.21',
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('joins targets with comma into a single string', () => {
|
|
102
|
+
const body = buildLaunchPayload({
|
|
103
|
+
proxyId: 1,
|
|
104
|
+
taskName: 'mymod::install',
|
|
105
|
+
targets: ['host1', 'host2', 'host3'],
|
|
106
|
+
parameters: {},
|
|
107
|
+
options: {},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(body.targets).toBe('host1,host2,host3');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('forwards smart_proxy_id, task_name, and options unchanged', () => {
|
|
114
|
+
const body = buildLaunchPayload({
|
|
115
|
+
proxyId: 42,
|
|
116
|
+
taskName: 'puppet_conf::set',
|
|
117
|
+
targets: ['host1'],
|
|
118
|
+
parameters: {},
|
|
119
|
+
options: { transport: 'choria', 'run-as': 'root' },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(body.smart_proxy_id).toBe(42);
|
|
123
|
+
expect(body.task_name).toBe('puppet_conf::set');
|
|
124
|
+
expect(body.options).toEqual({ transport: 'choria', 'run-as': 'root' });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -42,6 +42,9 @@ describe('useOpenBoltOptions', () => {
|
|
|
42
42
|
await result.current.fetchOpenBoltOptions(42);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
expect(API.get).toHaveBeenCalledWith(
|
|
46
|
+
expect.stringContaining('smart_proxy_id=42')
|
|
47
|
+
);
|
|
45
48
|
expect(result.current.openBoltOptionsMetadata).toEqual(options);
|
|
46
49
|
expect(result.current.openBoltOptions).toEqual({
|
|
47
50
|
transport: 'ssh',
|
|
@@ -42,14 +42,14 @@ describe('useTasksData', () => {
|
|
|
42
42
|
expect.stringContaining('fetch_tasks')
|
|
43
43
|
);
|
|
44
44
|
expect(API.get).toHaveBeenCalledWith(
|
|
45
|
-
expect.stringContaining('
|
|
45
|
+
expect.stringContaining('smart_proxy_id=42')
|
|
46
46
|
);
|
|
47
47
|
expect(result.current.taskMetadata).toEqual(tasks);
|
|
48
48
|
expect(result.current.isLoadingTasks).toBe(false);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
test('fetchTasks uses reload endpoint when forceReload is true', async () => {
|
|
52
|
-
API.
|
|
51
|
+
test('fetchTasks uses POST to the reload endpoint when forceReload is true', async () => {
|
|
52
|
+
API.post.mockResolvedValue({ data: {} });
|
|
53
53
|
|
|
54
54
|
const { result } = renderHook(() => useTasksData(), { wrapper });
|
|
55
55
|
|
|
@@ -57,9 +57,10 @@ describe('useTasksData', () => {
|
|
|
57
57
|
await result.current.fetchTasks(42, true);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
expect(API.
|
|
60
|
+
expect(API.post).toHaveBeenCalledWith(
|
|
61
61
|
expect.stringContaining('reload_tasks')
|
|
62
62
|
);
|
|
63
|
+
expect(API.get).not.toHaveBeenCalled();
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
test('fetchTasks resets state before fetching', async () => {
|
|
@@ -21,7 +21,7 @@ export const useOpenBoltOptions = () => {
|
|
|
21
21
|
|
|
22
22
|
try {
|
|
23
23
|
const { data } = await API.get(
|
|
24
|
-
`${ROUTES.API.FETCH_OPENBOLT_OPTIONS}?
|
|
24
|
+
`${ROUTES.API.FETCH_OPENBOLT_OPTIONS}?smart_proxy_id=${proxyId}`
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
setOpenBoltOptionsMetadata(data || {});
|
|
@@ -24,7 +24,10 @@ export const useTasksData = () => {
|
|
|
24
24
|
const endpoint = forceReload
|
|
25
25
|
? ROUTES.API.RELOAD_TASKS
|
|
26
26
|
: ROUTES.API.FETCH_TASKS;
|
|
27
|
-
const
|
|
27
|
+
const method = forceReload ? 'post' : 'get';
|
|
28
|
+
const { data } = await API[method](
|
|
29
|
+
`${endpoint}?smart_proxy_id=${proxyId}`
|
|
30
|
+
);
|
|
28
31
|
|
|
29
32
|
setTaskMetadata(data || {});
|
|
30
33
|
|
|
@@ -29,6 +29,21 @@ import { useTasksData } from './hooks/useTasksData';
|
|
|
29
29
|
import { useOpenBoltOptions } from './hooks/useOpenBoltOptions';
|
|
30
30
|
import { useShowMessage, extractErrorMessage } from '../common/helpers';
|
|
31
31
|
|
|
32
|
+
// Exported so tests can use the launch wire format
|
|
33
|
+
export const buildLaunchPayload = ({
|
|
34
|
+
proxyId,
|
|
35
|
+
taskName,
|
|
36
|
+
targets,
|
|
37
|
+
parameters,
|
|
38
|
+
options,
|
|
39
|
+
}) => ({
|
|
40
|
+
smart_proxy_id: proxyId,
|
|
41
|
+
task_name: taskName,
|
|
42
|
+
targets: targets.join(','),
|
|
43
|
+
parameters,
|
|
44
|
+
options,
|
|
45
|
+
});
|
|
46
|
+
|
|
32
47
|
const LaunchTask = () => {
|
|
33
48
|
const history = useHistory();
|
|
34
49
|
const showMessage = useShowMessage();
|
|
@@ -165,13 +180,13 @@ const LaunchTask = () => {
|
|
|
165
180
|
}
|
|
166
181
|
);
|
|
167
182
|
|
|
168
|
-
const body = {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
targets
|
|
172
|
-
|
|
183
|
+
const body = buildLaunchPayload({
|
|
184
|
+
proxyId: selectedProxy,
|
|
185
|
+
taskName: selectedTask,
|
|
186
|
+
targets,
|
|
187
|
+
parameters: taskParameters,
|
|
173
188
|
options: visibleOptions,
|
|
174
|
-
};
|
|
189
|
+
});
|
|
175
190
|
|
|
176
191
|
const { data } = await API.post(ROUTES.API.LAUNCH_TASK, body);
|
|
177
192
|
|
|
@@ -53,7 +53,7 @@ describe('TaskExecution', () => {
|
|
|
53
53
|
test('strips ANSI codes from log output when result is available', async () => {
|
|
54
54
|
const statusData = {
|
|
55
55
|
status: 'success',
|
|
56
|
-
|
|
56
|
+
name: 'test::task',
|
|
57
57
|
targets: ['host1'],
|
|
58
58
|
submitted_at: '2026-01-01T00:00:00Z',
|
|
59
59
|
completed_at: '2026-01-01T00:01:00Z',
|
|
@@ -22,9 +22,9 @@ describe('useJobPolling', () => {
|
|
|
22
22
|
status: 'success',
|
|
23
23
|
submitted_at: '2026-01-01T00:00:00Z',
|
|
24
24
|
completed_at: '2026-01-01T00:01:00Z',
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
name: 'test::task',
|
|
26
|
+
description: 'A test task',
|
|
27
|
+
parameters: { name: 'nginx' },
|
|
28
28
|
targets: ['host1.example.com'],
|
|
29
29
|
smart_proxy: { id: 1, name: 'proxy1' },
|
|
30
30
|
};
|
|
@@ -60,7 +60,7 @@ describe('useJobPolling', () => {
|
|
|
60
60
|
test('does not fetch result for INVALID status', async () => {
|
|
61
61
|
const statusData = {
|
|
62
62
|
status: 'invalid',
|
|
63
|
-
|
|
63
|
+
name: 'broken::task',
|
|
64
64
|
targets: [],
|
|
65
65
|
};
|
|
66
66
|
|
|
@@ -96,7 +96,7 @@ describe('useJobPolling', () => {
|
|
|
96
96
|
test('handles result fetch failure gracefully', async () => {
|
|
97
97
|
const statusData = {
|
|
98
98
|
status: 'success',
|
|
99
|
-
|
|
99
|
+
name: 'test::task',
|
|
100
100
|
targets: [],
|
|
101
101
|
};
|
|
102
102
|
|
|
@@ -119,7 +119,7 @@ describe('useJobPolling', () => {
|
|
|
119
119
|
test('cancels polling on unmount', async () => {
|
|
120
120
|
const statusData = {
|
|
121
121
|
status: 'running',
|
|
122
|
-
|
|
122
|
+
name: 'long::task',
|
|
123
123
|
targets: [],
|
|
124
124
|
};
|
|
125
125
|
API.get.mockResolvedValue({ data: statusData });
|
|
@@ -145,12 +145,12 @@ describe('useJobPolling', () => {
|
|
|
145
145
|
test('loads metadata only once across multiple polls', async () => {
|
|
146
146
|
API.get
|
|
147
147
|
.mockResolvedValueOnce({
|
|
148
|
-
data: { status: 'running',
|
|
148
|
+
data: { status: 'running', name: 'my::task', targets: ['host1'] },
|
|
149
149
|
})
|
|
150
150
|
.mockResolvedValueOnce({
|
|
151
151
|
data: {
|
|
152
152
|
status: 'success',
|
|
153
|
-
|
|
153
|
+
name: 'changed::name',
|
|
154
154
|
targets: ['host2'],
|
|
155
155
|
},
|
|
156
156
|
})
|
|
@@ -55,9 +55,9 @@ const useJobPolling = jobId => {
|
|
|
55
55
|
// Task metadata only needs to be set once since it never changes
|
|
56
56
|
if (!metadataLoaded.current) {
|
|
57
57
|
setSubmittedAt(statusData.submitted_at || null);
|
|
58
|
-
setTaskName(statusData.
|
|
59
|
-
setTaskDescription(statusData.
|
|
60
|
-
setTaskParameters(statusData.
|
|
58
|
+
setTaskName(statusData.name || null);
|
|
59
|
+
setTaskDescription(statusData.description || null);
|
|
60
|
+
setTaskParameters(statusData.parameters || {});
|
|
61
61
|
setTargets(statusData.targets || []);
|
|
62
62
|
setSmartProxy(statusData.smart_proxy || null);
|
|
63
63
|
metadataLoaded.current = true;
|
|
@@ -18,9 +18,9 @@ afterEach(() => {
|
|
|
18
18
|
const sampleJobs = [
|
|
19
19
|
{
|
|
20
20
|
job_id: 'job-1',
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
name: 'mymod::install',
|
|
22
|
+
description: 'Install a package',
|
|
23
|
+
parameters: { name: 'nginx' },
|
|
24
24
|
status: 'success',
|
|
25
25
|
targets: ['host1.example.com'],
|
|
26
26
|
submitted_at: '2026-01-15T10:00:00Z',
|
|
@@ -29,9 +29,9 @@ const sampleJobs = [
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
job_id: 'job-2',
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
name: 'mymod::mytask',
|
|
33
|
+
description: 'Restart a service',
|
|
34
|
+
parameters: {},
|
|
35
35
|
status: 'running',
|
|
36
36
|
targets: ['host2.example.com'],
|
|
37
37
|
submitted_at: '2026-01-15T11:00:00Z',
|
|
@@ -138,9 +138,9 @@ const TaskHistory = () => {
|
|
|
138
138
|
<Tr key={job.job_id}>
|
|
139
139
|
<Td hasRightBorder>
|
|
140
140
|
<TaskPopover
|
|
141
|
-
taskName={job.
|
|
142
|
-
taskDescription={job.
|
|
143
|
-
taskParameters={job.
|
|
141
|
+
taskName={job.name}
|
|
142
|
+
taskDescription={job.description}
|
|
143
|
+
taskParameters={job.parameters}
|
|
144
144
|
/>
|
|
145
145
|
</Td>
|
|
146
146
|
<Td hasRightBorder>{getStatusLabel(job.status)}</Td>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_openbolt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Overlook InfraTech
|
|
@@ -18,7 +18,7 @@ dependencies:
|
|
|
18
18
|
version: 11.0.0
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: '
|
|
21
|
+
version: '14'
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -28,7 +28,7 @@ dependencies:
|
|
|
28
28
|
version: 11.0.0
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: '
|
|
31
|
+
version: '14'
|
|
32
32
|
description: This plugin adds OpenBolt integration into Foreman, allowing users to
|
|
33
33
|
run tasks and plans present in their environment.
|
|
34
34
|
email:
|
|
@@ -40,6 +40,11 @@ files:
|
|
|
40
40
|
- LICENSE
|
|
41
41
|
- README.md
|
|
42
42
|
- Rakefile
|
|
43
|
+
- app/controllers/api/v2/openbolt_jobs_controller.rb
|
|
44
|
+
- app/controllers/api/v2/openbolt_tasks_controller.rb
|
|
45
|
+
- app/controllers/concerns/foreman_openbolt/common.rb
|
|
46
|
+
- app/controllers/concerns/foreman_openbolt/jobs.rb
|
|
47
|
+
- app/controllers/concerns/foreman_openbolt/tasks.rb
|
|
43
48
|
- app/controllers/foreman_openbolt/task_controller.rb
|
|
44
49
|
- app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb
|
|
45
50
|
- app/lib/actions/foreman_openbolt/poll_task_status.rb
|
|
@@ -49,6 +54,7 @@ files:
|
|
|
49
54
|
- db/migrate/20250819000000_create_openbolt_task_jobs.rb
|
|
50
55
|
- db/migrate/20250925000000_add_command_to_openbolt_task_jobs.rb
|
|
51
56
|
- db/migrate/20251001000000_add_task_description_to_task_jobs.rb
|
|
57
|
+
- db/migrate/20260824000000_remove_view_smart_proxies_openbolt_permission.rb
|
|
52
58
|
- db/seeds.d/001_add_openbolt_feature.rb
|
|
53
59
|
- lib/foreman_openbolt.rb
|
|
54
60
|
- lib/foreman_openbolt/engine.rb
|
|
@@ -61,6 +67,7 @@ files:
|
|
|
61
67
|
- locale/gemspec.rb
|
|
62
68
|
- package.json
|
|
63
69
|
- test/acceptance/acceptance_helper.rb
|
|
70
|
+
- test/acceptance/api_acceptance_helper.rb
|
|
64
71
|
- test/acceptance/docker/docker-compose.yml
|
|
65
72
|
- test/acceptance/docker/foreman/Dockerfile
|
|
66
73
|
- test/acceptance/docker/foreman/entrypoint.sh
|
|
@@ -79,6 +86,10 @@ files:
|
|
|
79
86
|
- test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.json
|
|
80
87
|
- test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.sh
|
|
81
88
|
- test/acceptance/fixtures/openbolt.yml
|
|
89
|
+
- test/acceptance/tests/api/authn_authz_test.rb
|
|
90
|
+
- test/acceptance/tests/api/jobs_test.rb
|
|
91
|
+
- test/acceptance/tests/api/launch_test.rb
|
|
92
|
+
- test/acceptance/tests/api/proxy_tasks_test.rb
|
|
82
93
|
- test/acceptance/tests/error_handling_test.rb
|
|
83
94
|
- test/acceptance/tests/host_selector_test.rb
|
|
84
95
|
- test/acceptance/tests/launch_task_test.rb
|
|
@@ -89,6 +100,8 @@ files:
|
|
|
89
100
|
- test/acceptance/tests/task_history_test.rb
|
|
90
101
|
- test/acceptance/tests/transport_options_test.rb
|
|
91
102
|
- test/test_plugin_helper.rb
|
|
103
|
+
- test/unit/controllers/api/v2/openbolt_jobs_controller_test.rb
|
|
104
|
+
- test/unit/controllers/api/v2/openbolt_tasks_controller_test.rb
|
|
92
105
|
- test/unit/controllers/task_controller_test.rb
|
|
93
106
|
- test/unit/docker/Dockerfile
|
|
94
107
|
- test/unit/docker/docker-compose.yml
|
|
@@ -180,7 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
180
193
|
- !ruby/object:Gem::Version
|
|
181
194
|
version: '0'
|
|
182
195
|
requirements: []
|
|
183
|
-
rubygems_version: 4.0.
|
|
196
|
+
rubygems_version: 4.0.16
|
|
184
197
|
specification_version: 4
|
|
185
198
|
summary: Foreman OpenBolt integration
|
|
186
199
|
test_files: []
|