Skip to content

Commit

Permalink
Fixes #33182 - Schedule recurring job for hostgroups
Browse files Browse the repository at this point in the history
  • Loading branch information
Ondrej Prazak authored and ezr-ondrej committed Nov 4, 2021
1 parent b5a5854 commit d185573
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
display_link_if_authorized(_('Run all Ansible roles'), hash_for_play_roles_hostgroup_path(id: hostgroup), :'data-no-turbolink' => true, title: _('Run all Ansible roles on hosts belonging to this host group'))
end

assign_jobs = link_to(_("Configure Ansible Job"), "/ansible/hostgroups/#{hostgroup.id}", { class: 'la' })

actions = [
display_link_if_authorized(_('Nest'), hash_for_nest_hostgroup_path(:id => hostgroup)),
display_link_if_authorized(_('Clone'), hash_for_clone_hostgroup_path(:id => hostgroup))
]
actions.push play_roles if User.current.can?(:create_job_invocations)
actions.push assign_jobs if User.current.can?(:view_job_invocations) && User.current.can?(:view_recurring_logics)
actions.push display_delete_if_authorized(hash_for_hostgroup_path(:id => hostgroup).merge(:auth_object => hostgroup, :authorizer => authorizer), :data => { :confirm => warning_message(hostgroup) })

action_buttons(*actions)
Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# frozen_string_literal: true

Rails.application.routes.draw do
match '/ansible/hostgroups' => 'react#index', :via => [:get]
match '/ansible/hostgroups/*page' => 'react#index', :via => [:get]

namespace :api, defaults: { format: 'json' } do
scope '(:apiv)',
:module => :v2,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { useQuery } from '@apollo/client';
import jobsQuery from '../../../../graphql/queries/recurringJobs.gql';

export const ansiblePurpose = hostId => `ansible-host-${hostId}`;
export const ansiblePurpose = (resourceName, resourceId) =>
`ansible-${resourceName}-${resourceId}`;

const jobSearch = (hostId, statusSearch) =>
`recurring = true && targeted_host_id = ${hostId} && pattern_template_name = "Ansible Roles - Ansible Default" && ${statusSearch} && recurring_logic.purpose = ${ansiblePurpose(
hostId
const jobSearch = (resourceName, resourceId, statusSearch) =>
`recurring = true && pattern_template_name = "Ansible Roles - Ansible Default" && ${statusSearch} && recurring_logic.purpose = ${ansiblePurpose(
resourceName,
resourceId
)}`;

export const scheduledJobsSearch = hostId =>
jobSearch(hostId, 'status = queued');
export const previousJobsSearch = hostId =>
jobSearch(hostId, 'status != queued');
export const scheduledJobsSearch = (resourceName, resourceId) =>
jobSearch(resourceName, resourceId, 'status = queued');
export const previousJobsSearch = (resourceName, resourceId) =>
jobSearch(resourceName, resourceId, 'status != queued');

const fetchJobsFn = searchFn => componentProps =>
useQuery(jobsQuery, {
variables: { search: searchFn(componentProps.hostId) },
variables: {
search: searchFn(componentProps.resourceName, componentProps.resourceId),
},
});

export const fetchRecurringFn = fetchJobsFn(scheduledJobsSearch);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,29 @@ export const toCron = (date, repeat) => {
}
};

export const toVars = (hostId, date, repeat) => ({
variables: {
jobInvocation: {
hostIds: [hostId],
feature: 'ansible_run_host',
targetingType: 'static_query',
scheduling: {
startAt: date,
},
recurrence: {
cronLine: toCron(date, repeat),
purpose: ansiblePurpose(hostId),
export const toVars = (resourceName, resourceId, date, repeat) => {
const targeting =
resourceName === 'host'
? { hostId: resourceId }
: { searchQuery: `hostgroup_id = ${resourceId}` };

return {
variables: {
jobInvocation: {
...targeting,
feature: 'ansible_run_host',
targetingType: 'static_query',
scheduling: {
startAt: date,
},
recurrence: {
cronLine: toCron(date, repeat),
purpose: ansiblePurpose(resourceName, resourceId),
},
},
},
},
});
};
};

const joinErrors = errors => errors.map(err => err.message).join(', ');

Expand All @@ -68,7 +75,7 @@ const formatError = error =>
error
);

export const onSubmit = (callMutation, onClose, hostId) => (
export const onSubmit = (callMutation, onClose, resourceName, resourceId) => (
values,
actions
) => {
Expand All @@ -95,7 +102,7 @@ export const onSubmit = (callMutation, onClose, hostId) => (
};

const date = new Date(`${values.startDate}T${values.startTime}`);
const variables = toVars(hostId, date, values.repeat);
const variables = toVars(resourceName, resourceId, date, values.repeat);
// eslint-disable-next-line promise/prefer-await-to-then
callMutation(variables).then(onCompleted, onError);
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,21 @@ import createJobInvocation from '../../../../graphql/mutations/createJobInvocati
import jobsQuery from '../../../../graphql/queries/recurringJobs.gql';

const NewRecurringJobModal = props => {
const { onClose, hostId } = props;
const { onClose, resourceId, resourceName } = props;

const [callMutation] = useMutation(createJobInvocation, {
refetchQueries: [
{
query: jobsQuery,
variables: { search: scheduledJobsSearch(hostId) },
variables: { search: scheduledJobsSearch(resourceName, resourceId) },
},
],
});

return (
<Formik
validationSchema={createValidationSchema()}
onSubmit={onSubmit(callMutation, onClose, hostId)}
onSubmit={onSubmit(callMutation, onClose, resourceName, resourceId)}
initialValues={{
startTime: '',
startDate: '',
Expand Down Expand Up @@ -121,7 +121,8 @@ const NewRecurringJobModal = props => {

NewRecurringJobModal.propTypes = {
onClose: PropTypes.func.isRequired,
hostId: PropTypes.number.isRequired,
resourceId: PropTypes.number.isRequired,
resourceName: PropTypes.string.isRequired,
isOpen: PropTypes.bool.isRequired,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ const futureDate = new Date(today.setDate(today.getDate() + 3));
futureDate.setMilliseconds(0);
futureDate.setSeconds(0);
export { futureDate };
export const emptyScheduledMocks = mockFactory([], scheduledJobsSearch(hostId));
export const emptyPreviousMocks = mockFactory([], previousJobsSearch(hostId));

const firstJob = {
export const firstJob = {
__typename: 'JobInvocation',
id: 'MDE6Sm9iSW52b2NhdGlvbi0yNTY=',
description: 'Run Ansible roles',
Expand All @@ -36,7 +34,7 @@ const firstJob = {
},
};

const secondJob = {
export const secondJob = {
__typename: 'JobInvocation',
id: 'MDE6Sm9iSW52b2NhdGlvbi0yNzE=',
description: 'Run Ansible roles',
Expand All @@ -56,34 +54,34 @@ const secondJob = {
},
};

const jobInvocationsMockFactory = mockFactory(
export const jobInvocationsMockFactory = mockFactory(
'jobInvocations',
recurringJobsQuery
);
const jobCreateMockFactory = mockFactory(
export const jobCreateMockFactory = mockFactory(
'createJobInvocation',
createJobMutation
);

const emptyScheduledJobsMock = jobInvocationsMockFactory(
{ search: scheduledJobsSearch(hostId) },
{ search: scheduledJobsSearch('host', hostId) },
{ nodes: [] }
);
const emptyScheduledJobsRefetchMock = jobInvocationsMockFactory(
{ search: scheduledJobsSearch(hostId) },
{ search: scheduledJobsSearch('host', hostId) },
{ nodes: [] },
{ refetchData: { nodes: [firstJob] } }
);
const emptyPreviousJobsMock = jobInvocationsMockFactory(
{ search: previousJobsSearch(hostId) },
{ search: previousJobsSearch('host', hostId) },
{ nodes: [] }
);
const scheduledJobsMocks = jobInvocationsMockFactory(
{ search: scheduledJobsSearch(hostId) },
{ search: scheduledJobsSearch('host', hostId) },
{ nodes: [firstJob] }
);
const previousJobsMocks = jobInvocationsMockFactory(
{ search: previousJobsSearch(hostId) },
{ search: previousJobsSearch('host', hostId) },
{ nodes: [secondJob] }
);

Expand All @@ -93,7 +91,7 @@ export const scheduledAndPreviousMocks = scheduledJobsMocks.concat(
);

const createJobMock = jobCreateMockFactory(
toVars(hostId, futureDate, 'weekly').variables,
toVars('host', hostId, futureDate, 'weekly').variables,
{ jobInvocation: { id: 'MDE6Sm9iSW52b2NhdGlvbi00MTU=' }, errors: [] }
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ describe('JobsTab', () => {
it('should load the page', async () => {
render(
<ComponentWithIntl
response={{ id: hostId }}
router={{ push: jest.fn() }}
resourceName="host"
resourceId={hostId}
mocks={scheduledAndPreviousMocks}
/>
);
Expand All @@ -51,8 +51,8 @@ describe('JobsTab', () => {
it('should show empty state', async () => {
render(
<ComponentWithIntl
response={{ id: hostId }}
router={{ push: jest.fn() }}
resourceName="host"
resourceId={hostId}
mocks={emptyMocks}
/>
);
Expand All @@ -66,7 +66,13 @@ describe('JobsTab', () => {
const showToast = jest.fn();
jest.spyOn(toasts, 'showToast').mockImplementation(showToast);

render(<ComponentWithIntl response={{ id: hostId }} mocks={createMocks} />);
render(
<ComponentWithIntl
resourceName="host"
resourceId={hostId}
mocks={createMocks}
/>
);
await waitFor(tick);
userEvent.click(
screen.getByRole('button', { name: 'schedule recurring job' })
Expand Down
14 changes: 9 additions & 5 deletions webpack/components/AnsibleHostDetail/components/JobsTab/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import RecurringJobsTable from './RecurringJobsTable';
import PreviousJobsTable from './PreviousJobsTable';
import NewRecurringJobModal from './NewRecurringJobModal';

const JobsTab = ({ response }) => {
const JobsTab = ({ resourceName, resourceId }) => {
const [modalOpen, setModalOpen] = useState(false);

const toggleModal = () => setModalOpen(!modalOpen);
Expand All @@ -26,7 +26,8 @@ const JobsTab = ({ response }) => {
<Grid>
<GridItem span={12}>
<RecurringJobsTable
hostId={response.id}
resourceId={resourceId}
resourceName={resourceName}
fetchFn={fetchRecurringFn}
renameData={renameData}
resultPath="jobInvocations.nodes"
Expand All @@ -38,7 +39,8 @@ const JobsTab = ({ response }) => {
</GridItem>
<GridItem span={12}>
<PreviousJobsTable
hostId={response.id}
resourceId={resourceId}
resourceName={resourceName}
fetchFn={fetchPreviousFn}
renameData={renameData}
emptyWrapper={() => null}
Expand All @@ -48,14 +50,16 @@ const JobsTab = ({ response }) => {
<NewRecurringJobModal
isOpen={modalOpen}
onClose={toggleModal}
hostId={response.id}
resourceId={resourceId}
resourceName={resourceName}
/>
</Grid>
);
};

JobsTab.propTypes = {
response: PropTypes.object.isRequired,
resourceName: PropTypes.string.isRequired,
resourceId: PropTypes.number.isRequired,
};

export default JobsTab;
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const SecondaryTabRoutes = ({ response, router, history }) => (
</Route>
<Route path={route('jobs')}>
<TabLayout>
<JobsTab response={response} />
<JobsTab resourceId={response.id} resourceName="host" />
</TabLayout>
</Route>
</Switch>
Expand Down
1 change: 1 addition & 0 deletions webpack/components/withLoading.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const withLoading = Component => {
}

const result = pluckData(data, resultPath);

if (
showEmptyState &&
((Array.isArray(result) && result.length === 0) || !result)
Expand Down
5 changes: 5 additions & 0 deletions webpack/global_index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from 'react';
import { registerRoutes } from 'foremanReact/routes/RoutingService';

import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';

import routes from './routes/routes';
import AnsibleHostDetail from './components/AnsibleHostDetail';

import { ANSIBLE_KEY } from './components/AnsibleHostDetail/constants';

addGlobalFill(
Expand All @@ -11,3 +14,5 @@ addGlobalFill(
<AnsibleHostDetail key="ansible-host-detail" />,
500
);

registerRoutes('foreman_ansible', routes);
Loading

0 comments on commit d185573

Please sign in to comment.