foreman_remote_execution 4.5.0 → 4.5.4

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/job_invocations_controller.rb +1 -1
  3. data/app/controllers/ui_job_wizard_controller.rb +7 -0
  4. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  5. data/app/helpers/remote_execution_helper.rb +9 -3
  6. data/app/lib/actions/remote_execution/run_host_job.rb +5 -1
  7. data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
  8. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +2 -0
  9. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  10. data/app/models/host_proxy_invocation.rb +4 -0
  11. data/app/models/host_status/execution_status.rb +3 -3
  12. data/app/models/job_invocation.rb +9 -6
  13. data/app/models/job_invocation_composer.rb +4 -4
  14. data/app/models/remote_execution_feature.rb +5 -1
  15. data/app/models/remote_execution_provider.rb +1 -1
  16. data/app/models/setting/remote_execution.rb +2 -2
  17. data/app/models/targeting.rb +5 -1
  18. data/app/views/job_invocations/index.html.erb +1 -1
  19. data/app/views/templates/ssh/module_action.erb +1 -0
  20. data/app/views/templates/ssh/power_action.erb +2 -0
  21. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  22. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  23. data/lib/foreman_remote_execution/engine.rb +0 -2
  24. data/lib/foreman_remote_execution/version.rb +1 -1
  25. data/test/unit/job_invocation_composer_test.rb +14 -1
  26. data/test/unit/job_invocation_test.rb +1 -1
  27. data/webpack/JobWizard/JobWizard.js +28 -8
  28. data/webpack/JobWizard/JobWizard.scss +39 -0
  29. data/webpack/JobWizard/JobWizardConstants.js +10 -0
  30. data/webpack/JobWizard/JobWizardSelectors.js +9 -0
  31. data/webpack/JobWizard/__tests__/fixtures.js +104 -2
  32. data/webpack/JobWizard/__tests__/integration.test.js +13 -85
  33. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +21 -4
  34. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  35. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +73 -59
  36. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +135 -16
  37. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  38. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -51
  39. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  40. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  41. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  42. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  43. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  44. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  45. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  46. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  47. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  48. data/webpack/JobWizard/steps/form/SelectField.js +14 -2
  49. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  50. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  51. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  52. metadata +15 -13
  53. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  54. data/test/models/orchestration/ssh_test.rb +0 -56
  55. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  56. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  57. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +0 -249
  58. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  59. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  60. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  61. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  62. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
@@ -10,5 +10,44 @@
10
10
  .advanced-fields-title {
11
11
  margin-bottom: 10px;
12
12
  }
13
+ #advanced-fields-job-template {
14
+ .foreman-search-field {
15
+ // Giving pf3 search bar a pf4 look
16
+ .search-bar {
17
+ display: block;
18
+ }
19
+ .input-group-btn {
20
+ display: none;
21
+ }
22
+ li {
23
+ font-size: 16px;
24
+ }
25
+ input {
26
+ font-size: 16px;
27
+ height: 36px;
28
+ }
29
+ .foreman-autocomplete .autocomplete-focus-shortcut {
30
+ top: 8px;
31
+ font-size: 16px;
32
+ }
33
+ .foreman-autocomplete .autocomplete-aux {
34
+ top: 8px;
35
+ font-size: 16px;
36
+ .autocomplete-clear-button {
37
+ font-size: 16px;
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ .schedule-tab {
45
+ input[type='radio'],
46
+ input[type='checkbox'] {
47
+ margin: 0;
48
+ }
49
+ .advanced-scheduling-button {
50
+ text-align: start;
51
+ }
13
52
  }
14
53
  }
@@ -1,6 +1,16 @@
1
+ import { translate as __ } from 'foremanReact/common/I18n';
1
2
  import { foremanUrl } from 'foremanReact/common/helpers';
2
3
 
3
4
  export const JOB_TEMPLATES = 'JOB_TEMPLATES';
4
5
  export const JOB_CATEGORIES = 'JOB_CATEGORIES';
5
6
  export const JOB_TEMPLATE = 'JOB_TEMPLATE';
6
7
  export const templatesUrl = foremanUrl('/api/v2/job_templates');
8
+
9
+ export const repeatTypes = {
10
+ noRepeat: __('Does not repeat'),
11
+ cronline: __('Cronline'),
12
+ monthly: __('Monthly'),
13
+ weekly: __('Weekly'),
14
+ daily: __('Daily'),
15
+ hourly: __('Hourly'),
16
+ };
@@ -36,3 +36,12 @@ export const selectTemplateError = state =>
36
36
 
37
37
  export const selectJobTemplate = state =>
38
38
  selectAPIResponse(state, JOB_TEMPLATE);
39
+
40
+ export const selectEffectiveUser = state =>
41
+ selectAPIResponse(state, JOB_TEMPLATE).effective_user;
42
+
43
+ export const selectAdvancedTemplateInputs = state =>
44
+ selectAPIResponse(state, JOB_TEMPLATE).advanced_template_inputs || [];
45
+
46
+ export const selectTemplateInputs = state =>
47
+ selectAPIResponse(state, JOB_TEMPLATE).template_inputs || [];
@@ -1,6 +1,8 @@
1
- const jobTemplate = {
1
+ import configureMockStore from 'redux-mock-store';
2
+
3
+ export const jobTemplate = {
2
4
  id: 178,
3
- name: 'Run Command - Ansible Default',
5
+ name: 'template1',
4
6
  template:
5
7
  "---\n- hosts: all\n tasks:\n - shell:\n cmd: |\n<%= indent(10) { input('command') } %>\n register: out\n - debug: var=out",
6
8
  snippet: false,
@@ -23,4 +25,104 @@ export const jobTemplateResponse = {
23
25
  overridable: true,
24
26
  current_user: false,
25
27
  },
28
+ advanced_template_inputs: [
29
+ {
30
+ name: 'adv plain hidden',
31
+ required: true,
32
+ input_type: 'user',
33
+ description: 'some Description',
34
+ advanced: true,
35
+ value_type: 'plain',
36
+ resource_type: 'ansible_roles',
37
+ default: 'Default val',
38
+ hidden_value: true,
39
+ },
40
+ {
41
+ name: 'adv plain select',
42
+ required: false,
43
+ input_type: 'user',
44
+ options: 'option 1\r\noption 2\r\noption 3\r\noption 4',
45
+ advanced: true,
46
+ value_type: 'plain',
47
+ resource_type: 'ansible_roles',
48
+ default: '',
49
+ hidden_value: false,
50
+ },
51
+ {
52
+ name: 'adv search',
53
+ required: false,
54
+ options: '',
55
+ advanced: true,
56
+ value_type: 'search',
57
+ resource_type: 'foreman_tasks/tasks',
58
+ default: '',
59
+ hidden_value: false,
60
+ },
61
+ {
62
+ name: 'adv date',
63
+ required: false,
64
+ options: '',
65
+ advanced: true,
66
+ value_type: 'date',
67
+ resource_type: 'ansible_roles',
68
+ default: '',
69
+ hidden_value: false,
70
+ },
71
+ ],
72
+ template_inputs: [
73
+ {
74
+ name: 'plain hidden',
75
+ required: true,
76
+ input_type: 'user',
77
+ description: 'some Description',
78
+ advanced: false,
79
+ value_type: 'plain',
80
+ resource_type: 'ansible_roles',
81
+ default: 'Default val',
82
+ hidden_value: true,
83
+ },
84
+ ],
85
+ };
86
+
87
+ export const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];
88
+
89
+ export const testSetup = (selectors, api) => {
90
+ jest.spyOn(api, 'get');
91
+ jest.spyOn(selectors, 'selectJobTemplate');
92
+ jest.spyOn(selectors, 'selectJobTemplates');
93
+ jest.spyOn(selectors, 'selectJobCategories');
94
+ jest.spyOn(selectors, 'selectJobCategoriesStatus');
95
+
96
+ selectors.selectJobCategories.mockImplementation(() => jobCategories);
97
+ selectors.selectJobTemplates.mockImplementation(() => [
98
+ jobTemplate,
99
+ { ...jobTemplate, id: 2, name: 'template2' },
100
+ ]);
101
+ const mockStore = configureMockStore([]);
102
+ const store = mockStore({});
103
+ return store;
104
+ };
105
+
106
+ export const mockTemplate = selectors => {
107
+ selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
108
+ selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
109
+ };
110
+ export const mockApi = api => {
111
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
112
+ if (action.key === 'JOB_CATEGORIES') {
113
+ handleSuccess &&
114
+ handleSuccess({ data: { job_categories: jobCategories } });
115
+ } else if (action.key === 'JOB_TEMPLATE') {
116
+ handleSuccess &&
117
+ handleSuccess({
118
+ data: jobTemplateResponse,
119
+ });
120
+ } else if (action.key === 'JOB_TEMPLATES') {
121
+ handleSuccess &&
122
+ handleSuccess({
123
+ data: { results: [jobTemplate] },
124
+ });
125
+ }
126
+ return { type: 'get', ...action };
127
+ });
26
128
  };
@@ -1,40 +1,27 @@
1
1
  import React from 'react';
2
2
  import { Provider } from 'react-redux';
3
- import configureMockStore from 'redux-mock-store';
4
3
  import { mount } from '@theforeman/test';
5
4
  import { render, fireEvent, screen, act } from '@testing-library/react';
6
5
  import * as api from 'foremanReact/redux/API';
7
6
  import { JobWizard } from '../JobWizard';
8
7
  import * as selectors from '../JobWizardSelectors';
9
- import { jobTemplates, jobTemplateResponse as jobTemplate } from './fixtures';
8
+ import {
9
+ testSetup,
10
+ mockApi,
11
+ jobCategories,
12
+ jobTemplateResponse as jobTemplate,
13
+ } from './fixtures';
10
14
 
11
- jest.spyOn(api, 'get');
12
- jest.spyOn(selectors, 'selectJobTemplate');
13
- jest.spyOn(selectors, 'selectJobTemplates');
14
- jest.spyOn(selectors, 'selectJobCategories');
15
- jest.spyOn(selectors, 'selectJobCategoriesStatus');
15
+ const store = testSetup(selectors, api);
16
16
 
17
- const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];
17
+ selectors.selectJobTemplate.mockImplementation(() => {});
18
18
 
19
19
  api.get.mockImplementation(({ handleSuccess, ...action }) => {
20
20
  if (action.key === 'JOB_CATEGORIES') {
21
21
  handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
22
- } else if (action.key === 'JOB_TEMPLATE') {
23
- handleSuccess &&
24
- handleSuccess({
25
- data: jobTemplate,
26
- });
27
22
  }
28
23
  return { type: 'get', ...action };
29
24
  });
30
-
31
- selectors.selectJobTemplate.mockImplementation(() => null);
32
- selectors.selectJobCategories.mockImplementation(() => jobCategories);
33
- selectors.selectJobCategoriesStatus.mockImplementation(() => null);
34
- selectors.selectJobTemplates.mockImplementation(() => jobTemplates);
35
-
36
- const mockStore = configureMockStore([]);
37
- const store = mockStore({});
38
25
  describe('Job wizard fill', () => {
39
26
  it('should select template', async () => {
40
27
  const wrapper = mount(
@@ -51,7 +38,10 @@ describe('Job wizard fill', () => {
51
38
  selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
52
39
  wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
53
40
  await act(async () => {
54
- await wrapper.find('.pf-c-select__menu-item').simulate('click');
41
+ await wrapper
42
+ .find('.pf-c-select__menu-item')
43
+ .first()
44
+ .simulate('click');
55
45
  await wrapper.update();
56
46
  });
57
47
  expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
@@ -60,74 +50,12 @@ describe('Job wizard fill', () => {
60
50
  );
61
51
  });
62
52
 
63
- it('should save data between steps for advanced fields', async () => {
64
- const wrapper = mount(
65
- <Provider store={store}>
66
- <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
67
- </Provider>
68
- );
69
- // setup
70
- selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
71
- selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
72
- wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
73
- wrapper.find('.pf-c-select__menu-item').simulate('click');
74
-
75
- // test
76
- expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
77
- 0
78
- );
79
- wrapper
80
- .find('.pf-c-wizard__nav-link')
81
- .at(2)
82
- .simulate('click'); // Advanced step
83
- const effectiveUserInput = () => wrapper.find('input#effective-user');
84
- const effectiveUesrValue = 'effective user new value';
85
- effectiveUserInput().getDOMNode().value = effectiveUesrValue;
86
- await act(async () => {
87
- await effectiveUserInput().simulate('change');
88
- wrapper.update();
89
- });
90
-
91
- expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
92
-
93
- wrapper
94
- .find('.pf-c-wizard__nav-link')
95
- .at(1)
96
- .simulate('click');
97
-
98
- expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
99
- 'Target Hosts'
100
- );
101
- wrapper
102
- .find('.pf-c-wizard__nav-link')
103
- .at(2)
104
- .simulate('click'); // Advanced step
105
-
106
- expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
107
- });
108
-
109
53
  it('have all steps', async () => {
110
54
  selectors.selectJobCategoriesStatus.mockImplementation(() => null);
111
55
  selectors.selectJobTemplate.mockRestore();
112
56
  selectors.selectJobTemplates.mockRestore();
113
57
  selectors.selectJobCategories.mockRestore();
114
- api.get.mockImplementation(({ handleSuccess, ...action }) => {
115
- if (action.key === 'JOB_CATEGORIES') {
116
- handleSuccess &&
117
- handleSuccess({ data: { job_categories: jobCategories } });
118
- } else if (action.key === 'JOB_TEMPLATE') {
119
- handleSuccess &&
120
- handleSuccess({
121
- data: jobTemplate,
122
- });
123
- } else if (action.key === 'JOB_TEMPLATES') {
124
- handleSuccess &&
125
- handleSuccess({
126
- data: { results: [jobTemplate.job_template] },
127
- });
128
- }
129
- return { type: 'get', ...action };
130
- });
58
+ mockApi(api);
131
59
 
132
60
  render(
133
61
  <Provider store={store}>
@@ -3,7 +3,11 @@ import PropTypes from 'prop-types';
3
3
  import { useSelector } from 'react-redux';
4
4
  import { Title, Form } from '@patternfly/react-core';
5
5
  import { translate as __ } from 'foremanReact/common/I18n';
6
- import { selectJobTemplate } from '../../JobWizardSelectors';
6
+ import {
7
+ selectEffectiveUser,
8
+ selectAdvancedTemplateInputs,
9
+ selectTemplateInputs,
10
+ } from '../../JobWizardSelectors';
7
11
  import {
8
12
  EffectiveUserField,
9
13
  TimeoutToKillField,
@@ -12,17 +16,25 @@ import {
12
16
  EffectiveUserPasswordField,
13
17
  ConcurrencyLevelField,
14
18
  TimeSpanLevelField,
19
+ TemplateInputsFields,
15
20
  } from './Fields';
21
+ import { DescriptionField } from './DescriptionField';
16
22
 
17
23
  export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
18
- const jobTemplate = useSelector(selectJobTemplate);
19
- const effectiveUser = jobTemplate.effective_user;
24
+ const effectiveUser = useSelector(selectEffectiveUser);
25
+ const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
26
+ const templateInputs = useSelector(selectTemplateInputs);
20
27
  return (
21
28
  <>
22
29
  <Title headingLevel="h2" className="advanced-fields-title">
23
30
  {__('Advanced Fields')}
24
31
  </Title>
25
- <Form>
32
+ <Form id="advanced-fields-job-template" autoComplete="off">
33
+ <TemplateInputsFields
34
+ inputs={advancedTemplateInputs}
35
+ value={advancedValues.templateValues}
36
+ setValue={newValue => setAdvancedValues({ templateValues: newValue })}
37
+ />
26
38
  {effectiveUser?.overridable && (
27
39
  <EffectiveUserField
28
40
  value={advancedValues.effectiveUserValue}
@@ -33,6 +45,11 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
33
45
  }
34
46
  />
35
47
  )}
48
+ <DescriptionField
49
+ inputs={templateInputs}
50
+ value={advancedValues.description}
51
+ setValue={newValue => setAdvancedValues({ description: newValue })}
52
+ />
36
53
  <TimeoutToKillField
37
54
  value={advancedValues.timeoutToKill}
38
55
  setValue={newValue =>
@@ -0,0 +1,67 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, TextInput, Button } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const DescriptionField = ({ inputs, value, setValue }) => {
7
+ const generateDesc = () => {
8
+ let newDesc = value;
9
+ if (value) {
10
+ const re = new RegExp('%\\{([^\\}]+)\\}', 'gm');
11
+ const results = [...newDesc.matchAll(re)].map(result => ({
12
+ name: result[1],
13
+ text: result[0],
14
+ }));
15
+ results.forEach(result => {
16
+ newDesc = newDesc.replace(
17
+ result.text,
18
+ // TODO: Replace with the value of the input from Target Hosts step
19
+ inputs.find(input => input.name === result.name)?.name || result.text
20
+ );
21
+ });
22
+ }
23
+ return newDesc;
24
+ };
25
+ const [generatedDesc, setGeneratedDesc] = useState(generateDesc());
26
+ const [isPreview, setIsPreview] = useState(true);
27
+
28
+ const togglePreview = () => {
29
+ setGeneratedDesc(generateDesc());
30
+ setIsPreview(v => !v);
31
+ };
32
+
33
+ return (
34
+ <FormGroup
35
+ label={__('Description')}
36
+ fieldId="description"
37
+ helperText={
38
+ <Button variant="link" isInline onClick={togglePreview}>
39
+ {isPreview
40
+ ? __('Edit job description template')
41
+ : __('Preview job description')}
42
+ </Button>
43
+ }
44
+ >
45
+ {isPreview ? (
46
+ <TextInput id="description-preview" value={generatedDesc} isDisabled />
47
+ ) : (
48
+ <TextInput
49
+ type="text"
50
+ autoComplete="description"
51
+ id="description"
52
+ value={value}
53
+ onChange={newValue => setValue(newValue)}
54
+ />
55
+ )}
56
+ </FormGroup>
57
+ );
58
+ };
59
+
60
+ DescriptionField.propTypes = {
61
+ inputs: PropTypes.array.isRequired,
62
+ value: PropTypes.string,
63
+ setValue: PropTypes.func.isRequired,
64
+ };
65
+ DescriptionField.defaultProps = {
66
+ value: '',
67
+ };