mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 17:37:37 -02:30
Merge pull request #3782 from jangsutsr/3518_graph_topology_validation
Add graph topology validations to workflow JT nodes
This commit is contained in:
@@ -370,6 +370,9 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
|||||||
# Base class for a sublist view that allows for creating subobjects and
|
# Base class for a sublist view that allows for creating subobjects and
|
||||||
# attaching/detaching them from the parent.
|
# attaching/detaching them from the parent.
|
||||||
|
|
||||||
|
def is_valid_relation(self, parent, sub, created=False):
|
||||||
|
return None
|
||||||
|
|
||||||
def get_description_context(self):
|
def get_description_context(self):
|
||||||
d = super(SubListCreateAttachDetachAPIView, self).get_description_context()
|
d = super(SubListCreateAttachDetachAPIView, self).get_description_context()
|
||||||
d.update({
|
d.update({
|
||||||
@@ -406,6 +409,13 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
|||||||
skip_sub_obj_read_check=created):
|
skip_sub_obj_read_check=created):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
# Verify that the relationship to be added is valid.
|
||||||
|
attach_errors = self.is_valid_relation(parent, sub, created=created)
|
||||||
|
if attach_errors is not None:
|
||||||
|
if created:
|
||||||
|
sub.delete()
|
||||||
|
return Response(attach_errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Attach the object to the collection.
|
# Attach the object to the collection.
|
||||||
if sub not in relationship.all():
|
if sub not in relationship.all():
|
||||||
relationship.add(sub)
|
relationship.add(sub)
|
||||||
|
|||||||
@@ -2655,6 +2655,46 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su
|
|||||||
self.check_parent_access(parent)
|
self.check_parent_access(parent)
|
||||||
return getattr(parent, self.relationship).all()
|
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',)
|
||||||
|
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
|
||||||
|
|
||||||
|
workflow_nodes = parent.workflow_job_template.workflow_job_template_nodes.all().\
|
||||||
|
prefetch_related('success_nodes', 'failure_nodes', 'always_nodes')
|
||||||
|
graph = {}
|
||||||
|
for workflow_node in workflow_nodes:
|
||||||
|
graph[workflow_node.pk] = dict(node_object=workflow_node, metadata={'parent': None, 'traversed': False})
|
||||||
|
|
||||||
|
find = False
|
||||||
|
for node_type in ['success_nodes', 'failure_nodes', 'always_nodes']:
|
||||||
|
for workflow_node in workflow_nodes:
|
||||||
|
parent_node = graph[workflow_node.pk]
|
||||||
|
related_nodes = getattr(parent_node['node_object'], node_type).all()
|
||||||
|
for related_node in related_nodes:
|
||||||
|
sub_node = graph[related_node.pk]
|
||||||
|
sub_node['metadata']['parent'] = parent_node
|
||||||
|
if not find and parent == workflow_node and sub == related_node and self.relationship == node_type:
|
||||||
|
find = True
|
||||||
|
if not find:
|
||||||
|
sub_node = graph[sub.pk]
|
||||||
|
parent_node = graph[parent.pk]
|
||||||
|
if sub_node['metadata']['parent'] is not None:
|
||||||
|
return {"Error": "Multiple parent relationship not allowed."}
|
||||||
|
sub_node['metadata']['parent'] = parent_node
|
||||||
|
iter_node = sub_node
|
||||||
|
while iter_node is not None:
|
||||||
|
if iter_node['metadata']['traversed']:
|
||||||
|
return {"Error": "Cycle detected."}
|
||||||
|
iter_node['metadata']['traversed'] = True
|
||||||
|
iter_node = iter_node['metadata']['parent']
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
class WorkflowJobTemplateNodeSuccessNodesList(WorkflowJobTemplateNodeChildrenBaseList):
|
class WorkflowJobTemplateNodeSuccessNodesList(WorkflowJobTemplateNodeChildrenBaseList):
|
||||||
relationship = 'success_nodes'
|
relationship = 'success_nodes'
|
||||||
|
|
||||||
|
|||||||
@@ -137,4 +137,3 @@ class SimpleDAG(object):
|
|||||||
if len(self.get_dependents(n['node_object'])) < 1:
|
if len(self.get_dependents(n['node_object'])) < 1:
|
||||||
roots.append(n)
|
roots.append(n)
|
||||||
return roots
|
return roots
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,22 @@ class TestWorkflowJobTemplate:
|
|||||||
assert len(parent_qs) == 1
|
assert len(parent_qs) == 1
|
||||||
assert parent_qs[0] == wfjt.workflow_job_template_nodes.all()[1]
|
assert parent_qs[0] == wfjt.workflow_job_template_nodes.all()[1]
|
||||||
|
|
||||||
|
def test_topology_validator(self, wfjt):
|
||||||
|
from awx.api.views import WorkflowJobTemplateNodeChildrenBaseList
|
||||||
|
test_view = WorkflowJobTemplateNodeChildrenBaseList()
|
||||||
|
nodes = wfjt.workflow_job_template_nodes.all()
|
||||||
|
node_assoc = WorkflowJobTemplateNode.objects.create(workflow_job_template=wfjt)
|
||||||
|
nodes[2].always_nodes.add(node_assoc)
|
||||||
|
# test cycle validation
|
||||||
|
assert test_view.is_valid_relation(node_assoc, nodes[0]) == {'Error': 'Cycle detected!'}
|
||||||
|
# test multi-ancestor validation
|
||||||
|
assert test_view.is_valid_relation(node_assoc, nodes[1]) == {'Error': 'Multiple ancestor detected!'}
|
||||||
|
# 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.'})
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestWorkflowJobFailure:
|
class TestWorkflowJobFailure:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user