Skip to content

AAP-68153 Introduce new internal classes for durable data in job preparation#16382

Open
AlanCoding wants to merge 9 commits intoansible:develfrom
AlanCoding:credential_cargo
Open

AAP-68153 Introduce new internal classes for durable data in job preparation#16382
AlanCoding wants to merge 9 commits intoansible:develfrom
AlanCoding:credential_cargo

Conversation

@AlanCoding
Copy link
Copy Markdown
Member

@AlanCoding AlanCoding commented Apr 1, 2026

SUMMARY

Created stemming from conversation in #16380

I didn't like it when the context was introduced on the Credential model. But I accepted it begrudgingly. However, if this results in the situation getting worse and worse and monkey patches being introduced everywhere, then I'd rather do this.

This does an internal refactor that is hopefully specific to the job "prep" phase. This is where the runner kwargs and private_data_dir are getting populated.

ISSUE TYPE
  • Bug, Docs Fix or other nominal change
COMPONENT NAME
  • API

Note

Medium Risk
Moderate risk because it refactors the job prep path (credential fetching, dynamic input resolution, and credential injection) across multiple job types; regressions could break job launches or external credential/OIDC resolution.

Overview
Introduces new in-memory prep structures (TaskPrepData/PreparedCredential) to own credentials and runtime workload-identity JWTs during the job preparation phase, and removes runtime state from the Credential model.

Updates BaseTask and all job task implementations to build prep artifacts (private data dir/files, env, args, extra vars, inventory/playbook params) from the shared prep snapshot, and changes OIDC workload identity token generation to populate prep.workload_tokens keyed by CredentialInputSource PK.

Adjusts RunInventoryUpdate injection to avoid double-injecting the cloud credential, and rewrites affected tests to construct/use TaskPrepData instead of relying on cached credential properties or per-credential context.

Written by Cursor Bugbot for commit fe7a28b. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

Release Notes

  • Refactor

    • Restructured internal task preparation system to improve credential and execution environment handling.
    • Consolidated credential management across the job execution pipeline into a unified preparation phase.
    • Reorganized how runtime configuration is computed and passed during job execution.
  • Tests

    • Updated functional and unit tests to validate the refactored task preparation system.

davemulford and others added 4 commits April 1, 2026 06:37
RunInventoryUpdate had two gaps preventing OIDC workload identity
tokens from reaching the cloud credential during inventory sync:

1. build_credentials_list() called get_extra_credentials() which
   excludes the cloud credential, so populate_workload_identity_tokens()
   never generated a JWT for it.

2. The inventory plugin injector calls inventory_update.get_cloud_credential()
   internally, which does a fresh DB fetch that loses the OIDC context
   set by populate_workload_identity_tokens().

Fix (1) by returning all credentials with prefetched input sources.
Fix (2) by overriding get_cloud_credential() on the instance to return
the credential that already carries the OIDC context.

Assisted-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

Credential handling in job task execution has been refactored. The system now uses a TaskPrepData object created at the start of job preparation to encapsulate credentials and workload tokens, replacing the previous instance-based credential fetching. Task build methods now accept TaskPrepData instead of model instances, and the Credential.context property has been removed in favor of storing workload tokens on the prep object.

Changes

Cohort / File(s) Summary
Credential Context Removal
awx/main/models/credential.py
Removed Credential.context cached property and updated _get_dynamic_input() to call get_input_value() without passing context parameter.
Task Prep Data Structure
awx/main/tasks/prep.py
New module introducing PreparedCredential (credential wrapper with dynamic input resolution) and TaskPrepData (in-memory snapshot of job credentials with workload token storage and credential filtering utilities).
Task Execution Pipeline Refactoring
awx/main/tasks/jobs.py
Refactored BaseTask and concrete task classes (RunJob, RunProjectUpdate, RunInventoryUpdate, RunAdHocCommand, RunSystemJob) to accept TaskPrepData instead of model instances; replaced _credentials caching with prep.credentials; updated workload token handling to write into prep.workload_tokens; added get_credentials_for_injection(prep) hook.
Functional Test Updates
awx/main/tests/functional/test_jobs.py
Updated workload identity token tests to use TaskPrepData and verify tokens stored on prep.workload_tokens; removed tests for deprecated _credentials caching and credential category helpers.
Unit Test Updates
awx/main/tests/unit/models/test_credential.py, awx/main/tests/unit/tasks/test_jobs.py
Removed Credential.context validation tests; added tests for workload tokens ownership via TaskPrepData; updated populate_workload_identity_tokens invocations to pass prep object.
Task Unit Test Infrastructure
awx/main/tests/unit/test_tasks.py
Added make_prep() helper to construct TaskPrepData for tests; refactored test setup to use TaskPrepData instead of mocking credential properties; removed mocks for Job.cloud_credentials, Job.network_credentials, and Organization.galaxy_credentials; updated task method calls to pass TaskPrepData instead of instances.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: introduction of new internal classes for managing durable data in job preparation phase.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AlanCoding AlanCoding marked this pull request as ready for review April 2, 2026 03:32
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

elif instance.__dict__.get('credential_id'):
creds = [instance.credential]
else:
creds = []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead elif branch loses credentials for FK-based jobs

High Severity

from_instance has an unreachable elif branch for FK-based credential types. All UnifiedJob subclasses inherit a credentials M2M field, so hasattr(instance, 'credentials') is always True for saved instances. This means AdHocCommand and ProjectUpdate, which store their credential via a FK (credential) rather than the M2M, will always enter the first branch and get an empty queryset from the M2M. The docstring even states the elif is meant for "types that have a single credential (ProjectUpdate, AdHocCommand)" but the condition never fires. This causes all credential-dependent behavior (SSH keys, passwords, args) to silently break for ad hoc commands and project updates.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@awx/main/tasks/jobs.py`:
- Around line 223-240: populate_workload_identity_tokens currently only iterates
prep.credentials so organization-level Galaxy credentials never get minted;
change the generator in populate_workload_identity_tokens to iterate over both
prep.credentials and the organization-level credentials loaded by
TaskPrepData.from_instance (e.g. prep.organization_credentials or
prep.organization_galaxy_credentials) — combine them (chain or simple
concatenation) and then run the same input_sources check so
workoad_identity_token JWTs are created for org-attached OIDC-backed Galaxy
credentials as well.
- Around line 688-691: The current if guard around
flag_enabled("FEATURE_OIDC_WORKLOAD_IDENTITY_ENABLED") prevents
populate_workload_identity_tokens(prep) from running when the feature flag is
off, which hides the codepath that sets self.instance.status = 'error'; in run()
remove the outer flag check so populate_workload_identity_tokens(prep) is always
invoked (so it can mark the job errored when workload-identity-backed
credentials are present and the feature is disabled); if you want to keep the
log message, keep logger.info(...) behind the flag but call
populate_workload_identity_tokens(prep) unconditionally so the disabled-flag
failure path can execute.

In `@awx/main/tasks/prep.py`:
- Around line 111-118: TaskPrepData.__init__ currently wraps entries in
credentials with PreparedCredential but leaves self.galaxy_credentials as raw
Credential objects; change the constructor to normalize galaxy_credentials the
same way as credentials by iterating over the incoming galaxy_credentials,
extracting raw = g._credential if isinstance(g, PreparedCredential) else g, and
appending PreparedCredential(raw, self) to self.galaxy_credentials so OIDC token
resolution runs through PreparedCredential (do the same fix for the second
occurrence referenced around the other block at 138-142).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b9912d4-8210-482f-8785-126118da2a4f

📥 Commits

Reviewing files that changed from the base of the PR and between e80ce43 and fe7a28b.

📒 Files selected for processing (7)
  • awx/main/models/credential.py
  • awx/main/tasks/jobs.py
  • awx/main/tasks/prep.py
  • awx/main/tests/functional/test_jobs.py
  • awx/main/tests/unit/models/test_credential.py
  • awx/main/tests/unit/tasks/test_jobs.py
  • awx/main/tests/unit/test_tasks.py

Comment on lines +223 to +240
def populate_workload_identity_tokens(self, prep):
"""
Credentials for the task execution.
Fetches credentials once using build_credentials_list() and stores
them for the duration of the task to avoid redundant database queries.
"""
credentials_list = self.build_credentials_list(self.instance)
# Convert to list to prevent re-evaluation of QuerySet
return list(credentials_list)

def populate_workload_identity_tokens(self):
"""
Populate credentials with workload identity tokens.
Populate prep.workload_tokens with workload identity JWTs.

Sets the context on Credential objects that have input sources
using compatible external credential types.
Writes JWTs keyed by CredentialInputSource PK. These tokens are
later consumed by PreparedCredential.get_input() when resolving
dynamic fields via the shared workload_tokens dict.
"""
credential_input_sources = (
(credential.context, src)
for credential in self._credentials
src
for credential in prep.credentials
for src in credential.input_sources.all()
if any(
field.get('id') == 'workload_identity_token' and field.get('internal')
for field in src.source_credential.credential_type.inputs.get('fields', [])
)
)
for credential_ctx, input_src in credential_input_sources:
for input_src in credential_input_sources:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mint workload tokens for Galaxy credentials as well.

TaskPrepData.from_instance() now loads organization Galaxy credentials, and RunProjectUpdate.build_env() later consumes them, but this generator only walks prep.credentials. Any OIDC-backed Galaxy credential attached at the organization level will never receive a workload_identity_token.

Suggested fix
-        credential_input_sources = (
-            src
-            for credential in prep.credentials
+        credential_input_sources = (
+            src
+            for credential in [*prep.credentials, *getattr(prep, 'galaxy_credentials', [])]
             for src in credential.input_sources.all()
             if any(
                 field.get('id') == 'workload_identity_token' and field.get('internal')
                 for field in src.source_credential.credential_type.inputs.get('fields', [])
             )
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def populate_workload_identity_tokens(self, prep):
"""
Credentials for the task execution.
Fetches credentials once using build_credentials_list() and stores
them for the duration of the task to avoid redundant database queries.
"""
credentials_list = self.build_credentials_list(self.instance)
# Convert to list to prevent re-evaluation of QuerySet
return list(credentials_list)
def populate_workload_identity_tokens(self):
"""
Populate credentials with workload identity tokens.
Populate prep.workload_tokens with workload identity JWTs.
Sets the context on Credential objects that have input sources
using compatible external credential types.
Writes JWTs keyed by CredentialInputSource PK. These tokens are
later consumed by PreparedCredential.get_input() when resolving
dynamic fields via the shared workload_tokens dict.
"""
credential_input_sources = (
(credential.context, src)
for credential in self._credentials
src
for credential in prep.credentials
for src in credential.input_sources.all()
if any(
field.get('id') == 'workload_identity_token' and field.get('internal')
for field in src.source_credential.credential_type.inputs.get('fields', [])
)
)
for credential_ctx, input_src in credential_input_sources:
for input_src in credential_input_sources:
def populate_workload_identity_tokens(self, prep):
"""
Populate prep.workload_tokens with workload identity JWTs.
Writes JWTs keyed by CredentialInputSource PK. These tokens are
later consumed by PreparedCredential.get_input() when resolving
dynamic fields via the shared workload_tokens dict.
"""
credential_input_sources = (
src
for credential in [*prep.credentials, *getattr(prep, 'galaxy_credentials', [])]
for src in credential.input_sources.all()
if any(
field.get('id') == 'workload_identity_token' and field.get('internal')
for field in src.source_credential.credential_type.inputs.get('fields', [])
)
)
for input_src in credential_input_sources:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@awx/main/tasks/jobs.py` around lines 223 - 240,
populate_workload_identity_tokens currently only iterates prep.credentials so
organization-level Galaxy credentials never get minted; change the generator in
populate_workload_identity_tokens to iterate over both prep.credentials and the
organization-level credentials loaded by TaskPrepData.from_instance (e.g.
prep.organization_credentials or prep.organization_galaxy_credentials) — combine
them (chain or simple concatenation) and then run the same input_sources check
so workoad_identity_token JWTs are created for org-attached OIDC-backed Galaxy
credentials as well.

Comment on lines 688 to 691
if flag_enabled("FEATURE_OIDC_WORKLOAD_IDENTITY_ENABLED"):
logger.info(f'Generating workload identity tokens for {self.instance.log_format}')
self.populate_workload_identity_tokens()
self.populate_workload_identity_tokens(prep)
if self.instance.status == 'error':
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t bypass the disabled-flag failure path in run().

populate_workload_identity_tokens() already marks the job errored when workload-identity-backed credentials are present but the feature flag is off. Guarding the call here makes that branch unreachable during normal execution, so prep continues until a later dynamic-input failure.

Suggested fix
-            if flag_enabled("FEATURE_OIDC_WORKLOAD_IDENTITY_ENABLED"):
-                logger.info(f'Generating workload identity tokens for {self.instance.log_format}')
-                self.populate_workload_identity_tokens(prep)
-                if self.instance.status == 'error':
-                    raise RuntimeError('not starting %s task' % self.instance.status)
+            self.populate_workload_identity_tokens(prep)
+            if self.instance.status == 'error':
+                raise RuntimeError('not starting %s task' % self.instance.status)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@awx/main/tasks/jobs.py` around lines 688 - 691, The current if guard around
flag_enabled("FEATURE_OIDC_WORKLOAD_IDENTITY_ENABLED") prevents
populate_workload_identity_tokens(prep) from running when the feature flag is
off, which hides the codepath that sets self.instance.status = 'error'; in run()
remove the outer flag check so populate_workload_identity_tokens(prep) is always
invoked (so it can mark the job errored when workload-identity-backed
credentials are present and the feature is disabled); if you want to keep the
log message, keep logger.info(...) behind the flag but call
populate_workload_identity_tokens(prep) unconditionally so the disabled-flag
failure path can execute.

Comment on lines +111 to +118
def __init__(self, instance, credentials, galaxy_credentials=None):
self._instance = instance
self.workload_tokens = {}
self.credentials = []
self.galaxy_credentials = galaxy_credentials or []
for c in credentials:
raw = c._credential if isinstance(c, PreparedCredential) else c
self.credentials.append(PreparedCredential(raw, self))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Wrap galaxy_credentials in PreparedCredential too.

TaskPrepData now carries organization Galaxy credentials, but they stay as raw Credential objects here. After awx/main/models/credential.py:368-372 stopped passing prep context through raw Credential.get_input(), RunProjectUpdate.build_env() in awx/main/tasks/jobs.py:1391-1398 will bypass PreparedCredential for dynamic token resolution, so OIDC-backed organization Galaxy credentials still fail during prep.

Suggested fix
     def __init__(self, instance, credentials, galaxy_credentials=None):
         self._instance = instance
         self.workload_tokens = {}
         self.credentials = []
-        self.galaxy_credentials = galaxy_credentials or []
+        self.galaxy_credentials = []
         for c in credentials:
             raw = c._credential if isinstance(c, PreparedCredential) else c
             self.credentials.append(PreparedCredential(raw, self))
+        for c in galaxy_credentials or []:
+            raw = c._credential if isinstance(c, PreparedCredential) else c
+            self.galaxy_credentials.append(PreparedCredential(raw, self))
-            galaxy_creds = list(instance.project.organization.galaxy_credentials.all())
+            galaxy_creds = list(
+                instance.project.organization.galaxy_credentials.prefetch_related('input_sources__source_credential').all()
+            )

Also applies to: 138-142

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@awx/main/tasks/prep.py` around lines 111 - 118, TaskPrepData.__init__
currently wraps entries in credentials with PreparedCredential but leaves
self.galaxy_credentials as raw Credential objects; change the constructor to
normalize galaxy_credentials the same way as credentials by iterating over the
incoming galaxy_credentials, extracting raw = g._credential if isinstance(g,
PreparedCredential) else g, and appending PreparedCredential(raw, self) to
self.galaxy_credentials so OIDC token resolution runs through PreparedCredential
(do the same fix for the second occurrence referenced around the other block at
138-142).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants