From 9acf3d1887cae8acaab6f00426d88334cdf5957b Mon Sep 17 00:00:00 2001 From: Liam Allen Date: Tue, 21 Jul 2026 10:00:21 +0100 Subject: [PATCH] AAP-73393 - Remove skip_tags character limit by converting to TextField (#16552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Optimize HostList API: conditional DISTINCT + composite index on JobHostSummary (#16530) AAP-81517 — Optimize HostList API: conditional DISTINCT + composite index Make .distinct() conditional on host_filter being set — without it the RBAC IN subquery on a direct FK cannot produce duplicates, so DISTINCT is pure overhead. Add composite index (host_id, id DESC) on main_jobhostsummary so the with_latest_summary_id() correlated subquery can use an index-only top-1 scan instead of scanning and sorting. Co-authored-by: Claude Opus 4.6 * Fix: Increase awx-operator molecule timeout to 20m (#16534) The awx-operator molecule kind test intermittently times out after 15 minutes on GitHub Actions runners, causing flaky CI failures. Bump the bash-level timeout from 15m to 20m (step-level timeout-minutes: 60 is unchanged). Closes: AAP-81583 Co-authored-by: Claude Opus 4.6 (1M context) * AAP-81860 — Eliminate LEFT OUTER JOINs in ActivityStream RBAC query (#16533) * AAP-81860 — Eliminate LEFT OUTER JOINs in ActivityStream RBAC query ActivityStreamAccess.filtered_queryset() used field-traversal Q objects (e.g. Q(host__inventory__in=...)) across 18 M2M relationships, which Django translates into LEFT OUTER JOINs. This forces PostgreSQL to join all intermediary tables before filtering, making it the #2 DB consumer at 483s in a 10-minute Scale Lab window. Replace all M2M field traversals with pk__in subqueries using .through intermediary tables, following the same pattern proven in AAP-81082 (28% improvement for unified job RBAC). This changes the SQL from LEFT OUTER JOIN to IN (SELECT ...) semi-joins, allowing PostgreSQL to skip the unconditional joins. Also: - Remove .distinct() (no longer needed without M2M JOINs) - Remove unnecessary if-truthy checks that evaluated subqueries as boolean before including them (5 extra DB roundtrips per request) - Migrate accessible_pk_qs(user, 'read_role') to access_ids_qs(user, 'view') for direct DAB RBAC API usage * Use .exists() instead of queryset truth-test for auditing_orgs guard Avoids evaluating the full queryset when checking whether the user has auditing orgs — .exists() issues a cheaper SELECT 1 … LIMIT 1 instead. * Restore .exists() guards for resource-type Q branches Re-add conditional guards around inventory, credential, project, job template, workflow job template, and team Q branches. Uses .exists() (cheap SELECT 1 LIMIT 1) instead of the old queryset truth-test to avoid unnecessary query evaluation while still preventing generation of an overly complex query when the user has no access to a given resource type. --------- Co-authored-by: Claude Opus 4.6 * chore: Upgrade kubernetes package to 36.0.2 * feat: remove extra_vars from jobs and unified_jobs list endpoint. Add… (#16461) feat: add exclude query parameter to unified_jobs and jobs list endpoints Allow clients to exclude heavy fields (artifacts, extra_vars) from list responses via ?exclude=artifacts,extra_vars. These fields are now included by default instead of being stripped. Co-authored-by: Claude Opus 4.6 (1M context) * AAP-80457 - Fix credential_input_source to handle state: absent when target crede… (#16523) Fix credential_input_source to handle state: absent when target credential is missing The credential_input_source module would error when trying to delete a credential input source (state: absent) if the target credential didn't exist. This breaks idempotent playbook runs where cleanup tasks assume resources may already be gone. The fix only applies to state: absent; state: present still correctly fails when the target credential doesn't exist. Co-authored-by: Liam Allen * AAP-83163 — Eliminate LEFT JOIN fan-out in user list RBAC query (#16546) Replace legacy Role M2M traversal in UserAccess.filtered_queryset() with DAB's visible_users(), which queries organizational membership through RoleUserAssignment subqueries instead of LEFT OUTER JOINs through the objectrole/roleevaluation tables. Also replace role__in=actor.has_roles.all() in can_admin() with RoleEvaluation._actor_role_filter() to avoid the objectrole JOIN in per-object permission checks. Co-authored-by: Claude Opus 4.6 * alter_skip_tags_to_textfield * Bump migration number --------- Co-authored-by: Dirk Julich Co-authored-by: Claude Opus 4.6 Co-authored-by: Rodrigo Toshiaki Horie Co-authored-by: Adrià Sala <22398818+adrisala@users.noreply.github.com> Co-authored-by: Peter Braun Co-authored-by: Nick Meyer --- .../0207_alter_skip_tags_to_textfield.py | 23 +++++++++++++++++++ awx/main/models/jobs.py | 3 +-- .../functional/api/test_job_runtime_params.py | 15 ++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 awx/main/migrations/0207_alter_skip_tags_to_textfield.py diff --git a/awx/main/migrations/0207_alter_skip_tags_to_textfield.py b/awx/main/migrations/0207_alter_skip_tags_to_textfield.py new file mode 100644 index 0000000000..9a6457cc07 --- /dev/null +++ b/awx/main/migrations/0207_alter_skip_tags_to_textfield.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.8 on 2026-07-20 11:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0206_jobhostsummary_host_id_idx'), + ] + + operations = [ + migrations.AlterField( + model_name='job', + name='skip_tags', + field=models.TextField(blank=True, default=''), + ), + migrations.AlterField( + model_name='jobtemplate', + name='skip_tags', + field=models.TextField(blank=True, default=''), + ), + ] diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 9b84488836..0336ab72c8 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -132,8 +132,7 @@ class JobOptions(BaseModel): blank=True, default=False, ) - skip_tags = models.CharField( - max_length=1024, + skip_tags = models.TextField( blank=True, default='', ) diff --git a/awx/main/tests/functional/api/test_job_runtime_params.py b/awx/main/tests/functional/api/test_job_runtime_params.py index 95b55e15b6..1220a0c8db 100644 --- a/awx/main/tests/functional/api/test_job_runtime_params.py +++ b/awx/main/tests/functional/api/test_job_runtime_params.py @@ -191,6 +191,21 @@ def test_job_accept_empty_tags(job_template_prompts, post, admin_user, mocker): mock_job.signal_start.assert_called_once() +@pytest.mark.django_db +@pytest.mark.job_runtime_vars +def test_job_accept_long_skip_tags(job_template_prompts, post, admin_user, mocker): + job_template = job_template_prompts(True) + long_skip_tags = ','.join(f'tag{i}' for i in range(500)) + assert len(long_skip_tags) > 1024 + + mock_job = mocker.MagicMock(spec=Job, id=968) + + mocker.patch.object(JobTemplate, 'create_unified_job', return_value=mock_job) + mocker.patch('awx.api.serializers.JobSerializer.to_representation') + post(reverse('api:job_template_launch', kwargs={'pk': job_template.pk}), {'skip_tags': long_skip_tags}, admin_user, expect=201) + JobTemplate.create_unified_job.assert_called_once_with(skip_tags=long_skip_tags) + + @pytest.mark.django_db @pytest.mark.job_runtime_vars def test_slice_timeout_forks_need_int(job_template_prompts, post, admin_user, mocker):