foreman_remote_execution 4.5.1 → 4.7.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/app/controllers/ui_job_wizard_controller.rb +7 -0
  4. data/app/graphql/types/job_invocation.rb +16 -0
  5. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  6. data/app/helpers/remote_execution_helper.rb +9 -3
  7. data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
  8. data/app/models/job_invocation_composer.rb +3 -3
  9. data/app/models/job_template.rb +1 -1
  10. data/app/models/remote_execution_feature.rb +5 -1
  11. data/app/models/remote_execution_provider.rb +1 -1
  12. data/app/views/templates/ssh/module_action.erb +1 -0
  13. data/app/views/templates/ssh/power_action.erb +2 -0
  14. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  15. data/foreman_remote_execution.gemspec +2 -4
  16. data/lib/foreman_remote_execution/engine.rb +3 -0
  17. data/lib/foreman_remote_execution/version.rb +1 -1
  18. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  19. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  20. data/test/unit/concerns/host_extensions_test.rb +4 -4
  21. data/test/unit/input_template_renderer_test.rb +1 -89
  22. data/test/unit/job_invocation_composer_test.rb +1 -12
  23. data/webpack/JobWizard/JobWizard.js +28 -8
  24. data/webpack/JobWizard/JobWizard.scss +39 -0
  25. data/webpack/JobWizard/JobWizardConstants.js +10 -0
  26. data/webpack/JobWizard/JobWizardSelectors.js +9 -0
  27. data/webpack/JobWizard/__tests__/fixtures.js +104 -2
  28. data/webpack/JobWizard/__tests__/integration.test.js +13 -85
  29. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +21 -4
  30. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  31. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +73 -59
  32. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +135 -16
  33. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  34. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -51
  35. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  36. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  37. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  38. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  39. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  40. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  41. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  42. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  43. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  44. data/webpack/JobWizard/steps/form/SelectField.js +14 -2
  45. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  46. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  47. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  48. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +72 -66
  49. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  50. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  51. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  52. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  53. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  54. metadata +23 -27
  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
  63. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -350,12 +350,6 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
350
350
  end
351
351
 
352
352
  describe '#available_bookmarks' do
353
- it 'obeys authorization' do
354
- composer
355
- Bookmark.expects(:authorized).with(:view_bookmarks).returns(Bookmark.where({}))
356
- composer.available_bookmarks
357
- end
358
-
359
353
  context 'there are hostgroups and hosts bookmark' do
360
354
  let(:hostgroups) { Bookmark.create(:name => 'hostgroups', :query => 'name = x', :controller => 'hostgroups') }
361
355
  let(:hosts) { Bookmark.create(:name => 'hosts', :query => 'name = x', :controller => 'hosts') }
@@ -931,12 +925,7 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
931
925
 
932
926
  context 'with template in setting present' do
933
927
  before do
934
- FactoryBot.create(
935
- :setting,
936
- :name => 'remote_execution_form_job_template',
937
- :category => 'Setting::RemoteExecution',
938
- :value => setting_template.name
939
- )
928
+ Setting[:remote_execution_form_job_template] = setting_template.name
940
929
  end
941
930
 
942
931
  it 'should resolve category to the setting value' do
@@ -1,3 +1,4 @@
1
+ /* eslint-disable camelcase */
1
2
  import React, { useState, useEffect, useCallback } from 'react';
2
3
  import { useDispatch, useSelector } from 'react-redux';
3
4
  import { Wizard } from '@patternfly/react-core';
@@ -8,6 +9,7 @@ import CategoryAndTemplate from './steps/CategoryAndTemplate/';
8
9
  import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
9
10
  import { JOB_TEMPLATE } from './JobWizardConstants';
10
11
  import { selectTemplateError } from './JobWizardSelectors';
12
+ import Schedule from './steps/Schedule/';
11
13
  import './JobWizard.scss';
12
14
 
13
15
  export const JobWizard = () => {
@@ -16,13 +18,31 @@ export const JobWizard = () => {
16
18
  const [advancedValues, setAdvancedValues] = useState({});
17
19
  const dispatch = useDispatch();
18
20
 
19
- const setDefaults = useCallback(response => {
20
- const responseJob = response.data;
21
- setAdvancedValues({
22
- effectiveUserValue: responseJob.effective_user?.value || '',
23
- timeoutToKill: responseJob.job_template.execution_timeout_interval || '',
24
- });
25
- }, []);
21
+ const setDefaults = useCallback(
22
+ ({
23
+ data: {
24
+ advanced_template_inputs,
25
+ effective_user,
26
+ job_template: { executionTimeoutInterval, description_format },
27
+ },
28
+ }) => {
29
+ const advancedTemplateValues = {};
30
+ const advancedInputs = advanced_template_inputs;
31
+ if (advancedInputs) {
32
+ advancedInputs.forEach(input => {
33
+ advancedTemplateValues[input.name] = input?.default || '';
34
+ });
35
+ }
36
+ setAdvancedValues(currentAdvancedValues => ({
37
+ ...currentAdvancedValues,
38
+ effectiveUserValue: effective_user?.value || '',
39
+ timeoutToKill: executionTimeoutInterval || '',
40
+ templateValues: advancedTemplateValues,
41
+ description: description_format || '',
42
+ }));
43
+ },
44
+ []
45
+ );
26
46
  useEffect(() => {
27
47
  if (jobTemplateID) {
28
48
  dispatch(
@@ -72,7 +92,7 @@ export const JobWizard = () => {
72
92
  },
73
93
  {
74
94
  name: __('Schedule'),
75
- component: <p>Schedule</p>,
95
+ component: <Schedule />,
76
96
  canJumpTo: isTemplate,
77
97
  },
78
98
  {
@@ -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 =>