foreman_remote_execution 8.0.0 → 8.1.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/app/controllers/job_invocations_controller.rb +1 -2
- data/app/controllers/job_templates_controller.rb +1 -1
- data/app/controllers/ui_job_wizard_controller.rb +1 -1
- data/app/helpers/job_invocations_helper.rb +0 -7
- data/app/helpers/remote_execution_helper.rb +1 -1
- data/app/lib/actions/remote_execution/proxy_action.rb +46 -0
- data/app/lib/actions/remote_execution/run_host_job.rb +38 -11
- data/app/lib/actions/remote_execution/run_hosts_job.rb +7 -6
- data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +27 -0
- data/app/models/job_invocation.rb +5 -9
- data/app/models/job_invocation_composer.rb +4 -0
- data/app/models/remote_execution_provider.rb +10 -2
- data/app/models/ssh_execution_provider.rb +1 -0
- data/app/models/template_invocation.rb +1 -0
- data/app/models/template_invocation_event.rb +11 -0
- data/app/views/job_invocations/_form.html.erb +4 -0
- data/app/views/job_invocations/new.html.erb +5 -0
- data/app/views/templates/script/package_action.erb +1 -1
- data/config/routes.rb +5 -5
- data/db/migrate/20220713095705_create_template_invocation_events.rb +17 -0
- data/db/migrate/20220822155946_add_time_to_pickup_to_job_invocation.rb +5 -0
- data/extra/cockpit/foreman-cockpit-session +303 -230
- data/extra/cockpit/foreman-cockpit.service +1 -0
- data/foreman_remote_execution.gemspec +1 -1
- data/lib/foreman_remote_execution/engine.rb +12 -7
- data/lib/foreman_remote_execution/tasks/explain_proxy_selection.rake +131 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/unit/remote_execution_provider_test.rb +22 -0
- data/webpack/JobWizard/JobWizard.js +53 -18
- data/webpack/JobWizard/JobWizard.scss +3 -0
- data/webpack/JobWizard/JobWizardConstants.js +1 -1
- data/webpack/JobWizard/JobWizardHelpers.js +15 -0
- data/webpack/JobWizard/JobWizardPageRerun.js +29 -5
- data/webpack/JobWizard/JobWizardSelectors.js +8 -2
- data/webpack/JobWizard/__tests__/JobWizardPageRerun.test.js +5 -0
- data/webpack/JobWizard/__tests__/fixtures.js +26 -2
- data/webpack/JobWizard/autofill.js +32 -10
- data/webpack/JobWizard/index.js +25 -6
- data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +25 -0
- data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +12 -1
- data/webpack/JobWizard/steps/AdvancedFields/Fields.js +41 -6
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +4 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +6 -2
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +28 -20
- data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +32 -0
- data/webpack/JobWizard/steps/HostsAndInputs/index.js +2 -2
- data/webpack/JobWizard/steps/ReviewDetails/index.js +1 -0
- data/webpack/JobWizard/steps/form/FormHelpers.js +21 -1
- data/webpack/JobWizard/steps/form/Formatter.js +22 -6
- data/webpack/JobWizard/steps/form/ResourceSelect.js +97 -10
- data/webpack/JobWizard/steps/form/SearchSelect.js +2 -2
- data/webpack/JobWizard/steps/form/SelectField.js +4 -0
- data/webpack/JobWizard/submit.js +3 -1
- data/webpack/JobWizard/validation.js +1 -0
- data/webpack/Routes/routes.js +3 -3
- data/webpack/react_app/components/FeaturesDropdown/actions.js +23 -2
- data/webpack/react_app/components/FeaturesDropdown/index.js +2 -0
- data/webpack/react_app/components/HostKebab/KebabItems.js +1 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +5 -0
- data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +51 -59
- data/webpack/react_app/extend/Fills.js +3 -3
- metadata +12 -5
@@ -0,0 +1,131 @@
|
|
1
|
+
namespace :foreman_remote_execution do
|
2
|
+
desc <<~DESC
|
3
|
+
Explains which proxies can be used for remote execution against HOST using a specified PROVIDER.
|
4
|
+
|
5
|
+
* HOST : A scoped search query to find hosts by
|
6
|
+
* PROVIDER : The PROVIDER to scope by
|
7
|
+
* FORMAT : Output format, one of verbose (or unset), json, pretty-json, csv
|
8
|
+
* FOREMAN_USER : Run as if FOREMAN_USER triggered a job (runs as anonymous_admin if unset)
|
9
|
+
* ORGANIZATION : Run in the context of ORGANIZATION (runs in any context if unset)
|
10
|
+
* LOCATION : Run in the context of LOCATION (runs in any context if unset)
|
11
|
+
DESC
|
12
|
+
task :explain_proxy_selection => ['environment'] do
|
13
|
+
options = {}
|
14
|
+
options[:host] = ENV['HOST']
|
15
|
+
options[:provider] = ENV['PROVIDER']
|
16
|
+
|
17
|
+
raise 'Environment variable HOST has to be set' unless options[:host]
|
18
|
+
raise 'Environment variable PROVIDER has to be set' unless options[:provider]
|
19
|
+
|
20
|
+
if ENV['FOREMAN_USER']
|
21
|
+
User.current = User.friendly.find(ENV['FOREMAN_USER'])
|
22
|
+
else
|
23
|
+
User.current = User.anonymous_admin
|
24
|
+
end
|
25
|
+
Location.current = Location.friendly.find(ENV['LOCATION']) if ENV['LOCATION']
|
26
|
+
Organization.current = Organization.friendly.find(ENV['ORGANIZATION']) if ENV['ORGANIZATION']
|
27
|
+
|
28
|
+
selector = ::RemoteExecutionProxySelector.new
|
29
|
+
|
30
|
+
results = Host.search_for(options[:host]).map do |host|
|
31
|
+
host_base = { :host => host }
|
32
|
+
proxies = selector.available_proxies(host, options[:provider])
|
33
|
+
determined_proxy = selector.determine_proxy(host, options[:provider])
|
34
|
+
counts = selector.instance_variable_get('@tasks')
|
35
|
+
counts.default = 0
|
36
|
+
|
37
|
+
strategies = selector.strategies.map do |strategy|
|
38
|
+
base = { :name => strategy, :enabled => !proxies[strategy].nil? }
|
39
|
+
next base if proxies[strategy].nil?
|
40
|
+
|
41
|
+
base.merge(:proxies => proxies[strategy].sort_by { |proxy| counts[proxy] }.map do |proxy|
|
42
|
+
{:proxy => proxy, :count => counts[proxy]}
|
43
|
+
end)
|
44
|
+
end
|
45
|
+
|
46
|
+
case determined_proxy
|
47
|
+
when :not_defined
|
48
|
+
settings = {
|
49
|
+
global_proxy: 'remote_execution_global_proxy',
|
50
|
+
fallback_proxy: 'remote_execution_fallback_proxy',
|
51
|
+
provider: options[:provider],
|
52
|
+
}
|
53
|
+
|
54
|
+
host_base[:detail] = _('Could not use any proxy for the %{provider} job. Consider configuring %{global_proxy}, ' +
|
55
|
+
'%{fallback_proxy} in settings') % settings
|
56
|
+
when :not_available
|
57
|
+
offline_proxies = selector.offline
|
58
|
+
settings = { :count => offline_proxies.count, :proxy_names => offline_proxies.map(&:name).join(', ') }
|
59
|
+
host_base[:detail] = n_('The only applicable proxy %{proxy_names} is down',
|
60
|
+
'All %{count} applicable proxies are down. Tried %{proxy_names}',
|
61
|
+
offline_proxies.count) % settings
|
62
|
+
else
|
63
|
+
winning_strategy = selector.strategies.find { |strategy| !proxies[strategy].empty? && proxies[strategy].include?(determined_proxy) }
|
64
|
+
end
|
65
|
+
|
66
|
+
{
|
67
|
+
:host => host,
|
68
|
+
:strategies => strategies,
|
69
|
+
:selected_proxy => determined_proxy,
|
70
|
+
:winning_strategy => winning_strategy,
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
case ENV['FORMAT']
|
75
|
+
when nil, 'verbose'
|
76
|
+
output_verbose(results, options[:provider])
|
77
|
+
when 'csv'
|
78
|
+
require 'csv'
|
79
|
+
output_csv(results)
|
80
|
+
when 'json'
|
81
|
+
require 'json'
|
82
|
+
puts JSON.generate(results)
|
83
|
+
when 'pretty-json'
|
84
|
+
require 'json'
|
85
|
+
puts JSON.pretty_generate(results)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def output_verbose(results, provider)
|
90
|
+
errors = [:not_defined, :not_available]
|
91
|
+
|
92
|
+
results.each do |host|
|
93
|
+
puts "=> Host #{host[:host]}"
|
94
|
+
host[:strategies].each do |strategy|
|
95
|
+
puts "==> Strategy #{strategy[:name]}"
|
96
|
+
unless strategy[:enabled]
|
97
|
+
puts " strategy is disabled"
|
98
|
+
puts
|
99
|
+
next
|
100
|
+
end
|
101
|
+
if strategy[:proxies].empty?
|
102
|
+
puts " no proxies available using this strategy"
|
103
|
+
else
|
104
|
+
strategy[:proxies].each_with_index do |proxy_record, i|
|
105
|
+
puts " #{i + 1}) #{proxy_record[:proxy]} - #{proxy_record[:count]} tasks"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
puts
|
109
|
+
end
|
110
|
+
if errors.include? host[:selected_proxy]
|
111
|
+
puts host[:detail]
|
112
|
+
else
|
113
|
+
puts "As of now, #{provider} job would use proxy #{host[:selected_proxy]}, determined by strategy #{host[:winning_strategy]}."
|
114
|
+
end
|
115
|
+
puts
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def output_csv(results)
|
120
|
+
writer = CSV.new($stdout)
|
121
|
+
writer << %w(host strategy proxy)
|
122
|
+
results.each do |host|
|
123
|
+
writer << [
|
124
|
+
host[:host].name,
|
125
|
+
host[:winning_strategy],
|
126
|
+
host[:selected_proxy],
|
127
|
+
]
|
128
|
+
end
|
129
|
+
writer.close
|
130
|
+
end
|
131
|
+
end
|
@@ -56,6 +56,28 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
+
describe '.proxy_feature' do
|
60
|
+
# rubocop:disable Naming/ConstantName
|
61
|
+
it 'handles provider subclasses properly' do
|
62
|
+
old = ::RemoteExecutionProvider
|
63
|
+
|
64
|
+
class P2 < old
|
65
|
+
end
|
66
|
+
::RemoteExecutionProvider = P2
|
67
|
+
|
68
|
+
class CustomProvider < ::RemoteExecutionProvider
|
69
|
+
end
|
70
|
+
|
71
|
+
::RemoteExecutionProvider.register('custom', CustomProvider)
|
72
|
+
|
73
|
+
feature = CustomProvider.proxy_feature
|
74
|
+
_(feature).must_equal 'custom'
|
75
|
+
ensure
|
76
|
+
::RemoteExecutionProvider = old
|
77
|
+
end
|
78
|
+
# rubocop:enable Naming/ConstantName
|
79
|
+
end
|
80
|
+
|
59
81
|
describe '.provider_proxy_features' do
|
60
82
|
it 'returns correct values' do
|
61
83
|
RemoteExecutionProvider.stubs(:providers).returns(
|
@@ -25,6 +25,7 @@ import {
|
|
25
25
|
selectIsSubmitting,
|
26
26
|
selectRouterSearch,
|
27
27
|
selectIsLoading,
|
28
|
+
selectJobCategoriesResponse,
|
28
29
|
} from './JobWizardSelectors';
|
29
30
|
import { ScheduleType } from './steps/Schedule/ScheduleType';
|
30
31
|
import { ScheduleFuture } from './steps/Schedule/ScheduleFuture';
|
@@ -34,13 +35,18 @@ import ReviewDetails from './steps/ReviewDetails/';
|
|
34
35
|
import { useValidation } from './validation';
|
35
36
|
import { useAutoFill } from './autofill';
|
36
37
|
import { submit } from './submit';
|
38
|
+
import { generateDefaultDescription } from './JobWizardHelpers';
|
37
39
|
import './JobWizard.scss';
|
38
40
|
|
39
41
|
export const JobWizard = ({ rerunData }) => {
|
42
|
+
const jobCategoriesResponse = useSelector(selectJobCategoriesResponse);
|
40
43
|
const [jobTemplateID, setJobTemplateID] = useState(
|
41
|
-
rerunData?.template_invocations?.[0]?.template_id
|
44
|
+
rerunData?.template_invocations?.[0]?.template_id ||
|
45
|
+
jobCategoriesResponse?.default_template
|
46
|
+
);
|
47
|
+
const [category, setCategory] = useState(
|
48
|
+
rerunData?.job_category || jobCategoriesResponse?.default_category || ''
|
42
49
|
);
|
43
|
-
const [category, setCategory] = useState(rerunData?.job_category || '');
|
44
50
|
const [advancedValues, setAdvancedValues] = useState({ templateValues: {} });
|
45
51
|
const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description https://github.com/theforeman/foreman_remote_execution/pull/605
|
46
52
|
const [scheduleValue, setScheduleValue] = useState(initialScheduleState);
|
@@ -56,6 +62,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
56
62
|
? {
|
57
63
|
search: rerunData?.targeting?.search_query,
|
58
64
|
...rerunData.inputs,
|
65
|
+
...routerSearch,
|
59
66
|
}
|
60
67
|
: routerSearch
|
61
68
|
);
|
@@ -70,6 +77,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
70
77
|
job_template: {
|
71
78
|
name,
|
72
79
|
execution_timeout_interval,
|
80
|
+
time_to_pickup,
|
73
81
|
description_format,
|
74
82
|
job_category,
|
75
83
|
},
|
@@ -78,8 +86,8 @@ export const JobWizard = ({ rerunData }) => {
|
|
78
86
|
concurrency_control = {},
|
79
87
|
},
|
80
88
|
}) => {
|
81
|
-
if (
|
82
|
-
setCategory(
|
89
|
+
if (category !== job_category) {
|
90
|
+
setCategory(job_category);
|
83
91
|
}
|
84
92
|
const advancedTemplateValues = {};
|
85
93
|
const defaultTemplateValues = {};
|
@@ -101,21 +109,19 @@ export const JobWizard = ({ rerunData }) => {
|
|
101
109
|
currentAdvancedValues[input.name] || input?.default || '';
|
102
110
|
});
|
103
111
|
}
|
104
|
-
const generateDefaultDescription = () => {
|
105
|
-
if (description_format) return description_format;
|
106
|
-
const allInputs = [...advancedInputs, ...inputs];
|
107
|
-
if (!allInputs.length) return name;
|
108
|
-
const inputsString = allInputs
|
109
|
-
.map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
|
110
|
-
.join(' ');
|
111
|
-
return `${name} with inputs ${inputsString}`;
|
112
|
-
};
|
113
112
|
return {
|
114
113
|
...currentAdvancedValues,
|
115
114
|
effectiveUserValue: effective_user?.value || '',
|
116
115
|
timeoutToKill: execution_timeout_interval || '',
|
116
|
+
timeToPickup: time_to_pickup || '',
|
117
117
|
templateValues: advancedTemplateValues,
|
118
|
-
description:
|
118
|
+
description:
|
119
|
+
generateDefaultDescription({
|
120
|
+
description_format,
|
121
|
+
advancedInputs,
|
122
|
+
inputs,
|
123
|
+
name,
|
124
|
+
}) || '',
|
119
125
|
isRandomizedOrdering: randomized_ordering,
|
120
126
|
sshUser: ssh_user || '',
|
121
127
|
timeSpan: concurrency_control.time_span || '',
|
@@ -123,6 +129,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
123
129
|
};
|
124
130
|
});
|
125
131
|
},
|
132
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
126
133
|
[category.length]
|
127
134
|
);
|
128
135
|
useEffect(() => {
|
@@ -134,6 +141,9 @@ export const JobWizard = ({ rerunData }) => {
|
|
134
141
|
},
|
135
142
|
job_template: {
|
136
143
|
execution_timeout_interval: rerunData.execution_timeout_interval,
|
144
|
+
description_format: rerunData.description_format,
|
145
|
+
job_category: rerunData.job_category,
|
146
|
+
time_to_pickup: rerunData.time_to_pickup,
|
137
147
|
},
|
138
148
|
randomized_ordering: rerunData.targeting.randomized_ordering,
|
139
149
|
ssh_user: rerunData.ssh_user,
|
@@ -141,18 +151,39 @@ export const JobWizard = ({ rerunData }) => {
|
|
141
151
|
},
|
142
152
|
});
|
143
153
|
}
|
144
|
-
|
154
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
155
|
+
}, [rerunData]);
|
145
156
|
useEffect(() => {
|
146
157
|
if (jobTemplateID) {
|
147
158
|
dispatch(
|
148
159
|
get({
|
149
160
|
key: JOB_TEMPLATE,
|
150
161
|
url: `/ui_job_wizard/template/${jobTemplateID}`,
|
151
|
-
handleSuccess: rerunData
|
162
|
+
handleSuccess: rerunData
|
163
|
+
? ({
|
164
|
+
data: {
|
165
|
+
template_inputs = [],
|
166
|
+
advanced_template_inputs = [],
|
167
|
+
job_template: { name, description_format },
|
168
|
+
},
|
169
|
+
}) => {
|
170
|
+
setAdvancedValues(currentAdvancedValues => ({
|
171
|
+
...currentAdvancedValues,
|
172
|
+
description:
|
173
|
+
generateDefaultDescription({
|
174
|
+
description_format,
|
175
|
+
advancedInputs: advanced_template_inputs,
|
176
|
+
inputs: template_inputs,
|
177
|
+
name,
|
178
|
+
}) || '',
|
179
|
+
}));
|
180
|
+
}
|
181
|
+
: setDefaults,
|
152
182
|
})
|
153
183
|
);
|
154
184
|
}
|
155
|
-
|
185
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
186
|
+
}, [rerunData, jobTemplateID, dispatch]);
|
156
187
|
|
157
188
|
const [valid, setValid] = useValidation({
|
158
189
|
advancedValues,
|
@@ -187,7 +218,9 @@ export const JobWizard = ({ rerunData }) => {
|
|
187
218
|
setJobTemplate={setJobTemplateID}
|
188
219
|
category={category}
|
189
220
|
setCategory={setCategory}
|
190
|
-
isCategoryPreselected={
|
221
|
+
isCategoryPreselected={
|
222
|
+
!!rerunData || !!fills.feature || !!fills.template_id
|
223
|
+
}
|
191
224
|
/>
|
192
225
|
),
|
193
226
|
enableNext: isTemplate,
|
@@ -370,6 +403,7 @@ JobWizard.propTypes = {
|
|
370
403
|
time_span: PropTypes.number,
|
371
404
|
}),
|
372
405
|
execution_timeout_interval: PropTypes.number,
|
406
|
+
time_to_pickup: PropTypes.number,
|
373
407
|
remote_execution_feature_id: PropTypes.string,
|
374
408
|
template_invocations: PropTypes.arrayOf(
|
375
409
|
PropTypes.shape({
|
@@ -379,6 +413,7 @@ JobWizard.propTypes = {
|
|
379
413
|
})
|
380
414
|
),
|
381
415
|
inputs: PropTypes.object,
|
416
|
+
description_format: PropTypes.string,
|
382
417
|
}),
|
383
418
|
};
|
384
419
|
JobWizard.defaultProps = {
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/* eslint-disable camelcase */
|
2
|
+
export const generateDefaultDescription = ({
|
3
|
+
description_format,
|
4
|
+
advancedInputs,
|
5
|
+
inputs,
|
6
|
+
name,
|
7
|
+
}) => {
|
8
|
+
if (description_format) return description_format;
|
9
|
+
const allInputs = [...advancedInputs, ...inputs];
|
10
|
+
if (!allInputs.length) return name;
|
11
|
+
const inputsString = allInputs
|
12
|
+
.map(({ name: inputname }) => `${inputname}="%{${inputname}}"`)
|
13
|
+
.join(' ');
|
14
|
+
return `${name} with inputs ${inputsString}`;
|
15
|
+
};
|
@@ -1,7 +1,15 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import PropTypes from 'prop-types';
|
3
3
|
import URI from 'urijs';
|
4
|
-
import {
|
4
|
+
import {
|
5
|
+
Alert,
|
6
|
+
Title,
|
7
|
+
Divider,
|
8
|
+
Skeleton,
|
9
|
+
Flex,
|
10
|
+
FlexItem,
|
11
|
+
Button,
|
12
|
+
} from '@patternfly/react-core';
|
5
13
|
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
6
14
|
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
7
15
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
@@ -31,7 +39,7 @@ const JobWizardPageRerun = ({
|
|
31
39
|
const title = __('Run job');
|
32
40
|
const breadcrumbOptions = {
|
33
41
|
breadcrumbItems: [
|
34
|
-
{ caption: __('Jobs'), url: `/
|
42
|
+
{ caption: __('Jobs'), url: `/job_invocations` },
|
35
43
|
{ caption: title },
|
36
44
|
],
|
37
45
|
};
|
@@ -48,9 +56,25 @@ const JobWizardPageRerun = ({
|
|
48
56
|
searchable={false}
|
49
57
|
>
|
50
58
|
<React.Fragment>
|
51
|
-
<
|
52
|
-
|
53
|
-
|
59
|
+
<React.Fragment>
|
60
|
+
<Flex>
|
61
|
+
<FlexItem>
|
62
|
+
<Title headingLevel="h2" size="2xl">
|
63
|
+
{title}
|
64
|
+
</Title>
|
65
|
+
</FlexItem>
|
66
|
+
<FlexItem align={{ default: 'alignRight' }}>
|
67
|
+
<Button
|
68
|
+
variant="link"
|
69
|
+
component="a"
|
70
|
+
href={`/old/job_invocations/${id}/rerun${search}`}
|
71
|
+
>
|
72
|
+
{__('Use old form')}
|
73
|
+
</Button>
|
74
|
+
</FlexItem>
|
75
|
+
</Flex>
|
76
|
+
<Divider component="div" />
|
77
|
+
</React.Fragment>
|
54
78
|
{!status || status === STATUS.PENDING ? (
|
55
79
|
<div style={{ height: '400px' }}>
|
56
80
|
<Skeleton
|
@@ -24,11 +24,17 @@ export const filterJobTemplates = templates =>
|
|
24
24
|
export const selectJobTemplates = state =>
|
25
25
|
filterJobTemplates(selectAPIResponse(state, JOB_TEMPLATES)?.results);
|
26
26
|
|
27
|
+
export const selectJobTemplatesSearch = state =>
|
28
|
+
selectAPIResponse(state, JOB_TEMPLATES)?.search;
|
29
|
+
|
30
|
+
export const selectJobCategoriesResponse = state =>
|
31
|
+
selectAPIResponse(state, JOB_CATEGORIES) || {};
|
32
|
+
|
27
33
|
export const selectJobCategories = state =>
|
28
|
-
|
34
|
+
selectJobCategoriesResponse(state).job_categories || [];
|
29
35
|
|
30
36
|
export const selectWithKatello = state =>
|
31
|
-
|
37
|
+
selectJobCategoriesResponse(state).with_katello || false;
|
32
38
|
|
33
39
|
export const selectJobCategoriesStatus = state =>
|
34
40
|
selectAPIStatus(state, JOB_CATEGORIES);
|
@@ -64,6 +64,11 @@ describe('Job wizard fill', () => {
|
|
64
64
|
selector: 'input',
|
65
65
|
}).value
|
66
66
|
).toBe('1');
|
67
|
+
expect(
|
68
|
+
screen.getByLabelText('time to pickup', {
|
69
|
+
selector: 'input',
|
70
|
+
}).value
|
71
|
+
).toBe('25');
|
67
72
|
|
68
73
|
expect(
|
69
74
|
screen.getByLabelText('Concurrency level', {
|
@@ -1,3 +1,4 @@
|
|
1
|
+
/* eslint-disable max-lines */
|
1
2
|
import configureMockStore from 'redux-mock-store';
|
2
3
|
import hostsQuery from '../steps/HostsAndInputs/hosts.gql';
|
3
4
|
import hostgroupsQuery from '../steps/HostsAndInputs/hostgroups.gql';
|
@@ -14,6 +15,18 @@ export const jobTemplate = {
|
|
14
15
|
execution_timeout_interval: 2,
|
15
16
|
description: null,
|
16
17
|
};
|
18
|
+
export const pupptetJobTemplate = {
|
19
|
+
id: 163,
|
20
|
+
name: 'Puppet Agent Disable - Script Default',
|
21
|
+
template:
|
22
|
+
'<% if @host.operatingsystem.family == \'Debian\' -%>\nexport PATH=/opt/puppetlabs/bin:$PATH\n<% end -%>\npuppet agent --disable "<%= input("comment").present? ? input("comment") : "Disabled using Foreman Remote Execution" %> - <%= current_user %> - $(date "+%d/%m/%Y %H:%M")"',
|
23
|
+
snippet: false,
|
24
|
+
default: true,
|
25
|
+
job_category: 'Puppet',
|
26
|
+
provider_type: 'script',
|
27
|
+
execution_timeout_interval: 2,
|
28
|
+
description: null,
|
29
|
+
};
|
17
30
|
|
18
31
|
export const jobTemplates = [jobTemplate];
|
19
32
|
|
@@ -120,6 +133,7 @@ export const testSetup = (selectors, api) => {
|
|
120
133
|
selectors.selectJobCategories.mockImplementation(() => jobCategories);
|
121
134
|
selectors.selectJobTemplates.mockImplementation(() => [
|
122
135
|
jobTemplate,
|
136
|
+
pupptetJobTemplate,
|
123
137
|
{ ...jobTemplate, id: 2, name: 'template2' },
|
124
138
|
]);
|
125
139
|
selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
|
@@ -164,12 +178,21 @@ export const mockApi = api => {
|
|
164
178
|
} else if (action.key === 'JOB_TEMPLATE') {
|
165
179
|
handleSuccess &&
|
166
180
|
handleSuccess({
|
167
|
-
data:
|
181
|
+
data:
|
182
|
+
action.url === '/ui_job_wizard/template/163'
|
183
|
+
? { ...jobTemplateResponse, job_template: pupptetJobTemplate }
|
184
|
+
: jobTemplateResponse,
|
168
185
|
});
|
169
186
|
} else if (action.key === 'JOB_TEMPLATES') {
|
170
187
|
handleSuccess &&
|
171
188
|
handleSuccess({
|
172
|
-
data: {
|
189
|
+
data: {
|
190
|
+
results:
|
191
|
+
action.url.search() ===
|
192
|
+
'?search=job_category%3D%22Puppet%22&per_page=all'
|
193
|
+
? [pupptetJobTemplate]
|
194
|
+
: [jobTemplate],
|
195
|
+
},
|
173
196
|
});
|
174
197
|
} else if (action.key === 'HOST_IDS') {
|
175
198
|
handleSuccess &&
|
@@ -252,6 +275,7 @@ export const jobInvocation = {
|
|
252
275
|
time_span: 4,
|
253
276
|
},
|
254
277
|
execution_timeout_interval: 1,
|
278
|
+
time_to_pickup: 25,
|
255
279
|
remote_execution_feature_id: null,
|
256
280
|
template_invocations: [
|
257
281
|
{
|
@@ -17,19 +17,30 @@ export const useAutoFill = ({
|
|
17
17
|
|
18
18
|
useEffect(() => {
|
19
19
|
if (Object.keys(fills).length) {
|
20
|
-
const {
|
20
|
+
const {
|
21
|
+
'host_ids[]': hostIds,
|
22
|
+
search,
|
23
|
+
feature,
|
24
|
+
template_id: templateID,
|
25
|
+
...rest
|
26
|
+
} = { ...fills };
|
21
27
|
setFills({});
|
22
28
|
if (hostIds) {
|
29
|
+
const hostSearch = Array.isArray(hostIds)
|
30
|
+
? `id = ${hostIds.join(' or id = ')}`
|
31
|
+
: `id = ${hostIds}`;
|
23
32
|
dispatch(
|
24
33
|
get({
|
25
34
|
key: HOST_IDS,
|
26
35
|
url: '/api/hosts',
|
27
|
-
params: {
|
36
|
+
params: {
|
37
|
+
search: hostSearch,
|
38
|
+
},
|
28
39
|
handleSuccess: ({ data }) => {
|
29
40
|
setSelectedTargets(currentTargets => ({
|
30
41
|
...currentTargets,
|
31
|
-
hosts: (data.results || []).map(({ name }) => ({
|
32
|
-
id
|
42
|
+
hosts: (data.results || []).map(({ id, name }) => ({
|
43
|
+
id,
|
33
44
|
name,
|
34
45
|
})),
|
35
46
|
}));
|
@@ -37,9 +48,12 @@ export const useAutoFill = ({
|
|
37
48
|
})
|
38
49
|
);
|
39
50
|
}
|
40
|
-
if (search) {
|
51
|
+
if (search && !hostIds?.length) {
|
41
52
|
setHostsSearchQuery(search);
|
42
53
|
}
|
54
|
+
if (templateID) {
|
55
|
+
setJobTemplateID(+templateID);
|
56
|
+
}
|
43
57
|
if (feature) {
|
44
58
|
dispatch(
|
45
59
|
get({
|
@@ -56,11 +70,19 @@ export const useAutoFill = ({
|
|
56
70
|
const re = /inputs\[(?<input>.*)\]/g;
|
57
71
|
const input = re.exec(key)?.groups?.input;
|
58
72
|
if (input) {
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
73
|
+
if (typeof rest[key] === 'string') {
|
74
|
+
setTemplateValues(prev => ({ ...prev, [input]: rest[key] }));
|
75
|
+
} else {
|
76
|
+
const { value, advanced } = rest[key];
|
77
|
+
if (advanced) {
|
78
|
+
setAdvancedValues(prev => ({
|
79
|
+
...prev,
|
80
|
+
templateValues: { ...prev.templateValues, [input]: value },
|
81
|
+
}));
|
82
|
+
} else {
|
83
|
+
setTemplateValues(prev => ({ ...prev, [input]: value }));
|
84
|
+
}
|
85
|
+
}
|
64
86
|
}
|
65
87
|
});
|
66
88
|
}
|
data/webpack/JobWizard/index.js
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { Title, Divider, Flex, FlexItem, Button } from '@patternfly/react-core';
|
3
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
4
5
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
5
6
|
import { JobWizard } from './JobWizard';
|
6
7
|
|
7
|
-
const JobWizardPage = () => {
|
8
|
+
const JobWizardPage = ({ location: { search } }) => {
|
8
9
|
const title = __('Run job');
|
9
10
|
const breadcrumbOptions = {
|
10
11
|
breadcrumbItems: [
|
11
|
-
{ caption: __('Jobs'), url: `/
|
12
|
+
{ caption: __('Jobs'), url: `/job_invocations` },
|
12
13
|
{ caption: title },
|
13
14
|
],
|
14
15
|
};
|
@@ -19,9 +20,22 @@ const JobWizardPage = () => {
|
|
19
20
|
searchable={false}
|
20
21
|
>
|
21
22
|
<React.Fragment>
|
22
|
-
<
|
23
|
-
|
24
|
-
|
23
|
+
<Flex>
|
24
|
+
<FlexItem>
|
25
|
+
<Title headingLevel="h2" size="2xl">
|
26
|
+
{title}
|
27
|
+
</Title>
|
28
|
+
</FlexItem>
|
29
|
+
<FlexItem align={{ default: 'alignRight' }}>
|
30
|
+
<Button
|
31
|
+
variant="link"
|
32
|
+
component="a"
|
33
|
+
href={`/old/job_invocations/new${search}`}
|
34
|
+
>
|
35
|
+
{__('Use legacy form')}
|
36
|
+
</Button>
|
37
|
+
</FlexItem>
|
38
|
+
</Flex>
|
25
39
|
<Divider component="div" />
|
26
40
|
<JobWizard />
|
27
41
|
</React.Fragment>
|
@@ -29,4 +43,9 @@ const JobWizardPage = () => {
|
|
29
43
|
);
|
30
44
|
};
|
31
45
|
|
46
|
+
JobWizardPage.propTypes = {
|
47
|
+
location: PropTypes.shape({
|
48
|
+
search: PropTypes.string,
|
49
|
+
}).isRequired,
|
50
|
+
};
|
32
51
|
export default JobWizardPage;
|