Skip to content

Commit

Permalink
No InventoryUpdates when source Project is failed (ansible#13063)
Browse files Browse the repository at this point in the history
Previously, in some cases, an InventoryUpdate sourced by an SCM project
would still run and be successful even after the project it is sourced
from failed to update. This would happen because the InventoryUpdate
would revert the project back to its last working revision. This
behavior is confusing and inconsistent with how we handle jobs (which
just refuse to launch when the project is failed).

This change pulls out the logic that the job launch serializer and
RunJob#pre_run_hook had implemented (independently) to check if the
project is in a failed state, and puts it into a method on the Project
model. This is then checked in the project launch serializer as well as
the inventory update serializer, along with
SourceControlMixin#sync_and_copy as a fallback for things that don't run
the serializer validation (such as scheduled jobs and WFJT jobs).

Signed-off-by: Rick Elrod <[email protected]>
  • Loading branch information
relrod authored Nov 3, 2022
1 parent 75e6366 commit 1c65339
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 15 deletions.
24 changes: 13 additions & 11 deletions awx/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2221,6 +2221,15 @@ class InventorySourceUpdateSerializer(InventorySourceSerializer):
class Meta:
fields = ('can_update',)

def validate(self, attrs):
project = self.instance.source_project
if project:
failed_reason = project.get_reason_if_failed()
if failed_reason:
raise serializers.ValidationError(failed_reason)

return super(InventorySourceUpdateSerializer, self).validate(attrs)


class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSerializer):

Expand Down Expand Up @@ -4272,17 +4281,10 @@ def validate(self, attrs):
# Basic validation - cannot run a playbook without a playbook
if not template.project:
errors['project'] = _("A project is required to run a job.")
elif template.project.status in ('error', 'failed'):
errors['playbook'] = _("Missing a revision to run due to failed project update.")

latest_update = template.project.project_updates.last()
if latest_update is not None and latest_update.failed:
failed_validation_tasks = latest_update.project_update_events.filter(
event='runner_on_failed',
play="Perform project signature/checksum verification",
)
if failed_validation_tasks:
errors['playbook'] = _("Last project update failed due to signature validation failure.")
else:
failure_reason = template.project.get_reason_if_failed()
if failure_reason:
errors['playbook'] = failure_reason

# cannot run a playbook without an inventory
if template.inventory and template.inventory.pending_deletion is True:
Expand Down
2 changes: 2 additions & 0 deletions awx/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2221,6 +2221,8 @@ class InventorySourceUpdateView(RetrieveAPIView):

def post(self, request, *args, **kwargs):
obj = self.get_object()
serializer = self.get_serializer(instance=obj, data=request.data)
serializer.is_valid(raise_exception=True)
if obj.can_update:
update = obj.update()
if not update:
Expand Down
23 changes: 23 additions & 0 deletions awx/main/models/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,29 @@ def notification_templates(self):
def get_absolute_url(self, request=None):
return reverse('api:project_detail', kwargs={'pk': self.pk}, request=request)

def get_reason_if_failed(self):
"""
If the project is in a failed or errored state, return a human-readable
error message explaining why. Otherwise return None.
This is used during validation in the serializer and also by
RunProjectUpdate/RunInventoryUpdate.
"""

if self.status not in ('error', 'failed'):
return None

latest_update = self.project_updates.last()
if latest_update is not None and latest_update.failed:
failed_validation_tasks = latest_update.project_update_events.filter(
event='runner_on_failed',
play="Perform project signature/checksum verification",
)
if failed_validation_tasks:
return _("Last project update failed due to signature validation failure.")

return _("Missing a revision to run due to failed project update.")

'''
RelatedJobsMixin
'''
Expand Down
8 changes: 4 additions & 4 deletions awx/main/tasks/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,10 @@ def sync_and_copy(self, project, private_data_dir, scm_branch=None):

try:
original_branch = None
failed_reason = project.get_reason_if_failed()
if failed_reason:
self.update_model(self.instance.pk, status='failed', job_explanation=failed_reason)
raise RuntimeError(failed_reason)
project_path = project.get_project_path(check_if_exists=False)
if project.scm_type == 'git' and (scm_branch and scm_branch != project.scm_branch):
if os.path.exists(project_path):
Expand Down Expand Up @@ -1056,10 +1060,6 @@ def pre_run_hook(self, job, private_data_dir):
error = _('Job could not start because no Execution Environment could be found.')
self.update_model(job.pk, status='error', job_explanation=error)
raise RuntimeError(error)
elif job.project.status in ('error', 'failed'):
msg = _('The project revision for this job template is unknown due to a failed update.')
job = self.update_model(job.pk, status='failed', job_explanation=msg)
raise RuntimeError(msg)

if job.inventory.kind == 'smart':
# cache smart inventory memberships so that the host_filter query is not
Expand Down

0 comments on commit 1c65339

Please sign in to comment.