foreman_remote_execution 4.2.1 → 4.4.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 (90) 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 +17 -1
  9. data/app/controllers/foreman_remote_execution/concerns/api/v2/registration_commands_controller_extensions.rb +19 -0
  10. data/app/controllers/ui_job_wizard_controller.rb +18 -0
  11. data/app/helpers/remote_execution_helper.rb +27 -0
  12. data/app/lib/actions/remote_execution/run_host_job.rb +38 -1
  13. data/app/lib/foreman_remote_execution/provider_input.rb +29 -0
  14. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +1 -0
  15. data/app/models/foreign_input_set.rb +1 -1
  16. data/app/models/host_status/execution_status.rb +7 -0
  17. data/app/models/invocation_provider_input_value.rb +12 -0
  18. data/app/models/job_invocation.rb +5 -1
  19. data/app/models/job_invocation_composer.rb +13 -0
  20. data/app/models/remote_execution_provider.rb +17 -2
  21. data/app/models/setting/remote_execution.rb +10 -0
  22. data/app/models/template_invocation.rb +2 -0
  23. data/app/services/renderer_methods.rb +12 -0
  24. data/app/views/job_invocations/_form.html.erb +8 -0
  25. data/app/views/template_invocations/show.html.erb +30 -23
  26. data/config/routes.rb +4 -0
  27. data/db/migrate/20210312074713_add_provider_inputs.rb +10 -0
  28. data/foreman_remote_execution.gemspec +1 -2
  29. data/lib/foreman_remote_execution/engine.rb +17 -7
  30. data/lib/foreman_remote_execution/version.rb +1 -1
  31. data/lib/tasks/foreman_remote_execution_tasks.rake +1 -18
  32. data/locale/action_names.rb +1 -0
  33. data/locale/de/foreman_remote_execution.po +77 -27
  34. data/locale/en/foreman_remote_execution.po +77 -27
  35. data/locale/en_GB/foreman_remote_execution.po +77 -27
  36. data/locale/es/foreman_remote_execution.po +77 -27
  37. data/locale/foreman_remote_execution.pot +241 -163
  38. data/locale/fr/foreman_remote_execution.po +77 -27
  39. data/locale/ja/foreman_remote_execution.po +77 -27
  40. data/locale/ko/foreman_remote_execution.po +77 -27
  41. data/locale/pt_BR/foreman_remote_execution.po +77 -27
  42. data/locale/ru/foreman_remote_execution.po +77 -27
  43. data/locale/zh_CN/foreman_remote_execution.po +77 -27
  44. data/locale/zh_TW/foreman_remote_execution.po +77 -27
  45. data/package.json +3 -2
  46. data/test/functional/api/v2/job_invocations_controller_test.rb +24 -4
  47. data/test/functional/api/v2/registration_controller_test.rb +4 -13
  48. data/test/functional/ui_job_wizard_controller_test.rb +16 -0
  49. data/test/helpers/remote_execution_helper_test.rb +16 -0
  50. data/test/unit/job_invocation_composer_test.rb +41 -1
  51. data/test/unit/job_invocation_report_template_test.rb +57 -0
  52. data/webpack/JobWizard/JobWizard.js +55 -0
  53. data/webpack/JobWizard/JobWizard.scss +12 -0
  54. data/webpack/JobWizard/JobWizardConstants.js +5 -0
  55. data/webpack/JobWizard/JobWizardSelectors.js +21 -0
  56. data/webpack/JobWizard/__tests__/JobWizard.test.js +20 -0
  57. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +83 -0
  58. data/webpack/JobWizard/index.js +32 -0
  59. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +77 -0
  60. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +45 -0
  61. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +64 -0
  62. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +86 -0
  63. data/webpack/JobWizard/steps/form/GroupedSelectField.js +88 -0
  64. data/webpack/JobWizard/steps/form/SelectField.js +39 -0
  65. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +38 -0
  66. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +23 -0
  67. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +36 -0
  68. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +22 -0
  69. data/webpack/Routes/routes.js +12 -0
  70. data/webpack/__mocks__/foremanReact/common/helpers.js +1 -0
  71. data/webpack/__mocks__/foremanReact/history.js +1 -0
  72. data/webpack/__mocks__/foremanReact/redux/API/index.js +5 -0
  73. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  74. data/webpack/fills_index.js +11 -0
  75. data/webpack/global_index.js +8 -0
  76. data/webpack/index.js +0 -4
  77. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +87 -0
  78. data/webpack/react_app/components/RecentJobsCard/constants.js +1 -0
  79. data/webpack/react_app/components/RecentJobsCard/index.js +1 -0
  80. data/webpack/react_app/components/RecentJobsCard/styles.css +15 -0
  81. data/webpack/react_app/components/RegistrationExtension/RexInterface.js +50 -0
  82. data/webpack/react_app/components/RegistrationExtension/__tests__/RexInterface.test.js +9 -0
  83. data/webpack/react_app/components/RegistrationExtension/__tests__/__snapshots__/RexInterface.test.js.snap +35 -0
  84. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +1 -1
  85. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +0 -3
  86. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
  87. data/webpack/react_app/extend/fills.js +10 -0
  88. data/webpack/react_app/extend/reducers.js +4 -0
  89. metadata +54 -24
  90. data/app/views/api/v2/registration/_form.html.erb +0 -12
@@ -0,0 +1,57 @@
1
+ require 'test_plugin_helper'
2
+
3
+ class JobReportTemplateTest < ActiveSupport::TestCase
4
+ class FakeTask < OpenStruct
5
+ class Jail < Safemode::Jail
6
+ allow :action_continuous_output, :result, :ended_at
7
+ end
8
+ end
9
+
10
+ context 'with valid job invocation report template' do
11
+ let(:job_invocation_template) do
12
+ file_path = File.read(File.expand_path(Rails.root + "app/views/unattended/report_templates/jobs_-_invocation_report_template.erb"))
13
+ template = ReportTemplate.import_without_save("Job Invocation Report Template", file_path)
14
+ template.save!
15
+ template
16
+ end
17
+
18
+ describe 'template setting' do
19
+ it 'in settings includes only report templates with job_id input' do
20
+ FactoryBot.create(:report_template, name: 'Template 1')
21
+ job_invocation_template
22
+ templates = Setting::RemoteExecution.job_invocation_report_templates_select
23
+
24
+ assert_include templates, 'Job Invocation Report Template'
25
+ end
26
+ end
27
+
28
+ describe 'task reporting' do
29
+ let(:fake_outputs) do
30
+ [
31
+ { 'output_type' => 'stderr', 'output' => "error", 'timestamp' => Time.new(2020, 12, 1, 0, 0, 0).utc },
32
+ { 'output_type' => 'stdout', 'output' => "output", 'timestamp' => Time.new(2020, 12, 1, 0, 0, 0).utc },
33
+ { 'output_type' => 'stdebug', 'output' => "debug", 'timestamp' => Time.new(2020, 12, 1, 0, 0, 0).utc },
34
+ ]
35
+ end
36
+ let(:fake_task) { FakeTask.new(result: 'success', action_continuous_output: fake_outputs) }
37
+
38
+ it 'should render task outputs' do
39
+ job_invocation = FactoryBot.create(:job_invocation, :with_task)
40
+ JobInvocation.any_instance.expects(:sub_task_for_host).returns(fake_task)
41
+
42
+ input = job_invocation_template.template_inputs.first
43
+ composer_params = { template_id: job_invocation_template.id, input_values: { input.id.to_s => { value: job_invocation.id.to_s } } }
44
+ result = ReportComposer.new(composer_params).render
45
+
46
+ # parsing the CSV result
47
+ CSV.parse(result.strip, headers: true).each_with_index do |row, i|
48
+ row_hash = row.to_h
49
+ assert_equal 'success', row_hash['result']
50
+ assert_equal fake_outputs[i]['output_type'], row_hash['type']
51
+ assert_equal fake_outputs[i]['output'], row_hash['message']
52
+ assert_kind_of Time, Time.zone.parse(row_hash['time']), 'Parsing of time column failed'
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,55 @@
1
+ import React, { useState } from 'react';
2
+ import { Wizard } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import history from 'foremanReact/history';
5
+ import CategoryAndTemplate from './steps/CategoryAndTemplate/';
6
+ import './JobWizard.scss';
7
+
8
+ export const JobWizard = () => {
9
+ const [jobTemplate, setJobTemplate] = useState(null);
10
+ const [category, setCategory] = useState('');
11
+ const steps = [
12
+ {
13
+ name: __('Category and template'),
14
+ component: (
15
+ <CategoryAndTemplate
16
+ jobTemplate={jobTemplate}
17
+ setJobTemplate={setJobTemplate}
18
+ category={category}
19
+ setCategory={setCategory}
20
+ />
21
+ ),
22
+ },
23
+ {
24
+ name: __('Target hosts'),
25
+ component: <p>TargetHosts </p>,
26
+ canJumpTo: !!jobTemplate,
27
+ },
28
+ {
29
+ name: __('Advanced fields'),
30
+ component: <p> AdvancedFields </p>,
31
+ canJumpTo: !!jobTemplate,
32
+ },
33
+ {
34
+ name: __('Schedule'),
35
+ component: <p>Schedule</p>,
36
+ canJumpTo: !!jobTemplate,
37
+ },
38
+ {
39
+ name: __('Review details'),
40
+ component: <p>ReviewDetails</p>,
41
+ nextButtonText: 'Run',
42
+ canJumpTo: !!jobTemplate,
43
+ },
44
+ ];
45
+ const title = __('Run Job');
46
+ return (
47
+ <Wizard
48
+ onClose={() => history.goBack()}
49
+ navAriaLabel={`${title} steps`}
50
+ steps={steps}
51
+ height="70vh"
52
+ className="job-wizard"
53
+ />
54
+ );
55
+ };
@@ -0,0 +1,12 @@
1
+ .job-wizard {
2
+ .pf-c-wizard__main {
3
+ overflow: visible;
4
+ z-index: calc(
5
+ var(--pf-c-wizard__footer--ZIndex) + 1
6
+ ); // So the select box can be shown above the wizard footer
7
+ }
8
+
9
+ .pf-c-wizard__main-body {
10
+ max-width: 500px;
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ import { foremanUrl } from 'foremanReact/common/helpers';
2
+
3
+ export const JOB_TEMPLATES = 'JOB_TEMPLATES';
4
+ export const JOB_CATEGORIES = 'JOB_CATEGORIES';
5
+ export const templatesUrl = foremanUrl('/api/v2/job_templates');
@@ -0,0 +1,21 @@
1
+ import {
2
+ selectAPIResponse,
3
+ selectAPIStatus,
4
+ } from 'foremanReact/redux/API/APISelectors';
5
+
6
+ import { JOB_TEMPLATES, JOB_CATEGORIES } from './JobWizardConstants';
7
+
8
+ export const selectJobTemplatesStatus = state =>
9
+ selectAPIStatus(state, JOB_TEMPLATES);
10
+
11
+ export const filterJobTemplates = templates =>
12
+ templates?.filter(template => !template.snippet) || [];
13
+
14
+ export const selectJobTemplates = state =>
15
+ filterJobTemplates(selectAPIResponse(state, JOB_TEMPLATES)?.results);
16
+
17
+ export const selectJobCategories = state =>
18
+ selectAPIResponse(state, JOB_CATEGORIES).job_categories || [];
19
+
20
+ export const selectJobCategoriesStatus = state =>
21
+ selectAPIStatus(state, JOB_CATEGORIES);
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import * as patternfly from '@patternfly/react-core';
3
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
4
+ import JobWizardPage from '../index';
5
+ import { JobWizard } from '../JobWizard';
6
+
7
+ jest.spyOn(patternfly, 'Wizard');
8
+ patternfly.Wizard.mockImplementation(props => <div>{props}</div>);
9
+ const fixtures = {
10
+ 'renders ': {},
11
+ };
12
+ describe('JobWizardPage', () => {
13
+ describe('rendering', () =>
14
+ testComponentSnapshotsWithFixtures(JobWizardPage, fixtures));
15
+ });
16
+
17
+ describe('JobWizard', () => {
18
+ describe('rendering', () =>
19
+ testComponentSnapshotsWithFixtures(JobWizard, fixtures));
20
+ });
@@ -0,0 +1,83 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`JobWizard rendering renders 1`] = `
4
+ <mockConstructor
5
+ className="job-wizard"
6
+ height="70vh"
7
+ navAriaLabel="Run Job steps"
8
+ onClose={[Function]}
9
+ steps={
10
+ Array [
11
+ Object {
12
+ "component": <ConnectedCategoryAndTemplate
13
+ category=""
14
+ jobTemplate={null}
15
+ setCategory={[Function]}
16
+ setJobTemplate={[Function]}
17
+ />,
18
+ "name": "Category and template",
19
+ },
20
+ Object {
21
+ "canJumpTo": false,
22
+ "component": <p>
23
+ TargetHosts
24
+ </p>,
25
+ "name": "Target hosts",
26
+ },
27
+ Object {
28
+ "canJumpTo": false,
29
+ "component": <p>
30
+ AdvancedFields
31
+ </p>,
32
+ "name": "Advanced fields",
33
+ },
34
+ Object {
35
+ "canJumpTo": false,
36
+ "component": <p>
37
+ Schedule
38
+ </p>,
39
+ "name": "Schedule",
40
+ },
41
+ Object {
42
+ "canJumpTo": false,
43
+ "component": <p>
44
+ ReviewDetails
45
+ </p>,
46
+ "name": "Review details",
47
+ "nextButtonText": "Run",
48
+ },
49
+ ]
50
+ }
51
+ />
52
+ `;
53
+
54
+ exports[`JobWizardPage rendering renders 1`] = `
55
+ <PageLayout
56
+ breadcrumbOptions={
57
+ Object {
58
+ "breadcrumbItems": Array [
59
+ Object {
60
+ "caption": "Jobs",
61
+ "url": "/jobs",
62
+ },
63
+ Object {
64
+ "caption": "Run job",
65
+ },
66
+ ],
67
+ }
68
+ }
69
+ header="Run job"
70
+ searchable={false}
71
+ >
72
+ <Title
73
+ headingLevel="h2"
74
+ size="2xl"
75
+ >
76
+ Run job
77
+ </Title>
78
+ <Divider
79
+ component="div"
80
+ />
81
+ <JobWizard />
82
+ </PageLayout>
83
+ `;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { Title, Divider } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
5
+ import { JobWizard } from './JobWizard';
6
+
7
+ const JobWizardPage = () => {
8
+ const title = __('Run job');
9
+ const breadcrumbOptions = {
10
+ breadcrumbItems: [
11
+ { caption: __('Jobs'), url: `/jobs` },
12
+ { caption: title },
13
+ ],
14
+ };
15
+ return (
16
+ <PageLayout
17
+ header={title}
18
+ breadcrumbOptions={breadcrumbOptions}
19
+ searchable={false}
20
+ >
21
+ <React.Fragment>
22
+ <Title headingLevel="h2" size="2xl">
23
+ {title}
24
+ </Title>
25
+ <Divider component="div" />
26
+ <JobWizard />
27
+ </React.Fragment>
28
+ </PageLayout>
29
+ );
30
+ };
31
+
32
+ export default JobWizardPage;
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Title, Text, TextVariants, Form } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { SelectField } from '../form/SelectField';
6
+ import { GroupedSelectField } from '../form/GroupedSelectField';
7
+
8
+ export const CategoryAndTemplate = ({
9
+ jobCategories,
10
+ jobTemplates,
11
+ setJobTemplate,
12
+ selectedTemplateID,
13
+ selectedCategory,
14
+ setCategory,
15
+ }) => {
16
+ const templatesGroups = {};
17
+ jobTemplates.forEach(template => {
18
+ if (templatesGroups[template.provider_type]?.options)
19
+ templatesGroups[template.provider_type].options.push({
20
+ label: template.name,
21
+ value: template.id,
22
+ });
23
+ else
24
+ templatesGroups[template.provider_type] = {
25
+ options: [{ label: template.name, value: template.id }],
26
+ groupLabel: template.provider_type,
27
+ };
28
+ });
29
+
30
+ const selectedTemplate = jobTemplates.find(
31
+ template => template.id === selectedTemplateID
32
+ )?.name;
33
+
34
+ const onSelectCategory = newCategory => {
35
+ setCategory(newCategory);
36
+ setJobTemplate(null);
37
+ };
38
+ return (
39
+ <>
40
+ <Title headingLevel="h2">{__('Category And Template')}</Title>
41
+ <Text component={TextVariants.p}>{__('All fields are required.')}</Text>
42
+ <Form>
43
+ <SelectField
44
+ label={__('Job category')}
45
+ fieldId="job_category"
46
+ options={jobCategories}
47
+ setValue={onSelectCategory}
48
+ value={selectedCategory}
49
+ />
50
+ <GroupedSelectField
51
+ label={__('Job template')}
52
+ fieldId="job_template"
53
+ groups={Object.values(templatesGroups)}
54
+ setSelected={setJobTemplate}
55
+ selected={selectedTemplate}
56
+ />
57
+ </Form>
58
+ </>
59
+ );
60
+ };
61
+
62
+ CategoryAndTemplate.propTypes = {
63
+ jobCategories: PropTypes.array,
64
+ jobTemplates: PropTypes.array,
65
+ setJobTemplate: PropTypes.func.isRequired,
66
+ selectedTemplateID: PropTypes.number,
67
+ setCategory: PropTypes.func.isRequired,
68
+ selectedCategory: PropTypes.string,
69
+ };
70
+ CategoryAndTemplate.defaultProps = {
71
+ jobCategories: [],
72
+ jobTemplates: [],
73
+ selectedTemplateID: null,
74
+ selectedCategory: null,
75
+ };
76
+
77
+ export default CategoryAndTemplate;
@@ -0,0 +1,45 @@
1
+ import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
+ import { CategoryAndTemplate } from './CategoryAndTemplate';
3
+
4
+ const fixtures = {
5
+ 'renders with props': {
6
+ jobCategories: [
7
+ 'Commands',
8
+ 'Ansible Playbook',
9
+ 'Ansible Galaxy',
10
+ 'Ansible Roles Installation',
11
+ ],
12
+ jobTemplates: [
13
+ {
14
+ id: 190,
15
+ name: 'ab Run Command - SSH Default clone',
16
+ job_category: 'Commands',
17
+ provider_type: 'SSH',
18
+ snippet: false,
19
+ },
20
+ {
21
+ id: 168,
22
+ name: 'Ansible Roles - Ansible Default',
23
+ job_category: 'Ansible Playbook',
24
+ provider_type: 'Ansible',
25
+ snippet: false,
26
+ },
27
+ {
28
+ id: 170,
29
+ name: 'Ansible Roles - Install from git',
30
+ job_category: 'Ansible Roles Installation',
31
+ provider_type: 'Ansible',
32
+ snippet: false,
33
+ },
34
+ ],
35
+ setJobTemplate: jest.fn(),
36
+ selectedTemplateID: 190,
37
+ setCategory: jest.fn(),
38
+ selectedCategory: 'I am a category',
39
+ },
40
+ };
41
+
42
+ describe('CategoryAndTemplate', () => {
43
+ describe('rendering', () =>
44
+ testComponentSnapshotsWithFixtures(CategoryAndTemplate, fixtures));
45
+ });
@@ -0,0 +1,64 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`CategoryAndTemplate rendering renders with props 1`] = `
4
+ <Fragment>
5
+ <Title
6
+ headingLevel="h2"
7
+ >
8
+ Category And Template
9
+ </Title>
10
+ <Text
11
+ component="p"
12
+ >
13
+ All fields are required.
14
+ </Text>
15
+ <Form>
16
+ <SelectField
17
+ fieldId="job_category"
18
+ label="Job category"
19
+ options={
20
+ Array [
21
+ "Commands",
22
+ "Ansible Playbook",
23
+ "Ansible Galaxy",
24
+ "Ansible Roles Installation",
25
+ ]
26
+ }
27
+ setValue={[Function]}
28
+ value="I am a category"
29
+ />
30
+ <GroupedSelectField
31
+ fieldId="job_template"
32
+ groups={
33
+ Array [
34
+ Object {
35
+ "groupLabel": "SSH",
36
+ "options": Array [
37
+ Object {
38
+ "label": "ab Run Command - SSH Default clone",
39
+ "value": 190,
40
+ },
41
+ ],
42
+ },
43
+ Object {
44
+ "groupLabel": "Ansible",
45
+ "options": Array [
46
+ Object {
47
+ "label": "Ansible Roles - Ansible Default",
48
+ "value": 168,
49
+ },
50
+ Object {
51
+ "label": "Ansible Roles - Install from git",
52
+ "value": 170,
53
+ },
54
+ ],
55
+ },
56
+ ]
57
+ }
58
+ label="Job template"
59
+ selected="ab Run Command - SSH Default clone"
60
+ setSelected={[MockFunction]}
61
+ />
62
+ </Form>
63
+ </Fragment>
64
+ `;