foreman_remote_execution 16.5.2 → 16.6.3
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/Rakefile +0 -15
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +2 -2
- data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +4 -4
- data/app/views/api/v2/smart_proxies/ca_pubkey.json.rabl +1 -1
- data/app/views/api/v2/smart_proxies/pubkey.json.rabl +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/unit/concerns/host_extensions_test.rb +28 -4
- data/test/unit/concerns/smart_proxy_extensions_test.rb +83 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +5 -2
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +6 -3
- data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +1 -0
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +60 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +9 -0
- data/webpack/JobWizard/JobWizard.js +7 -1
- data/webpack/JobWizard/JobWizardSelectors.js +31 -15
- data/webpack/JobWizard/PermissionDenied.js +5 -2
- data/webpack/JobWizard/__tests__/fixtures.js +29 -13
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +2 -2
- data/webpack/react_app/components/RegistrationExtension/RexPull.js +8 -5
- metadata +4 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fd2360fc3d4b48170494112c77f2d07079e94e52c8c6c9b5fcbe2402a690a49
|
|
4
|
+
data.tar.gz: ef36ba8ac16d062f24ee3fd78954d26c86c51001c3b6c8e85dcc1c9d01667159
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96dcba56a99ca8340051cd788eb0115dde2dbcd8ff7cfce5dc82ca6f3679300cebe0c98e74d1fb9c61a09d0e2293d48140996b717114eda87c414968272dcd07
|
|
7
|
+
data.tar.gz: 5e33d478ff9bbaaab3edfac6570e378407a9bf501698ad83b31aecfd0b9261293c1d8229d5cd0f014587478a238341af0fb83defdeb5f58b193d21cd8669335a
|
data/Rakefile
CHANGED
|
@@ -3,21 +3,6 @@ begin
|
|
|
3
3
|
rescue LoadError
|
|
4
4
|
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
5
|
end
|
|
6
|
-
begin
|
|
7
|
-
require 'rdoc/task'
|
|
8
|
-
rescue LoadError
|
|
9
|
-
require 'rdoc/rdoc'
|
|
10
|
-
require 'rake/rdoctask'
|
|
11
|
-
RDoc::Task = Rake::RDocTask
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
RDoc::Task.new(:rdoc) do |rdoc|
|
|
15
|
-
rdoc.rdoc_dir = 'rdoc'
|
|
16
|
-
rdoc.title = 'ForemanRemoteExecution'
|
|
17
|
-
rdoc.options << '--line-numbers'
|
|
18
|
-
rdoc.rdoc_files.include('README.rdoc')
|
|
19
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
20
|
-
end
|
|
21
6
|
|
|
22
7
|
APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
|
|
23
8
|
|
|
@@ -109,11 +109,11 @@ module ForemanRemoteExecution
|
|
|
109
109
|
|
|
110
110
|
def remote_execution_ssh_keys
|
|
111
111
|
# only include public keys from SSH proxies that don't have SSH cert verification configured
|
|
112
|
-
remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.pubkey if proxy.ca_pubkey.blank? }.compact.uniq
|
|
112
|
+
remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.pubkey(refresh: false) if proxy.ca_pubkey(refresh: false).blank? }.compact.uniq
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def remote_execution_ssh_ca_keys
|
|
116
|
-
remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.ca_pubkey }.compact.uniq
|
|
116
|
+
remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.ca_pubkey(refresh: false) }.compact.uniq
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def drop_execution_interface_cache
|
|
@@ -7,12 +7,12 @@ module ForemanRemoteExecution
|
|
|
7
7
|
end
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def pubkey
|
|
11
|
-
self[:pubkey] || update_pubkey
|
|
10
|
+
def pubkey(refresh: true)
|
|
11
|
+
self[:pubkey] || (refresh && update_pubkey || nil)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def ca_pubkey
|
|
15
|
-
self[:ca_pubkey] || update_ca_pubkey
|
|
14
|
+
def ca_pubkey(refresh: true)
|
|
15
|
+
self[:ca_pubkey] || (refresh && update_ca_pubkey || nil)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def update_pubkey
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
node(:remote_execution_ca_pubkey) { |p| p.ca_pubkey(refresh: false) }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
node(:remote_execution_pubkey) { |p| p.pubkey(refresh: false) }
|
|
@@ -11,8 +11,9 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
|
11
11
|
let(:sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQ foo@example.com' }
|
|
12
12
|
|
|
13
13
|
before do
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
host.subnet.remote_execution_proxies.each do |proxy|
|
|
15
|
+
proxy.update(pubkey: sshkey, ca_pubkey: nil)
|
|
16
|
+
end
|
|
16
17
|
Setting[:remote_execution_ssh_user] = 'root'
|
|
17
18
|
Setting[:remote_execution_effective_user_method] = 'sudo'
|
|
18
19
|
end
|
|
@@ -60,6 +61,17 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
|
60
61
|
User.current = nil
|
|
61
62
|
assert_includes host.remote_execution_ssh_keys, sshkey
|
|
62
63
|
end
|
|
64
|
+
|
|
65
|
+
it 'triggers no calls to the proxy' do
|
|
66
|
+
host.subnet.remote_execution_proxies.each do |proxy|
|
|
67
|
+
proxy.update(pubkey: nil)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
SmartProxy.any_instance.expects(:update_pubkey).never
|
|
71
|
+
SmartProxy.any_instance.expects(:update_ca_pubkey).never
|
|
72
|
+
|
|
73
|
+
host.host_param('remote_execution_ssh_keys')
|
|
74
|
+
end
|
|
63
75
|
end
|
|
64
76
|
|
|
65
77
|
describe 'has ssh CA key configured' do
|
|
@@ -68,8 +80,9 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
|
68
80
|
let(:ca_sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJE bar@example.com' }
|
|
69
81
|
|
|
70
82
|
before do
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
host.subnet.remote_execution_proxies.each do |proxy|
|
|
84
|
+
proxy.update(pubkey: sshkey, ca_pubkey: ca_sshkey)
|
|
85
|
+
end
|
|
73
86
|
Setting[:remote_execution_ssh_user] = 'root'
|
|
74
87
|
Setting[:remote_execution_effective_user_method] = 'sudo'
|
|
75
88
|
end
|
|
@@ -102,6 +115,17 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
|
102
115
|
assert_includes host.host_param('remote_execution_ssh_ca_keys'), key
|
|
103
116
|
assert_includes host.host_param('remote_execution_ssh_ca_keys'), ca_sshkey
|
|
104
117
|
end
|
|
118
|
+
|
|
119
|
+
it 'triggers no calls to the proxy' do
|
|
120
|
+
host.subnet.remote_execution_proxies.each do |proxy|
|
|
121
|
+
proxy.update(ca_pubkey: nil)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
SmartProxy.any_instance.expects(:update_pubkey).never
|
|
125
|
+
SmartProxy.any_instance.expects(:update_ca_pubkey).never
|
|
126
|
+
|
|
127
|
+
host.host_param('remote_execution_ssh_ca_keys')
|
|
128
|
+
end
|
|
105
129
|
end
|
|
106
130
|
|
|
107
131
|
context 'host has multiple nics' do
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require 'test_plugin_helper'
|
|
2
|
+
|
|
3
|
+
class ForemanRemoteExecutionSmartProxyExtensionsTest < ActiveSupport::TestCase
|
|
4
|
+
let(:proxy) { FactoryBot.create(:smart_proxy, :ssh) }
|
|
5
|
+
|
|
6
|
+
describe '#pubkey' do
|
|
7
|
+
context 'when key is cached in the database' do
|
|
8
|
+
it 'returns the cached key without fetching from proxy' do
|
|
9
|
+
proxy.expects(:update_pubkey).never
|
|
10
|
+
assert_equal 'ssh-rsa AAAAB3N...', proxy.pubkey
|
|
11
|
+
assert_equal 'ssh-rsa AAAAB3N...', proxy.pubkey(refresh: false)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
context 'when key is not cached' do
|
|
16
|
+
before { proxy.update(pubkey: nil) }
|
|
17
|
+
|
|
18
|
+
it 'fetches from proxy by default' do
|
|
19
|
+
proxy.expects(:update_pubkey).returns('ssh-rsa FETCHED...')
|
|
20
|
+
assert_equal 'ssh-rsa FETCHED...', proxy.pubkey
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'returns nil without fetching when refresh: false' do
|
|
24
|
+
proxy.expects(:update_pubkey).never
|
|
25
|
+
assert_nil proxy.pubkey(refresh: false)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '#ca_pubkey' do
|
|
31
|
+
context 'when key is cached in the database' do
|
|
32
|
+
before { proxy.update(ca_pubkey: 'ssh-rsa CA_KEY...') }
|
|
33
|
+
|
|
34
|
+
it 'returns the cached key without fetching from proxy' do
|
|
35
|
+
proxy.expects(:update_ca_pubkey).never
|
|
36
|
+
assert_equal 'ssh-rsa CA_KEY...', proxy.ca_pubkey
|
|
37
|
+
assert_equal 'ssh-rsa CA_KEY...', proxy.ca_pubkey(refresh: false)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context 'when key is not cached' do
|
|
42
|
+
before { proxy.update(ca_pubkey: nil) }
|
|
43
|
+
|
|
44
|
+
it 'fetches from proxy by default' do
|
|
45
|
+
proxy.expects(:update_ca_pubkey).returns('ssh-rsa FETCHED_CA...')
|
|
46
|
+
assert_equal 'ssh-rsa FETCHED_CA...', proxy.ca_pubkey
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'returns nil without fetching when refresh: false' do
|
|
50
|
+
proxy.expects(:update_ca_pubkey).never
|
|
51
|
+
assert_nil proxy.ca_pubkey(refresh: false)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#refresh' do
|
|
57
|
+
context 'when the key is cached' do
|
|
58
|
+
before do
|
|
59
|
+
proxy.update(pubkey: 'ssh-rsa KEY...', ca_pubkey: 'ssh-rsa CA_KEY...')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'fetches the keys' do
|
|
63
|
+
proxy.expects(:update_pubkey).once
|
|
64
|
+
proxy.expects(:update_ca_pubkey).once
|
|
65
|
+
|
|
66
|
+
proxy.refresh
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
context 'when the key is not cached' do
|
|
71
|
+
before do
|
|
72
|
+
proxy.update(pubkey: nil, ca_pubkey: nil)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'fetches the keys' do
|
|
76
|
+
proxy.expects(:update_pubkey).once
|
|
77
|
+
proxy.expects(:update_ca_pubkey).once
|
|
78
|
+
|
|
79
|
+
proxy.refresh
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
/* eslint-disable camelcase */
|
|
3
3
|
import {
|
|
4
|
+
Icon,
|
|
4
5
|
EmptyState,
|
|
5
6
|
EmptyStateBody,
|
|
6
7
|
EmptyStateHeader,
|
|
7
8
|
EmptyStateVariant,
|
|
8
9
|
ToolbarItem,
|
|
9
10
|
} from '@patternfly/react-core';
|
|
11
|
+
import { AddCircleOIcon } from '@patternfly/react-icons';
|
|
10
12
|
import { ExpandableRowContent, Tbody, Td, Tr } from '@patternfly/react-table';
|
|
11
13
|
import { useDispatch } from 'react-redux';
|
|
12
14
|
import { APIActions } from 'foremanReact/redux/API';
|
|
@@ -22,7 +24,6 @@ import {
|
|
|
22
24
|
import { getPageStats } from 'foremanReact/components/PF4/TableIndexPage/Table/helpers';
|
|
23
25
|
import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage';
|
|
24
26
|
import { getControllerSearchProps } from 'foremanReact/constants';
|
|
25
|
-
import { Icon } from 'patternfly-react';
|
|
26
27
|
import PropTypes from 'prop-types';
|
|
27
28
|
import React, {
|
|
28
29
|
useEffect,
|
|
@@ -339,7 +340,9 @@ const JobInvocationHostTable = ({
|
|
|
339
340
|
<Td colSpan={100}>
|
|
340
341
|
<EmptyState variant={EmptyStateVariant.xl}>
|
|
341
342
|
<span className="empty-state-icon">
|
|
342
|
-
<Icon
|
|
343
|
+
<Icon size="xl" iconSize="xl">
|
|
344
|
+
<AddCircleOIcon name="add-circle-o" />
|
|
345
|
+
</Icon>
|
|
343
346
|
</span>
|
|
344
347
|
<EmptyStateHeader
|
|
345
348
|
titleText={<>{__('No Results')}</>}
|
|
@@ -11,7 +11,7 @@ import { translate as __ } from 'foremanReact/common/I18n';
|
|
|
11
11
|
export const OutputCodeBlock = ({ code, showOutputType, scrollElement }) => {
|
|
12
12
|
let lineCounter = 0;
|
|
13
13
|
// eslint-disable-next-line no-control-regex
|
|
14
|
-
const COLOR_PATTERN = /\x1b\[
|
|
14
|
+
const COLOR_PATTERN = /\x1b\[[\d;]*m/g;
|
|
15
15
|
const CONSOLE_COLOR = {
|
|
16
16
|
'31': 'red',
|
|
17
17
|
'32': 'lightgreen',
|
|
@@ -27,12 +27,15 @@ export const OutputCodeBlock = ({ code, showOutputType, scrollElement }) => {
|
|
|
27
27
|
'95': 'violet',
|
|
28
28
|
'96': 'turquoise',
|
|
29
29
|
'0': 'default',
|
|
30
|
+
'39': 'default',
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
const colorizeLine = line => {
|
|
33
34
|
line = line.replace(COLOR_PATTERN, seq => {
|
|
34
|
-
const
|
|
35
|
-
|
|
35
|
+
const codes = seq.match(/(\d+)/g) || [];
|
|
36
|
+
const lastColorCode =
|
|
37
|
+
[...codes].reverse().find(code_ => code_ in CONSOLE_COLOR) || '0';
|
|
38
|
+
return `{{{format color:${lastColorCode}}}}`;
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
let currentColor = 'default';
|
|
@@ -31,6 +31,7 @@ describe('OutputCodeBlock', () => {
|
|
|
31
31
|
expect(screen.getByText('This is green text')).toHaveStyle(
|
|
32
32
|
'color: lightgreen'
|
|
33
33
|
);
|
|
34
|
+
expect(screen.getByText('Compound red text')).toHaveStyle('color: red');
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
test('displays no output message when filtered', () => {
|
|
@@ -11,6 +11,10 @@ import { mockTemplateInvocationResponse } from './fixtures';
|
|
|
11
11
|
jest.spyOn(api, 'get');
|
|
12
12
|
jest.mock('../JobInvocationSelectors');
|
|
13
13
|
|
|
14
|
+
jest.mock('foremanReact/components/ToastsList', () => ({
|
|
15
|
+
addToast: jest.fn(payload => ({ type: 'ADD_TOAST', payload })),
|
|
16
|
+
}));
|
|
17
|
+
|
|
14
18
|
const mockStore = configureMockStore([]);
|
|
15
19
|
const store = mockStore({
|
|
16
20
|
HOSTS_API: {
|
|
@@ -186,4 +190,60 @@ describe('TemplateInvocation', () => {
|
|
|
186
190
|
await screen.findByText('Successfully copied to clipboard!')
|
|
187
191
|
).toBeInTheDocument();
|
|
188
192
|
});
|
|
193
|
+
|
|
194
|
+
describe('Cancel/Abort task buttons API calls', () => {
|
|
195
|
+
const responseWithCancellableTask = {
|
|
196
|
+
...mockTemplateInvocationResponse,
|
|
197
|
+
task: { id: 'task-123', cancellable: true },
|
|
198
|
+
permissions: {
|
|
199
|
+
view_foreman_tasks: true,
|
|
200
|
+
cancel_job_invocations: true,
|
|
201
|
+
execute_jobs: true,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
|
207
|
+
'RESOLVED'
|
|
208
|
+
);
|
|
209
|
+
selectors.selectTemplateInvocation.mockImplementation(() => () =>
|
|
210
|
+
responseWithCancellableTask
|
|
211
|
+
);
|
|
212
|
+
jest.spyOn(api.APIActions, 'post').mockReturnValue({ type: 'MOCK_POST' });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('clicking the `Cancel Task` button calls API with cancel param', () => {
|
|
216
|
+
render(
|
|
217
|
+
<Provider store={store}>
|
|
218
|
+
<TemplateInvocation {...mockProps} />
|
|
219
|
+
</Provider>
|
|
220
|
+
);
|
|
221
|
+
fireEvent.click(screen.getByText('Cancel Task'));
|
|
222
|
+
|
|
223
|
+
const postCall = api.APIActions.post.mock.calls.find(
|
|
224
|
+
call => call[0].key === 'CANCEL_TASK'
|
|
225
|
+
)?.[0];
|
|
226
|
+
expect(postCall.url).toBe(
|
|
227
|
+
`/foreman_tasks/tasks/${responseWithCancellableTask.task.id}/cancel`
|
|
228
|
+
);
|
|
229
|
+
expect(postCall.key).toBe('CANCEL_TASK');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('clicking the `Abort Task` button calls API with abort param', () => {
|
|
233
|
+
render(
|
|
234
|
+
<Provider store={store}>
|
|
235
|
+
<TemplateInvocation {...mockProps} />
|
|
236
|
+
</Provider>
|
|
237
|
+
);
|
|
238
|
+
fireEvent.click(screen.getByText('Abort task'));
|
|
239
|
+
|
|
240
|
+
const postCall = api.APIActions.post.mock.calls.find(
|
|
241
|
+
call => call[0].key === 'ABORT_TASK'
|
|
242
|
+
)?.[0];
|
|
243
|
+
expect(postCall.url).toBe(
|
|
244
|
+
`/foreman_tasks/tasks/${responseWithCancellableTask.task.id}/abort`
|
|
245
|
+
);
|
|
246
|
+
expect(postCall.key).toBe('ABORT_TASK');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
189
249
|
});
|
|
@@ -196,6 +196,15 @@ export const jobInvocationOutput = [
|
|
|
196
196
|
timestamp: 1733931148.2044532,
|
|
197
197
|
},
|
|
198
198
|
|
|
199
|
+
{
|
|
200
|
+
id: 1960,
|
|
201
|
+
template_invocation_id: templateInvocationID,
|
|
202
|
+
timestamp: 1733931149.2044532,
|
|
203
|
+
meta: null,
|
|
204
|
+
external_id: '0',
|
|
205
|
+
output_type: 'stdout',
|
|
206
|
+
output: '\u001b[0;31mCompound red text\u001b[0m\n',
|
|
207
|
+
},
|
|
199
208
|
{
|
|
200
209
|
id: 1907,
|
|
201
210
|
template_invocation_id: templateInvocationID,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
/* eslint-disable camelcase */
|
|
3
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
4
|
import { useDispatch, useSelector } from 'react-redux';
|
|
5
5
|
import PropTypes from 'prop-types';
|
|
6
6
|
import { Wizard } from '@patternfly/react-core/deprecated';
|
|
@@ -51,6 +51,8 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
51
51
|
const [category, setCategory] = useState(
|
|
52
52
|
rerunData?.job_category || jobCategoriesResponse?.default_category || ''
|
|
53
53
|
);
|
|
54
|
+
const categoryRef = useRef(category);
|
|
55
|
+
categoryRef.current = category;
|
|
54
56
|
const [advancedValues, setAdvancedValues] = useState({ templateValues: {} });
|
|
55
57
|
const [templateValues, setTemplateValues] = useState({});
|
|
56
58
|
const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
|
|
@@ -89,6 +91,10 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
89
91
|
concurrency_control = {},
|
|
90
92
|
},
|
|
91
93
|
}) => {
|
|
94
|
+
if (categoryRef.current !== job_category) {
|
|
95
|
+
setCategory(job_category);
|
|
96
|
+
}
|
|
97
|
+
|
|
92
98
|
const advancedTemplateValues = {};
|
|
93
99
|
const defaultTemplateValues = {};
|
|
94
100
|
const inputs = template_inputs;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
1
2
|
import URI from 'urijs';
|
|
2
3
|
import { get } from 'lodash';
|
|
4
|
+
import { createSelector } from 'reselect';
|
|
3
5
|
import {
|
|
4
6
|
selectAPIResponse,
|
|
5
7
|
selectAPIStatus,
|
|
@@ -17,8 +19,12 @@ import {
|
|
|
17
19
|
JOB_API_KEY,
|
|
18
20
|
} from './JobWizardConstants';
|
|
19
21
|
|
|
22
|
+
/** Stable fallbacks so useSelector does not see a new reference every run */
|
|
23
|
+
const EMPTY_ARRAY = [];
|
|
24
|
+
const EMPTY_OBJECT = {};
|
|
25
|
+
|
|
20
26
|
export const selectRerunJobInvocationResponse = state =>
|
|
21
|
-
selectAPIResponse(state, JOB_API_KEY) ||
|
|
27
|
+
selectAPIResponse(state, JOB_API_KEY) || EMPTY_OBJECT;
|
|
22
28
|
|
|
23
29
|
export const selectRerunJobInvocationStatus = state =>
|
|
24
30
|
selectAPIStatus(state, JOB_API_KEY);
|
|
@@ -27,19 +33,26 @@ export const selectJobTemplatesStatus = state =>
|
|
|
27
33
|
selectAPIStatus(state, JOB_TEMPLATES);
|
|
28
34
|
|
|
29
35
|
export const filterJobTemplates = templates =>
|
|
30
|
-
templates?.filter(template => !template.snippet) ||
|
|
36
|
+
templates?.filter(template => !template.snippet) || EMPTY_ARRAY;
|
|
37
|
+
|
|
38
|
+
const selectJobTemplatesResults = state =>
|
|
39
|
+
selectAPIResponse(state, JOB_TEMPLATES)?.results;
|
|
31
40
|
|
|
32
|
-
export const selectJobTemplates =
|
|
33
|
-
|
|
41
|
+
export const selectJobTemplates = createSelector(
|
|
42
|
+
[selectJobTemplatesResults],
|
|
43
|
+
results => filterJobTemplates(results)
|
|
44
|
+
);
|
|
34
45
|
|
|
35
46
|
export const selectJobTemplatesSearch = state =>
|
|
36
47
|
selectAPIResponse(state, JOB_TEMPLATES)?.search;
|
|
37
48
|
|
|
38
49
|
export const selectJobCategoriesResponse = state =>
|
|
39
|
-
selectAPIResponse(state, JOB_CATEGORIES) ||
|
|
50
|
+
selectAPIResponse(state, JOB_CATEGORIES) || EMPTY_OBJECT;
|
|
40
51
|
|
|
41
|
-
export const selectJobCategories = state =>
|
|
42
|
-
selectJobCategoriesResponse(state)
|
|
52
|
+
export const selectJobCategories = state => {
|
|
53
|
+
const { job_categories: jobCategories } = selectJobCategoriesResponse(state);
|
|
54
|
+
return jobCategories || EMPTY_ARRAY;
|
|
55
|
+
};
|
|
43
56
|
|
|
44
57
|
export const selectWithKatello = state =>
|
|
45
58
|
selectJobCategoriesResponse(state).with_katello || false;
|
|
@@ -58,7 +71,7 @@ export const selectJobCategoriesMissingPermissions = state => {
|
|
|
58
71
|
'data',
|
|
59
72
|
'error',
|
|
60
73
|
'missing_permissions',
|
|
61
|
-
]) ||
|
|
74
|
+
]) || EMPTY_ARRAY
|
|
62
75
|
);
|
|
63
76
|
};
|
|
64
77
|
|
|
@@ -75,29 +88,32 @@ export const selectEffectiveUser = state =>
|
|
|
75
88
|
selectAPIResponse(state, JOB_TEMPLATE).effective_user;
|
|
76
89
|
|
|
77
90
|
export const selectAdvancedTemplateInputs = state =>
|
|
78
|
-
selectAPIResponse(state, JOB_TEMPLATE)
|
|
91
|
+
selectAPIResponse(state, JOB_TEMPLATE)?.advanced_template_inputs ||
|
|
92
|
+
EMPTY_ARRAY;
|
|
79
93
|
|
|
80
94
|
export const selectTemplateInputs = state =>
|
|
81
|
-
selectAPIResponse(state, JOB_TEMPLATE)
|
|
95
|
+
selectAPIResponse(state, JOB_TEMPLATE)?.template_inputs || EMPTY_ARRAY;
|
|
82
96
|
|
|
83
97
|
export const selectHostsResponse = state => selectAPIResponse(state, HOSTS_API);
|
|
84
98
|
|
|
85
99
|
export const selectHostCount = state =>
|
|
86
100
|
selectHostsResponse(state).subtotal || 0;
|
|
87
101
|
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
const selectHostsResults = state => selectHostsResponse(state).results;
|
|
103
|
+
|
|
104
|
+
export const selectHosts = createSelector([selectHostsResults], results => {
|
|
105
|
+
const hosts = results || EMPTY_ARRAY;
|
|
90
106
|
return hosts.map(host => ({
|
|
91
107
|
name: host.name,
|
|
92
108
|
display_name: host.display_name,
|
|
93
109
|
}));
|
|
94
|
-
};
|
|
110
|
+
});
|
|
95
111
|
|
|
96
112
|
export const selectHostsMissingPermissions = state => {
|
|
97
113
|
const hostsResponse = selectHostsResponse(state);
|
|
98
114
|
return (
|
|
99
115
|
get(hostsResponse, ['response', 'data', 'error', 'missing_permissions']) ||
|
|
100
|
-
|
|
116
|
+
EMPTY_ARRAY
|
|
101
117
|
);
|
|
102
118
|
};
|
|
103
119
|
|
|
@@ -115,6 +131,6 @@ export const selectIsSubmitting = state =>
|
|
|
115
131
|
selectAPIStatus(state, JOB_INVOCATION) === STATUS.RESOLVED;
|
|
116
132
|
|
|
117
133
|
export const selectRouterSearch = state => {
|
|
118
|
-
const { search } = selectRouterLocation(state);
|
|
134
|
+
const { search } = selectRouterLocation(state) || {};
|
|
119
135
|
return URI.parseQuery(search);
|
|
120
136
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
|
-
import {
|
|
4
|
+
import { LockIcon } from '@patternfly/react-icons';
|
|
5
5
|
import {
|
|
6
|
+
Icon,
|
|
6
7
|
Button,
|
|
7
8
|
EmptyState,
|
|
8
9
|
EmptyStateVariant,
|
|
@@ -36,7 +37,9 @@ const PermissionDenied = ({ missingPermissions, setProceedAnyway }) => {
|
|
|
36
37
|
return (
|
|
37
38
|
<EmptyState variant={EmptyStateVariant.xl}>
|
|
38
39
|
<span className="empty-state-icon">
|
|
39
|
-
<Icon
|
|
40
|
+
<Icon size="xl" iconSize="xl">
|
|
41
|
+
<LockIcon name="lock" />
|
|
42
|
+
</Icon>
|
|
40
43
|
</span>
|
|
41
44
|
<EmptyStateHeader
|
|
42
45
|
titleText={<>{__('Permission Denied')}</>}
|
|
@@ -30,6 +30,12 @@ export const pupptetJobTemplate = {
|
|
|
30
30
|
|
|
31
31
|
export const jobTemplates = [jobTemplate];
|
|
32
32
|
|
|
33
|
+
export const wizardJobTemplatesMockList = [
|
|
34
|
+
jobTemplate,
|
|
35
|
+
pupptetJobTemplate,
|
|
36
|
+
{ ...jobTemplate, id: 2, name: 'template2' },
|
|
37
|
+
];
|
|
38
|
+
|
|
33
39
|
export const jobTemplateResponse = {
|
|
34
40
|
job_template: jobTemplate,
|
|
35
41
|
effective_user: {
|
|
@@ -137,11 +143,9 @@ export const testSetup = (selectors, api) => {
|
|
|
137
143
|
() => jobTemplateResponse.advanced_template_inputs
|
|
138
144
|
);
|
|
139
145
|
selectors.selectJobCategories.mockImplementation(() => jobCategories);
|
|
140
|
-
selectors.selectJobTemplates.mockImplementation(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{ ...jobTemplate, id: 2, name: 'template2' },
|
|
144
|
-
]);
|
|
146
|
+
selectors.selectJobTemplates.mockImplementation(
|
|
147
|
+
() => wizardJobTemplatesMockList
|
|
148
|
+
);
|
|
145
149
|
selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
|
|
146
150
|
|
|
147
151
|
selectors.selectEffectiveUser.mockImplementation(
|
|
@@ -187,13 +191,25 @@ export const mockApi = api => {
|
|
|
187
191
|
},
|
|
188
192
|
});
|
|
189
193
|
} else if (action.key === 'JOB_TEMPLATE') {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
194
|
+
const url = String(action.url);
|
|
195
|
+
let templatePayload = jobTemplateResponse;
|
|
196
|
+
if (url.includes('/template/163')) {
|
|
197
|
+
templatePayload = {
|
|
198
|
+
...jobTemplateResponse,
|
|
199
|
+
job_template: pupptetJobTemplate,
|
|
200
|
+
};
|
|
201
|
+
} else if (url.includes('/template/2')) {
|
|
202
|
+
const jobTemplate2 = { ...jobTemplate, id: 2, name: 'template2' };
|
|
203
|
+
templatePayload = {
|
|
204
|
+
...jobTemplateResponse,
|
|
205
|
+
job_template: jobTemplate2,
|
|
206
|
+
effective_user: {
|
|
207
|
+
...jobTemplateResponse.effective_user,
|
|
208
|
+
job_template_id: 2,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
handleSuccess && handleSuccess({ data: templatePayload });
|
|
197
213
|
} else if (action.key === 'JOB_TEMPLATES') {
|
|
198
214
|
handleSuccess &&
|
|
199
215
|
handleSuccess({
|
|
@@ -202,7 +218,7 @@ export const mockApi = api => {
|
|
|
202
218
|
action.url.search() ===
|
|
203
219
|
'?search=job_category%3D%22Puppet%22&per_page=all'
|
|
204
220
|
? [pupptetJobTemplate]
|
|
205
|
-
:
|
|
221
|
+
: wizardJobTemplatesMockList,
|
|
206
222
|
},
|
|
207
223
|
});
|
|
208
224
|
} else if (action.key === 'HOST_IDS') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
1
|
+
import React, { useEffect, memo } from 'react';
|
|
2
2
|
import { useSelector, useDispatch } from 'react-redux';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import URI from 'urijs';
|
|
@@ -133,4 +133,4 @@ ConnectedCategoryAndTemplate.propTypes = {
|
|
|
133
133
|
};
|
|
134
134
|
ConnectedCategoryAndTemplate.defaultProps = { jobTemplate: null };
|
|
135
135
|
|
|
136
|
-
export default ConnectedCategoryAndTemplate;
|
|
136
|
+
export default memo(ConnectedCategoryAndTemplate);
|
|
@@ -4,9 +4,8 @@ import PropTypes from 'prop-types';
|
|
|
4
4
|
|
|
5
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
6
6
|
import LabelIcon from 'foremanReact/components/common/LabelIcon';
|
|
7
|
-
import { Alert } from 'patternfly-react';
|
|
8
|
-
|
|
9
7
|
import {
|
|
8
|
+
Alert,
|
|
10
9
|
FormGroup,
|
|
11
10
|
FormSelectOption,
|
|
12
11
|
FormSelect,
|
|
@@ -26,11 +25,15 @@ const options = (value = '') => {
|
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
const pullWarning = (
|
|
29
|
-
<Alert
|
|
30
|
-
|
|
28
|
+
<Alert
|
|
29
|
+
ouiaId="overrideAlert"
|
|
30
|
+
variant="info"
|
|
31
|
+
isInline
|
|
32
|
+
title={__(
|
|
31
33
|
'Please make sure that the Smart Proxy is configured correctly for the Pull provider.'
|
|
32
34
|
)}
|
|
33
|
-
|
|
35
|
+
style={{ marginTop: '10px' }}
|
|
36
|
+
/>
|
|
34
37
|
);
|
|
35
38
|
|
|
36
39
|
function showPullWarning(valueFromParam, value) {
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_remote_execution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 16.
|
|
4
|
+
version: 16.6.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Remote Execution team
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-05-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: deface
|
|
@@ -37,34 +37,6 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: 8.3.0
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: factory_bot_rails
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: 4.8.0
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: 4.8.0
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: rdoc
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - ">="
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0'
|
|
61
|
-
type: :development
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - ">="
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0'
|
|
68
40
|
description: A plugin bringing remote execution to the Foreman, completing the config
|
|
69
41
|
management functionality with remote management functionality.
|
|
70
42
|
email:
|
|
@@ -390,6 +362,7 @@ files:
|
|
|
390
362
|
- test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb
|
|
391
363
|
- test/unit/concerns/host_extensions_test.rb
|
|
392
364
|
- test/unit/concerns/nic_extensions_test.rb
|
|
365
|
+
- test/unit/concerns/smart_proxy_extensions_test.rb
|
|
393
366
|
- test/unit/execution_task_status_mapper_test.rb
|
|
394
367
|
- test/unit/input_template_renderer_test.rb
|
|
395
368
|
- test/unit/job_invocation_composer_test.rb
|
|
@@ -588,7 +561,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
588
561
|
- !ruby/object:Gem::Version
|
|
589
562
|
version: '0'
|
|
590
563
|
requirements: []
|
|
591
|
-
rubygems_version: 4.0.
|
|
564
|
+
rubygems_version: 4.0.10
|
|
592
565
|
specification_version: 4
|
|
593
566
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|
|
594
567
|
management functionality with remote management functionality.
|