foreman_remote_execution 4.5.0 → 4.5.4

Sign up to get free protection for your applications and to get access to all the features.
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
+ };