diff --git a/awx/api/serializers.py b/awx/api/serializers.py index e916550d82..102be782f0 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -4143,8 +4143,24 @@ class LaunchConfigurationBaseSerializer(BaseSerializer): # Build unsaved version of this config, use it to detect prompts errors mock_obj = self._build_mock_obj(attrs) - if set(list(ujt.get_ask_mapping().keys()) + ['extra_data']) & set(attrs.keys()): - accepted, rejected, errors = ujt._accept_or_ignore_job_kwargs(_exclude_errors=self.exclude_errors, **mock_obj.prompts_dict()) + ask_mapping_keys = set(ujt.get_ask_mapping().keys()) + requested_prompt_fields = set(attrs.keys()) & ask_mapping_keys + if 'extra_data' in attrs: + requested_prompt_fields.add('extra_vars') + + # prompts_dict() pulls persisted M2M state (labels, credentials, + # instance_groups) via the instance pk. Only re-validate the full prompt + # state when the caller is switching the underlying template; otherwise + # restrict validation to the fields the request explicitly provided. + if 'unified_job_template' in attrs: + prompts_to_validate = mock_obj.prompts_dict() + elif requested_prompt_fields: + prompts_to_validate = {k: v for k, v in mock_obj.prompts_dict().items() if k in requested_prompt_fields} + else: + prompts_to_validate = None + + if prompts_to_validate is not None: + accepted, rejected, errors = ujt._accept_or_ignore_job_kwargs(_exclude_errors=self.exclude_errors, **prompts_to_validate) else: # Only perform validation of prompts if prompts fields are provided errors = {} diff --git a/awx/main/tests/functional/api/test_workflow_node.py b/awx/main/tests/functional/api/test_workflow_node.py index 71874085d7..02860d06b6 100644 --- a/awx/main/tests/functional/api/test_workflow_node.py +++ b/awx/main/tests/functional/api/test_workflow_node.py @@ -13,6 +13,7 @@ from awx.main.models.workflow import ( WorkflowJobTemplateNode, ) from awx.main.models.credential import Credential +from awx.main.models.label import Label from awx.main.scheduler import TaskManager, WorkflowManager, DependencyManager # Django @@ -51,6 +52,31 @@ def test_node_accepts_prompted_fields(inventory, project, workflow_job_template, post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'}, user=admin_user, expect=201) +@pytest.mark.django_db +def test_node_extra_data_patch_with_unprompted_labels(inventory, project, organization, workflow_job_template, patch, admin_user): + """AAP-41742: PATCH extra_data on a workflow node should succeed even when + the node has labels associated but the JT has ask_labels_on_launch=False.""" + jt = JobTemplate.objects.create( + inventory=inventory, + project=project, + playbook='helloworld.yml', + ask_variables_on_launch=True, + ask_labels_on_launch=False, + ) + label = Label.objects.create(name='repro-label', organization=organization) + + node = WorkflowJobTemplateNode.objects.create( + workflow_job_template=workflow_job_template, + unified_job_template=jt, + extra_data={'foo': 'bar'}, + ) + node.labels.add(label) + + url = reverse('api:workflow_job_template_node_detail', kwargs={'pk': node.pk}) + r = patch(url, {'extra_data': {'foo': 'edited'}}, user=admin_user, expect=200) + assert r.data['extra_data'] == {'foo': 'edited'} + + @pytest.mark.django_db @pytest.mark.parametrize( "field_name, field_value",