From 1c65339a24cab91d0ab6c49824dff3925160b23f Mon Sep 17 00:00:00 2001 From: Rick Elrod Date: Thu, 3 Nov 2022 13:23:34 -0500 Subject: [PATCH] No InventoryUpdates when source Project is failed (#13063) 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 --- awx/api/serializers.py | 24 +++++++++++++----------- awx/api/views/__init__.py | 2 ++ awx/main/models/projects.py | 23 +++++++++++++++++++++++ awx/main/tasks/jobs.py | 8 ++++---- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cf6fa391e9..c4436424f5 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -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): @@ -4272,17 +4281,10 @@ class JobLaunchSerializer(BaseSerializer): # 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: diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 14fae507d3..90e52ed883 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -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: diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 5af858fa8d..6577d24c40 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -471,6 +471,29 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn 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 ''' diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index 3295adcc9c..3557c4110c 100644 --- a/awx/main/tasks/jobs.py +++ b/awx/main/tasks/jobs.py @@ -767,6 +767,10 @@ class SourceControlMixin(BaseTask): 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): @@ -1056,10 +1060,6 @@ class RunJob(SourceControlMixin, BaseTask): 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