foreman_remote_execution 4.5.1 → 4.7.0

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