foreman_remote_execution 4.2.3 → 4.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/js_ci.yml +29 -0
  3. data/.github/workflows/{ci.yml → ruby_ci.yml} +22 -50
  4. data/.prettierrc +4 -0
  5. data/.rubocop.yml +13 -49
  6. data/.rubocop_todo.yml +326 -102
  7. data/Gemfile +1 -4
  8. data/app/controllers/api/v2/job_invocations_controller.rb +28 -23
  9. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_commands_controller_extensions.rb +19 -0
  10. data/app/controllers/job_templates_controller.rb +4 -4
  11. data/app/controllers/ui_job_wizard_controller.rb +30 -0
  12. data/app/helpers/job_invocations_helper.rb +2 -2
  13. data/app/helpers/remote_execution_helper.rb +35 -8
  14. data/app/lib/actions/remote_execution/run_host_job.rb +68 -5
  15. data/app/lib/foreman_remote_execution/provider_input.rb +29 -0
  16. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -0
  17. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +5 -5
  18. data/app/models/host_status/execution_status.rb +12 -5
  19. data/app/models/invocation_provider_input_value.rb +12 -0
  20. data/app/models/job_invocation.rb +28 -8
  21. data/app/models/job_invocation_composer.rb +73 -18
  22. data/app/models/remote_execution_provider.rb +18 -3
  23. data/app/models/setting/remote_execution.rb +10 -0
  24. data/app/models/ssh_execution_provider.rb +4 -4
  25. data/app/models/template_invocation.rb +2 -0
  26. data/app/overrides/execution_interface.rb +8 -8
  27. data/app/overrides/subnet_proxies.rb +6 -6
  28. data/app/services/renderer_methods.rb +12 -0
  29. data/app/views/job_invocations/_form.html.erb +8 -0
  30. data/config/routes.rb +4 -0
  31. data/db/migrate/20180110104432_rename_template_invocation_permission.rb +1 -1
  32. data/db/migrate/20190111153330_remove_remote_execution_without_proxy_setting.rb +4 -4
  33. data/db/migrate/20210312074713_add_provider_inputs.rb +10 -0
  34. data/extra/cockpit/foreman-cockpit-session +6 -6
  35. data/foreman_remote_execution.gemspec +1 -3
  36. data/lib/foreman_remote_execution/engine.rb +22 -7
  37. data/lib/foreman_remote_execution/version.rb +1 -1
  38. data/lib/tasks/foreman_remote_execution_tasks.rake +1 -18
  39. data/locale/action_names.rb +1 -0
  40. data/locale/de/foreman_remote_execution.po +77 -27
  41. data/locale/en/foreman_remote_execution.po +77 -27
  42. data/locale/en_GB/foreman_remote_execution.po +77 -27
  43. data/locale/es/foreman_remote_execution.po +77 -27
  44. data/locale/foreman_remote_execution.pot +241 -163
  45. data/locale/fr/foreman_remote_execution.po +77 -27
  46. data/locale/ja/foreman_remote_execution.po +77 -27
  47. data/locale/ko/foreman_remote_execution.po +77 -27
  48. data/locale/pt_BR/foreman_remote_execution.po +77 -27
  49. data/locale/ru/foreman_remote_execution.po +77 -27
  50. data/locale/zh_CN/foreman_remote_execution.po +77 -27
  51. data/locale/zh_TW/foreman_remote_execution.po +77 -27
  52. data/package.json +4 -2
  53. data/test/functional/api/v2/job_invocations_controller_test.rb +29 -6
  54. data/test/functional/api/v2/registration_controller_test.rb +4 -13
  55. data/test/functional/ui_job_wizard_controller_test.rb +16 -0
  56. data/test/helpers/remote_execution_helper_test.rb +16 -0
  57. data/test/unit/job_invocation_composer_test.rb +86 -2
  58. data/test/unit/job_invocation_report_template_test.rb +57 -0
  59. data/webpack/JobWizard/JobWizard.js +96 -0
  60. data/webpack/JobWizard/JobWizard.scss +14 -0
  61. data/webpack/JobWizard/JobWizardConstants.js +6 -0
  62. data/webpack/JobWizard/JobWizardSelectors.js +38 -0
  63. data/webpack/JobWizard/__tests__/JobWizard.test.js +13 -0
  64. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +32 -0
  65. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +43 -0
  66. data/webpack/JobWizard/__tests__/fixtures.js +26 -0
  67. data/webpack/JobWizard/__tests__/integration.test.js +156 -0
  68. data/webpack/JobWizard/index.js +32 -0
  69. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +93 -0
  70. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +181 -0
  71. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +25 -0
  72. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +249 -0
  73. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +109 -0
  74. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +52 -0
  75. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +113 -0
  76. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +94 -0
  77. data/webpack/JobWizard/steps/form/FormHelpers.js +19 -0
  78. data/webpack/JobWizard/steps/form/GroupedSelectField.js +91 -0
  79. data/webpack/JobWizard/steps/form/SelectField.js +48 -0
  80. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +38 -0
  81. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +23 -0
  82. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +37 -0
  83. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +23 -0
  84. data/webpack/Routes/routes.js +12 -0
  85. data/webpack/__mocks__/foremanReact/common/helpers.js +1 -0
  86. data/webpack/__mocks__/foremanReact/history.js +1 -0
  87. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +21 -2
  88. data/webpack/__mocks__/foremanReact/redux/API/index.js +5 -0
  89. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  90. data/webpack/global_index.js +10 -0
  91. data/webpack/index.js +3 -4
  92. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +83 -0
  93. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  94. data/webpack/react_app/components/RecentJobsCard/index.js +1 -0
  95. data/webpack/react_app/components/RecentJobsCard/styles.css +15 -0
  96. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +50 -0
  97. data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +9 -0
  98. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +35 -0
  99. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +1 -1
  100. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +0 -3
  101. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +8 -3
  102. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
  103. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +7 -2
  104. data/webpack/react_app/extend/fillRecentJobsCard.js +11 -0
  105. data/webpack/react_app/extend/fillregistrationAdvanced.js +11 -0
  106. data/webpack/react_app/extend/reducers.js +5 -0
  107. metadata +58 -34
  108. data/app/views/api/v2/registration/_form.html.erb +0 -12
@@ -0,0 +1,94 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useSelector, useDispatch } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import URI from 'urijs';
5
+ import { get } from 'foremanReact/redux/API';
6
+ import {
7
+ selectJobCategories,
8
+ selectJobTemplates,
9
+ selectJobCategoriesStatus,
10
+ filterJobTemplates,
11
+ selectCategoryError,
12
+ selectAllTemplatesError,
13
+ selectTemplateError,
14
+ } from '../../JobWizardSelectors';
15
+ import { CategoryAndTemplate } from './CategoryAndTemplate';
16
+
17
+ import {
18
+ JOB_TEMPLATES,
19
+ JOB_CATEGORIES,
20
+ templatesUrl,
21
+ } from '../../JobWizardConstants';
22
+
23
+ const ConnectedCategoryAndTemplate = ({
24
+ jobTemplate,
25
+ setJobTemplate,
26
+ category,
27
+ setCategory,
28
+ }) => {
29
+ const dispatch = useDispatch();
30
+
31
+ const jobCategoriesStatus = useSelector(selectJobCategoriesStatus);
32
+ useEffect(() => {
33
+ if (!jobCategoriesStatus) {
34
+ // Don't reload categories if they are already loaded
35
+ dispatch(
36
+ get({
37
+ key: JOB_CATEGORIES,
38
+ url: '/ui_job_wizard/categories',
39
+ handleSuccess: response =>
40
+ setCategory(response.data.job_categories[0] || ''),
41
+ })
42
+ );
43
+ }
44
+ }, [jobCategoriesStatus, dispatch, setCategory]);
45
+
46
+ const jobCategories = useSelector(selectJobCategories);
47
+ useEffect(() => {
48
+ if (category) {
49
+ const templatesUrlObject = new URI(templatesUrl);
50
+ dispatch(
51
+ get({
52
+ key: JOB_TEMPLATES,
53
+ url: templatesUrlObject.addSearch({
54
+ search: `job_category="${category}"`,
55
+ per_page: 'all',
56
+ }),
57
+ handleSuccess: response =>
58
+ setJobTemplate(
59
+ Number(filterJobTemplates(response?.data?.results)[0]?.id) || null
60
+ ),
61
+ })
62
+ );
63
+ }
64
+ }, [category, dispatch, setJobTemplate]);
65
+
66
+ const jobTemplates = useSelector(selectJobTemplates);
67
+
68
+ const errors = {
69
+ categoryError: useSelector(selectCategoryError),
70
+ allTemplatesError: useSelector(selectAllTemplatesError),
71
+ templateError: useSelector(selectTemplateError),
72
+ };
73
+ return (
74
+ <CategoryAndTemplate
75
+ jobTemplates={jobTemplates}
76
+ jobCategories={jobCategories}
77
+ setJobTemplate={setJobTemplate}
78
+ selectedTemplateID={jobTemplate}
79
+ setCategory={setCategory}
80
+ selectedCategory={category}
81
+ errors={errors}
82
+ />
83
+ );
84
+ };
85
+
86
+ ConnectedCategoryAndTemplate.propTypes = {
87
+ jobTemplate: PropTypes.number,
88
+ setJobTemplate: PropTypes.func.isRequired,
89
+ category: PropTypes.string.isRequired,
90
+ setCategory: PropTypes.func.isRequired,
91
+ };
92
+ ConnectedCategoryAndTemplate.defaultProps = { jobTemplate: null };
93
+
94
+ export default ConnectedCategoryAndTemplate;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { Popover } from '@patternfly/react-core';
3
+ import { HelpIcon } from '@patternfly/react-icons';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const helpLabel = (text, id) => {
7
+ if (!text) return null;
8
+ return (
9
+ <Popover id={`${id}-help`} bodyContent={text} aria-label="help-text">
10
+ <button
11
+ aria-label={__('open-help-tooltip-button')}
12
+ onClick={e => e.preventDefault()}
13
+ className="pf-c-form__group-label-help"
14
+ >
15
+ <HelpIcon noVerticalAlign />
16
+ </button>
17
+ </Popover>
18
+ );
19
+ };
@@ -0,0 +1,91 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ SelectOption,
5
+ Select,
6
+ SelectGroup,
7
+ SelectVariant,
8
+ FormGroup,
9
+ } from '@patternfly/react-core';
10
+
11
+ export const GroupedSelectField = ({
12
+ label,
13
+ fieldId,
14
+ groups,
15
+ selected,
16
+ setSelected,
17
+ ...props
18
+ }) => {
19
+ const [isOpen, setIsOpen] = useState(false);
20
+ const onSelect = selection => {
21
+ setIsOpen(false);
22
+ setSelected(selection);
23
+ };
24
+
25
+ const onClear = () => {
26
+ onSelect(null);
27
+ };
28
+
29
+ const options = groups.map((group, groupIndex) => (
30
+ <SelectGroup key={groupIndex} label={group.groupLabel}>
31
+ {group.options.map((option, optionIndex) => (
32
+ <SelectOption
33
+ key={optionIndex}
34
+ value={option.label}
35
+ onClick={() => onSelect(option.value)}
36
+ />
37
+ ))}
38
+ </SelectGroup>
39
+ ));
40
+
41
+ const onFilter = evt => {
42
+ const textInput = evt?.target?.value || '';
43
+ if (textInput === '') {
44
+ return options;
45
+ }
46
+ return options
47
+ .map(group => {
48
+ const filteredGroup = React.cloneElement(group, {
49
+ children: group.props.children.filter(item =>
50
+ item.props.value.toLowerCase().includes(textInput.toLowerCase())
51
+ ),
52
+ });
53
+ if (filteredGroup.props.children.length > 0) return filteredGroup;
54
+ return null;
55
+ })
56
+ .filter(newGroup => newGroup);
57
+ };
58
+
59
+ return (
60
+ <FormGroup label={label} fieldId={fieldId}>
61
+ <Select
62
+ isGrouped
63
+ variant={SelectVariant.typeahead}
64
+ onToggle={setIsOpen}
65
+ onFilter={onFilter}
66
+ isOpen={isOpen}
67
+ onSelect={() => null}
68
+ selections={selected}
69
+ className="without_select2"
70
+ onClear={onClear}
71
+ menuAppendTo={() => document.body}
72
+ {...props}
73
+ >
74
+ {options}
75
+ </Select>
76
+ </FormGroup>
77
+ );
78
+ };
79
+
80
+ GroupedSelectField.propTypes = {
81
+ label: PropTypes.string.isRequired,
82
+ fieldId: PropTypes.string.isRequired,
83
+ groups: PropTypes.array,
84
+ selected: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
85
+ setSelected: PropTypes.func.isRequired,
86
+ };
87
+
88
+ GroupedSelectField.defaultProps = {
89
+ groups: [],
90
+ selected: null,
91
+ };
@@ -0,0 +1,48 @@
1
+ import React, { useState } from 'react';
2
+ import { FormGroup, Select, SelectOption } from '@patternfly/react-core';
3
+ import PropTypes from 'prop-types';
4
+
5
+ export const SelectField = ({
6
+ label,
7
+ fieldId,
8
+ options,
9
+ value,
10
+ setValue,
11
+ ...props
12
+ }) => {
13
+ const onSelect = (event, selection) => {
14
+ setValue(selection);
15
+ setIsOpen(false);
16
+ };
17
+ const [isOpen, setIsOpen] = useState(false);
18
+ return (
19
+ <FormGroup label={label} fieldId={fieldId}>
20
+ <Select
21
+ selections={value}
22
+ onSelect={onSelect}
23
+ onToggle={setIsOpen}
24
+ isOpen={isOpen}
25
+ className="without_select2"
26
+ maxHeight="45vh"
27
+ menuAppendTo={() => document.body}
28
+ {...props}
29
+ >
30
+ {options.map((option, index) => (
31
+ <SelectOption key={index} value={option} />
32
+ ))}
33
+ </Select>
34
+ </FormGroup>
35
+ );
36
+ };
37
+ SelectField.propTypes = {
38
+ label: PropTypes.string.isRequired,
39
+ fieldId: PropTypes.string.isRequired,
40
+ options: PropTypes.array,
41
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
42
+ setValue: PropTypes.func.isRequired,
43
+ };
44
+
45
+ SelectField.defaultProps = {
46
+ options: [],
47
+ value: null,
48
+ };
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import * as patternfly from '@patternfly/react-core';
3
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
4
+ import { GroupedSelectField } from '../GroupedSelectField';
5
+
6
+ jest.spyOn(patternfly, 'Select');
7
+ jest.spyOn(patternfly, 'SelectOption');
8
+ patternfly.Select.mockImplementation(props => <div>{props}</div>);
9
+ patternfly.SelectOption.mockImplementation(props => <div>{props}</div>);
10
+
11
+ const fixtures = {
12
+ 'renders with props': {
13
+ label: 'grouped select',
14
+ fieldId: 'field-id',
15
+ groups: [
16
+ {
17
+ groupLabel: 'Ansible',
18
+ options: [
19
+ {
20
+ label: 'Ansible Roles - Ansible Default',
21
+ value: 168,
22
+ },
23
+ {
24
+ label: 'Ansible Roles - Install from git',
25
+ value: 170,
26
+ },
27
+ ],
28
+ },
29
+ ],
30
+ selected: 170,
31
+ setSelected: jest.fn(),
32
+ },
33
+ };
34
+
35
+ describe('GroupedSelectField', () => {
36
+ describe('rendering', () =>
37
+ testComponentSnapshotsWithFixtures(GroupedSelectField, fixtures));
38
+ });
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import * as patternfly from '@patternfly/react-core';
3
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
4
+ import { SelectField } from '../SelectField';
5
+
6
+ jest.spyOn(patternfly, 'Select');
7
+ jest.spyOn(patternfly, 'SelectOption');
8
+ patternfly.Select.mockImplementation(props => <div>{props}</div>);
9
+ patternfly.SelectOption.mockImplementation(props => <div>{props}</div>);
10
+ const fixtures = {
11
+ 'renders with props': {
12
+ label: 'grouped select',
13
+ fieldId: 'field-id',
14
+ options: ['Commands'],
15
+ value: 'Commands',
16
+ setValue: jest.fn(),
17
+ },
18
+ };
19
+
20
+ describe('SelectField', () => {
21
+ describe('rendering', () =>
22
+ testComponentSnapshotsWithFixtures(SelectField, fixtures));
23
+ });
@@ -0,0 +1,37 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`GroupedSelectField rendering renders with props 1`] = `
4
+ <FormGroup
5
+ fieldId="field-id"
6
+ label="grouped select"
7
+ >
8
+ <mockConstructor
9
+ className="without_select2"
10
+ isGrouped={true}
11
+ isOpen={false}
12
+ menuAppendTo={[Function]}
13
+ onClear={[Function]}
14
+ onFilter={[Function]}
15
+ onSelect={[Function]}
16
+ onToggle={[Function]}
17
+ selections={170}
18
+ variant="typeahead"
19
+ >
20
+ <SelectGroup
21
+ key="0"
22
+ label="Ansible"
23
+ >
24
+ <mockConstructor
25
+ key="0"
26
+ onClick={[Function]}
27
+ value="Ansible Roles - Ansible Default"
28
+ />
29
+ <mockConstructor
30
+ key="1"
31
+ onClick={[Function]}
32
+ value="Ansible Roles - Install from git"
33
+ />
34
+ </SelectGroup>
35
+ </mockConstructor>
36
+ </FormGroup>
37
+ `;
@@ -0,0 +1,23 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`SelectField rendering renders with props 1`] = `
4
+ <FormGroup
5
+ fieldId="field-id"
6
+ label="grouped select"
7
+ >
8
+ <mockConstructor
9
+ className="without_select2"
10
+ isOpen={false}
11
+ maxHeight="45vh"
12
+ menuAppendTo={[Function]}
13
+ onSelect={[Function]}
14
+ onToggle={[Function]}
15
+ selections="Commands"
16
+ >
17
+ <mockConstructor
18
+ key="0"
19
+ value="Commands"
20
+ />
21
+ </mockConstructor>
22
+ </FormGroup>
23
+ `;
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import JobWizardPage from '../JobWizard';
3
+
4
+ const ForemanREXRoutes = [
5
+ {
6
+ path: '/experimental/job_wizard',
7
+ exact: true,
8
+ render: props => <JobWizardPage {...props} />,
9
+ },
10
+ ];
11
+
12
+ export default ForemanREXRoutes;
@@ -0,0 +1 @@
1
+ export const foremanUrl = path => `foreman${path}`;
@@ -0,0 +1 @@
1
+ export default { goBack: () => null };
@@ -1,2 +1,21 @@
1
- export const selectAPIStatus = () => 'RESOLVED';
2
- export const selectAPIResponse = state => state;
1
+ export const selectAPI = state => state;
2
+ export const selectAPIByKey = (state, key) => selectAPI(state)[key] || {};
3
+
4
+ export const selectAPIStatus = (state, key) =>
5
+ selectAPIByKey(state, key).status;
6
+
7
+ export const selectAPIPayload = (state, key) =>
8
+ selectAPIByKey(state, key).payload || {};
9
+
10
+ export const selectAPIResponse = (state, key) =>
11
+ selectAPIByKey(state, key).response || {};
12
+
13
+ export const selectAPIError = (state, key) =>
14
+ selectAPIStatus(state, key) === 'ERROR'
15
+ ? selectAPIResponse(state, key)
16
+ : null;
17
+
18
+ export const selectAPIErrorMessage = (state, key) => {
19
+ const error = selectAPIError(state, key);
20
+ return error && error.message;
21
+ };
@@ -0,0 +1,5 @@
1
+ export const API = {
2
+ get: jest.fn(),
3
+ };
4
+
5
+ export const get = data => ({ type: 'get-some-type', ...data });
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ const PageLayout = ({ children }) => <div>{children}</div>;
5
+
6
+ PageLayout.propTypes = {
7
+ children: PropTypes.node.isRequired,
8
+ };
9
+
10
+ export default PageLayout;