Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display WF always nodes in conjunction with success and failure #2373

Merged
4 changes: 0 additions & 4 deletions awx/api/views/__init__.py
Expand Up @@ -3438,10 +3438,6 @@ def get_queryset(self):
return getattr(parent, self.relationship).all()

def is_valid_relation(self, parent, sub, created=False):
mutex_list = ('success_nodes', 'failure_nodes') if self.relationship == 'always_nodes' else ('always_nodes',)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can confirm, this test is currently failing on the always_nodes_ui branch:

py.test -c config/docker.cfg --base-url='http://localhost:8013' -k 'test_node_triggers_should_be_mutually_exclusive' --disable-pytest-warnings

for relation in mutex_list:
if getattr(parent, relation).all().exists():
return {'Error': _('Cannot associate {0} when {1} have been associated.').format(self.relationship, relation)}

if created:
return None
Expand Down
13 changes: 10 additions & 3 deletions awx/main/tests/functional/models/test_workflow.py
Expand Up @@ -7,6 +7,7 @@
from awx.main.models.jobs import JobTemplate, Job
from awx.main.models.projects import ProjectUpdate
from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.api.versioning import reverse

# Django
from django.test import TransactionTestCase
Expand Down Expand Up @@ -196,9 +197,15 @@ def test_topology_validator(self, wfjt):
assert test_view.is_valid_relation(node_assoc, nodes[1]) == {'Error': 'Multiple parent relationship not allowed.'}
# test mutex validation
test_view.relationship = 'failure_nodes'
node_assoc_1 = WorkflowJobTemplateNode.objects.create(workflow_job_template=wfjt)
assert (test_view.is_valid_relation(nodes[2], node_assoc_1) ==
{'Error': 'Cannot associate failure_nodes when always_nodes have been associated.'})

def test_always_success_failure_creation(self, wfjt, admin, get):
wfjt_node = wfjt.workflow_job_template_nodes.all()[1]
node = WorkflowJobTemplateNode.objects.create(workflow_job_template=wfjt)
wfjt_node.always_nodes.add(node)
assert len(node.get_parent_nodes()) == 1
url = reverse('api:workflow_job_template_node_list') + str(wfjt_node.id) + '/'
resp = get(url, admin)
assert node.id in resp.data['always_nodes']

def test_wfjt_unique_together_with_org(self, organization):
wfjt1 = WorkflowJobTemplate(name='foo', organization=organization)
Expand Down
1 change: 0 additions & 1 deletion awx/ui/client/features/templates/templates.strings.js
Expand Up @@ -112,7 +112,6 @@ function TemplatesStrings (BaseString) {
RUN: t.s('RUN'),
CHECK: t.s('CHECK'),
SELECT: t.s('SELECT'),
EDGE_CONFLICT: t.s('EDGE CONFLICT'),
DELETED: t.s('DELETED'),
START: t.s('START'),
DETAILS: t.s('DETAILS'),
Expand Down
Expand Up @@ -102,13 +102,6 @@
width: 90px;
color: @default-interface-txt;
}
.WorkflowChart-conflictIcon {
color: @default-err;
}
.WorkflowChart-conflictText {
width: 90px;
color: @default-interface-txt;
}
.WorkflowChart-activeNode {
fill: @default-link;
}
Expand Down
Expand Up @@ -327,16 +327,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
}).each(wrap);

thisNode.append("foreignObject")
.attr("x", 54)
.attr("y", 45)
.style("font-size","0.7em")
.attr("class", "WorkflowChart-conflictText")
.html(function () {
return `<span class=\"WorkflowChart-conflictIcon\">\uf06a</span><span> ${TemplatesStrings.get('workflow_maker.EDGE_CONFLICT')}</span>`;
})
.style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });

thisNode.append("foreignObject")
.attr("x", 62)
.attr("y", 22)
Expand Down Expand Up @@ -831,9 +821,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
t.selectAll(".WorkflowChart-deletedText")
.style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; });

t.selectAll(".WorkflowChart-conflictText")
.style("display", function(d) { return (d.edgeConflict && !d.placeholder) ? null : "none"; });

t.selectAll(".WorkflowChart-activeNode")
.style("display", function(d) { return d.isActiveEdit ? null : "none"; });

Expand Down

Large diffs are not rendered by default.

Expand Up @@ -139,7 +139,7 @@
</div>
<div class="WorkflowMaker-buttonHolder">
<button type="button" class="btn btn-sm WorkflowMaker-cancelButton" ng-click="closeWorkflowMaker()"> {{:: strings.get('CLOSE') }}</button>
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate" ng-disabled="edgeFlags.conflict || workflowMakerFormConfig.nodeMode === 'add'"> {{:: strings.get('SAVE') }}</button>
<button type="button" class="btn btn-sm WorkflowMaker-saveButton" ng-click="saveWorkflowMaker()" ng-show="workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate" ng-disabled="workflowMakerFormConfig.nodeMode === 'add'"> {{:: strings.get('SAVE') }}</button>
</div>
<prompt prompt-data="promptData" action-text="{{:: strings.get('prompt.CONFIRM')}}" prevent-creds-with-passwords="preventCredsWithPasswords" read-only-prompts="!(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)"></prompt>
</div>
34 changes: 0 additions & 34 deletions awx/ui/client/src/templates/workflows/workflow.service.js
Expand Up @@ -290,39 +290,5 @@ export default ['$q', function($q){
}

},
checkForEdgeConflicts: function(params) {
//params.treeData
//params.edgeFlags

let hasAlways = false;
let hasSuccessFailure = false;
let _this = this;

_.forEach(params.treeData.children, function(child) {
// Flip the flag to false for now - we'll set it to true later on
// if we detect a conflict
child.edgeConflict = false;
if(child.edgeType === 'always') {
hasAlways = true;
}
else if(child.edgeType === 'success' || child.edgeType === 'failure') {
hasSuccessFailure = true;
}

_this.checkForEdgeConflicts({
treeData: child,
edgeFlags: params.edgeFlags
});
});

if(hasAlways && hasSuccessFailure) {
// We have a conflict
_.forEach(params.treeData.children, function(child) {
child.edgeConflict = true;
});

params.edgeFlags.conflict = true;
}
}
};
}];
11 changes: 6 additions & 5 deletions docs/workflow.md
Expand Up @@ -13,7 +13,7 @@ The CRUD operations against a workflow job template and its corresponding workfl
### Workflow Nodes
Workflow Nodes are containers of workflow spawned job resources and function as nodes of workflow decision trees. Like that of workflow itself, the two types of workflow nodes are workflow job template nodes and workflow job nodes.

Workflow job template nodes are listed and created under endpoint `/workflow_job_templates/\d+/workflow_nodes/` to be associated with underlying workflow job template, or directly under endpoint `/workflow_job_template_nodes/`. The most important fields of a workflow job template node are `success_nodes`, `failure_nodes`, `always_nodes`, `unified_job_template` and `workflow_job_template`. The former three are lists of workflow job template nodes that, in union, forms the set of all its child nodes, in specific, `success_nodes` are triggered when parnent node job succeeds, `failure_nodes` are triggered when parent node job fails, and `always_nodes` are triggered regardless of whether parent job succeeds or fails; The later two reference the job template resource it contains and workflow job template it belongs to.
Workflow job template nodes are listed and created under endpoint `/workflow_job_templates/\d+/workflow_nodes/` to be associated with underlying workflow job template, or directly under endpoint `/workflow_job_template_nodes/`. The most important fields of a workflow job template node are `success_nodes`, `failure_nodes`, `always_nodes`, `unified_job_template` and `workflow_job_template`. The former three are lists of workflow job template nodes that, in union, forms the set of all its child nodes, in specific, `success_nodes` are triggered when parent node job succeeds, `failure_nodes` are triggered when parent node job fails, and `always_nodes` are triggered regardless of whether parent job succeeds or fails; The later two reference the job template resource it contains and workflow job template it belongs to.

#### Workflow Node Launch Configuration

Expand All @@ -30,12 +30,13 @@ the launch configurations on workflow nodes.
The tree-graph structure of a workflow is enforced by associating workflow job template nodes via endpoints `/workflow_job_template_nodes/\d+/*_nodes/`, where `*` has options `success`, `failure` and `always`. However there are restrictions that must be enforced when setting up new connections. Here are the three restrictions that will raise validation error when break:
* Cycle restriction: According to tree definition, no cycle is allowed.
* Convergent restriction: Different paths should not come into the same node, in other words, a node cannot have multiple parents.
* Mutex restriction: A node cannot have all three types of child nodes. It contains either always nodes only, or any type other than always nodes.

> Note: A node can now have all three types of child nodes.

### Workflow Run Details
A typical workflow run starts by either POSTing to endpoint `/workflow_job_templates/\d+/launch/`, or being triggered automatically by related schedule. At the very first, the workflow job template creats workflow job, and all related workflow job template nodes create workflow job nodes. Right after that, all root nodes are populated with corresponding job resources and start running. If nothing goes wrong, each decision tree will follow its own route to completion. The entire workflow finishes running when all its decision trees complete.
A typical workflow run starts by either POSTing to endpoint `/workflow_job_templates/\d+/launch/`, or being triggered automatically by related schedule. At the very first, the workflow job template creates workflow job, and all related workflow job template nodes create workflow job nodes. Right after that, all root nodes are populated with corresponding job resources and start running. If nothing goes wrong, each decision tree will follow its own route to completion. The entire workflow finishes running when all its decision trees complete.

As stated, workflow job templates can be created with populated `extra_vars`. These `extra_vars` are combined with the `extra_vars` of any job template launched by the workflow with higher variable precedence, meaning they will overwrite job template variables with the same name. Note before the extra_vars set is applied as runtime job extra variables, it might be expaneded and over-written by the cumulative job artifacts of ancestor nodes. The meaning of 'cumulative' here is children overwritting parent. For example, if a node has a parent node and a grandparent node, and both ancestors generate job artifacts, then the job artifacts of grandparent node is overwritten by that of parent node to form the set of cumulative job artifacts of the current node.
As stated, workflow job templates can be created with populated `extra_vars`. These `extra_vars` are combined with the `extra_vars` of any job template launched by the workflow with higher variable precedence, meaning they will overwrite job template variables with the same name. Note before the extra_vars set is applied as runtime job extra variables, it might be expanded and over-written by the cumulative job artifacts of ancestor nodes. The meaning of 'cumulative' here is children overwriting parent. For example, if a node has a parent node and a grandparent node, and both ancestors generate job artifacts, then the job artifacts of grandparent node is overwritten by that of parent node to form the set of cumulative job artifacts of the current node.

Job resources spawned by workflow jobs are needed by workflow to run correctly. Therefore deletion of spawned job resources is blocked while the underlying workflow job is executing.

Expand Down Expand Up @@ -84,7 +85,7 @@ Artifact support starts in Ansible and is carried through in Tower. The `set_sta
* Verify that workflow job template nodes can be created under, or (dis)associated with workflow job templates.
* Verify that only the permitted types of job template types can be associated with a workflow job template node. Currently the permitted types are *job templates, inventory sources and projects*.
* Verify that workflow job template nodes under the same workflow job template can be associated to form parent-child relationship of decision trees. In specific, one node takes another as its child node by POSTing another node's id to one of the three endpoints: `/success_nodes/`, `/failure_nodes/` and `/always_nodes/`.
* Verify that workflow job template nodes are not allowed to have invalid association. Any attempt that causes invalidity will trigger 400-level response. The three types of invalid associations are cycle, convergence(multiple parent) and mutex('always' XOR the rest).
* Verify that workflow job template nodes are not allowed to have invalid association. Any attempt that causes invalidity will trigger 400-level response. The three types of invalid associations are cycle, convergence(multiple parent).
* Verify that a workflow job template can be successfully copied and the created workflow job template does not miss any field that should be copied or intentionally modified.
* Verify that if a user has no access to any of the related resources of a workflow job template node, that node will not be copied and will have `null` as placeholder.
* Verify that `artifacts` is populated when `set_stats` is used in Ansible >= v2.2.1.0-0.3.rc3.
Expand Down