foreman_remote_execution 4.2.1 → 4.4.0

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