Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
object @task

attributes :id, :action, :state, :result
node(:humanized) { @task.humanized[:action] }
20 changes: 20 additions & 0 deletions app/views/foreman_tasks/api/tasks/details.json.rabl
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,23 @@ node(:links) do
end
node(:username_path) { username_link_task(@task.owner, @task.username) }
node(:dynflow_enable_console) { Setting['dynflow_enable_console'] }
node(:depends_on) do
if @task.execution_plan
dynflow_uuids = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(@task.execution_plan.id)
ForemanTasks::Task.where(external_id: dynflow_uuids).map do |task|
partial('foreman_tasks/api/tasks/dependency_summary', :object => task)
end
else
[]
end
end
node(:blocks) do
if @task.execution_plan
dynflow_uuids = ForemanTasks.dynflow.world.persistence.find_blocked_execution_plans(@task.execution_plan.id)
ForemanTasks::Task.where(external_id: dynflow_uuids).map do |task|
partial('foreman_tasks/api/tasks/dependency_summary', :object => task)
end
else
[]
end
end
12 changes: 12 additions & 0 deletions lib/foreman_tasks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ def self.delay(action, delay_options, *args)
ForemanTasks::Task::DynflowTask.where(:external_id => result.id).first!
end

# Chain a task to wait for prerequisite task(s) to finish before executing.
# The chained task remains 'scheduled' until all prerequisites reach 'stopped' state.
#
# @param plan_uuids [String, Array<String>] UUID(s) of prerequisite execution plan(s)
# @param action [Class] Action class to execute
# @param args Arguments to pass to the action
# @return [ForemanTasks::Task::DynflowTask] The chained task
def self.chain(plan_uuids, action, *args)
result = dynflow.world.chain(plan_uuids, action, *args)
ForemanTasks::Task::DynflowTask.where(:external_id => result.id).first!
end

def self.register_scheduled_task(task_class, cronline)
ForemanTasks::RecurringLogic.transaction(isolation: :serializable) do
return if ForemanTasks::RecurringLogic.joins(:tasks)
Expand Down
35 changes: 35 additions & 0 deletions test/unit/chaining_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'foreman_tasks_test_helper'

module ForemanTasks
class ChainingTest < ActiveSupport::TestCase
include ForemanTasks::TestHelpers::WithInThreadExecutor

before do
User.current = User.where(:login => 'apiadmin').first
end

it 'creates a scheduled task chained to a prerequisite execution plan' do
prerequisite_plan = ForemanTasks.dynflow.world.plan(Support::DummyDynflowAction)

task = ForemanTasks.chain(prerequisite_plan.id, Support::DummyDynflowAction)

assert_kind_of ForemanTasks::Task::DynflowTask, task
assert_predicate task, :scheduled?

dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
assert_includes dependencies, prerequisite_plan.id
end

it 'accepts multiple prerequisite execution plans' do
prerequisite_plan_1 = ForemanTasks.dynflow.world.plan(Support::DummyDynflowAction)
prerequisite_plan_2 = ForemanTasks.dynflow.world.plan(Support::DummyDynflowAction)

task = ForemanTasks.chain([prerequisite_plan_1.id, prerequisite_plan_2.id], Support::DummyDynflowAction)

dependencies = ForemanTasks.dynflow.world.persistence.find_execution_plan_dependencies(task.execution_plan.id)
assert_includes dependencies, prerequisite_plan_1.id
assert_includes dependencies, prerequisite_plan_2.id
end
end
end

Check failure on line 35 in test/unit/chaining_test.rb

View workflow job for this annotation

GitHub Actions / Rubocop / Rubocop

Layout/TrailingEmptyLines: 1 trailing blank lines detected.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Alert,
AlertVariant,
Grid,
GridItem,
Title,
} from '@patternfly/react-core';
import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table';
import { translate as __ } from 'foremanReact/common/I18n';

const DependencyTable = ({ title, tasks }) => {
const tableId = title.toLowerCase().replace(/\s+/g, '-');
return (
<div>
<Title headingLevel="h4" size="md" ouiaId={`${tableId}-title`}>
{title}
</Title>
{tasks.length === 0 ? (
<p className="text-muted">{__('None')}</p>
) : (
<Table aria-label={title} variant="compact" ouiaId={`${tableId}-table`}>
<Thead>
<Tr ouiaId={`${tableId}-table-header`}>
<Th width={50}>{__('Action')}</Th>
<Th width={25}>{__('State')}</Th>
<Th width={25}>{__('Result')}</Th>
</Tr>
</Thead>
<Tbody>
{tasks.map(task => (
<Tr key={task.id} ouiaId={`${tableId}-table-row-${task.id}`}>
<Td>
<a href={`/foreman_tasks/tasks/${task.id}`}>
{task.humanized || task.action}
</a>
</Td>
<Td>{task.state}</Td>
<Td>{task.result}</Td>
</Tr>
))}
</Tbody>
</Table>
)}
</div>
);
};

DependencyTable.propTypes = {
title: PropTypes.string.isRequired,
tasks: PropTypes.array,
};

DependencyTable.defaultProps = {
tasks: [],
};

const Dependencies = ({ dependsOn, blocks }) => (
<div>
<Alert
variant={AlertVariant.info}
isInline
title={__('Task dependencies')}
ouiaId="task-dependencies-info-alert"
>
{__(
'This task may have dependencies on other tasks or may be blocking other tasks from executing. Dependencies are established through task chaining relationships.'
)}
</Alert>
<br />
<Grid hasGutter>
<GridItem span={6}>
<DependencyTable title={__('Depends on')} tasks={dependsOn} />
</GridItem>
<GridItem span={6}>
<DependencyTable title={__('Blocks')} tasks={blocks} />
</GridItem>
</Grid>
</div>
);

Dependencies.propTypes = {
dependsOn: PropTypes.array,
blocks: PropTypes.array,
};

Dependencies.defaultProps = {
dependsOn: [],
blocks: [],
};

export default Dependencies;
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { mount } from 'enzyme';
import Dependencies from '../Dependencies';

describe('Dependencies', () => {
it('should render with no dependencies', () => {
const wrapper = mount(<Dependencies dependsOn={[]} blocks={[]} />);
expect(wrapper.find('Alert')).toHaveLength(1);
expect(wrapper.find('DependencyTable')).toHaveLength(2);
expect(wrapper.find('Table')).toHaveLength(0);
expect(wrapper.text()).toContain('None');
});

it('should render with depends_on dependencies', () => {
const dependsOn = [
{
id: '123',
action: 'Actions::FooBar',
humanized: 'Foo Bar Action',
state: 'stopped',
result: 'success',
},
{
id: '456',
action: 'Actions::BazQux',
humanized: 'Baz Qux Action',
state: 'running',
result: 'pending',
},
];
const wrapper = mount(<Dependencies dependsOn={dependsOn} blocks={[]} />);
expect(wrapper.find('Table')).toHaveLength(1);
expect(wrapper.find('Tbody').find('Tr')).toHaveLength(2);
expect(wrapper.text()).toContain('Foo Bar Action');
expect(wrapper.text()).toContain('Baz Qux Action');
expect(wrapper.text()).toContain('stopped');
expect(wrapper.text()).toContain('success');
});

it('should render with blocks dependencies', () => {
const blocks = [
{
id: '789',
action: 'Actions::Test',
humanized: 'Test Action',
state: 'paused',
result: 'warning',
},
];
const wrapper = mount(<Dependencies dependsOn={[]} blocks={blocks} />);
expect(wrapper.find('Table')).toHaveLength(1);
expect(wrapper.find('Tbody').find('Tr')).toHaveLength(1);
expect(wrapper.text()).toContain('Test Action');
expect(wrapper.text()).toContain('paused');
expect(wrapper.text()).toContain('warning');
});

it('should render with both dependency types', () => {
const dependsOn = [
{
id: '123',
action: 'Actions::Foo',
humanized: 'Foo Action',
state: 'stopped',
result: 'success',
},
];
const blocks = [
{
id: '456',
action: 'Actions::Bar',
humanized: 'Bar Action',
state: 'running',
result: 'pending',
},
{
id: '789',
action: 'Actions::Baz',
humanized: 'Baz Action',
state: 'stopped',
result: 'error',
},
];
const wrapper = mount(
<Dependencies dependsOn={dependsOn} blocks={blocks} />
);
expect(wrapper.find('Table')).toHaveLength(2);
expect(wrapper.text()).toContain('Foo Action');
expect(wrapper.text()).toContain('Bar Action');
expect(wrapper.text()).toContain('Baz Action');
});
});
14 changes: 13 additions & 1 deletion webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import RunningSteps from './Components/RunningSteps';
import Errors from './Components/Errors';
import Locks from './Components/Locks';
import Raw from './Components/Raw';
import Dependencies from './Components/Dependencies';
import { getTaskID } from './TasksDetailsHelper';
import { TaskSkeleton } from './Components/TaskSkeleton';

Expand All @@ -20,6 +21,8 @@ const TaskDetails = ({
runningSteps,
locks,
links,
dependsOn,
blocks,
cancelStep,
taskReloadStart,
taskReloadStop,
Expand Down Expand Up @@ -90,7 +93,10 @@ const TaskDetails = ({
<Tab eventKey={4} disabled={isLoading} title={__('Locks')}>
<Locks locks={locks.concat(links)} />
</Tab>
<Tab eventKey={5} disabled={isLoading} title={__('Raw')}>
<Tab eventKey={5} disabled={isLoading} title={__('Dependencies')}>
<Dependencies dependsOn={dependsOn} blocks={blocks} />
</Tab>
<Tab eventKey={6} disabled={isLoading} title={__('Raw')}>
<Raw
id={id}
label={props.label}
Expand All @@ -116,9 +122,12 @@ TaskDetails.propTypes = {
taskReloadStop: PropTypes.func.isRequired,
taskReloadStart: PropTypes.func.isRequired,
links: PropTypes.array,
dependsOn: PropTypes.array,
blocks: PropTypes.array,
...Task.propTypes,
...Errors.propTypes,
...Locks.propTypes,
...Dependencies.propTypes,
...Raw.propTypes,
};
TaskDetails.defaultProps = {
Expand All @@ -127,10 +136,13 @@ TaskDetails.defaultProps = {
APIerror: null,
status: STATUS.PENDING,
links: [],
dependsOn: [],
blocks: [],
...Task.defaultProps,
...RunningSteps.defaultProps,
...Errors.defaultProps,
...Locks.defaultProps,
...Dependencies.defaultProps,
...Raw.defaultProps,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,9 @@ export const selectAPIError = state =>
export const selectIsLoading = state =>
!!selectAPIByKey(state, FOREMAN_TASK_DETAILS).response &&
selectStatus(state) === STATUS.PENDING;

export const selectDependsOn = state =>
selectTaskDetailsResponse(state).depends_on || [];

export const selectBlocks = state =>
selectTaskDetailsResponse(state).blocks || [];
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ exports[`TaskDetails rendering render with loading Props 1`] = `
<Tab
disabled={true}
eventKey={5}
title="Dependencies"
>
<Dependencies
blocks={Array []}
dependsOn={Array []}
/>
</Tab>
<Tab
disabled={true}
eventKey={6}
title="Raw"
>
<Raw
Expand Down Expand Up @@ -136,6 +146,15 @@ exports[`TaskDetails rendering render with min Props 1`] = `
</Tab>
<Tab
eventKey={5}
title="Dependencies"
>
<Dependencies
blocks={Array []}
dependsOn={Array []}
/>
</Tab>
<Tab
eventKey={6}
title="Raw"
>
<Raw
Expand Down
4 changes: 4 additions & 0 deletions webpack/ForemanTasks/Components/TaskDetails/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
selectStatus,
selectAPIError,
selectIsLoading,
selectDependsOn,
selectBlocks,
} from './TaskDetailsSelectors';

const mapStateToProps = state => ({
Expand Down Expand Up @@ -71,6 +73,8 @@ const mapStateToProps = state => ({
status: selectStatus(state),
APIerror: selectAPIError(state),
isLoading: selectIsLoading(state),
dependsOn: selectDependsOn(state),
blocks: selectBlocks(state),
});

const mapDispatchToProps = dispatch =>
Expand Down
Loading