Skip to content

Implement Candlepin certificate integration#16388

Open
jessicamack wants to merge 12 commits intoansible:develfrom
jessicamack:candlepin-integration
Open

Implement Candlepin certificate integration#16388
jessicamack wants to merge 12 commits intoansible:develfrom
jessicamack:candlepin-integration

Conversation

@jessicamack
Copy link
Copy Markdown
Member

@jessicamack jessicamack commented Apr 7, 2026

SUMMARY

Integrate with Candlepin to automatically obtain and maintain an identity certificate for analytics authentication when customers use basic auth (username/password) during manifest renewal. This PR is meant to bring the work from the metrics-utility into AWX.

ISSUE TYPE
  • New or Enhanced Feature
COMPONENT NAME
  • API
  • Other
STEPS TO REPRODUCE AND EXTRA INFO

Summary by CodeRabbit

  • New Features

    • Candlepin mTLS for analytics: automated consumer registration, check‑in, renewal, and DB-backed certificate storage; management command to register/renew certificates; analytics upload now prefers mTLS, then basic auth, then OIDC; new API endpoints exposing certificate health and status.
  • Tests

    • New unit tests for Candlepin client, lifecycle, licensing helpers, persistence, and renewal logic.
  • Settings

    • Added defaults for Candlepin URL, CA path, proxy, and renewal threshold.

@jessicamack jessicamack requested a review from fosterseth April 7, 2026 17:30
@jessicamack jessicamack self-assigned this Apr 7, 2026
@jessicamack jessicamack marked this pull request as draft April 7, 2026 17:30
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Candlepin consumer registration and mTLS certificate lifecycle: a management command, REST client, lifecycle helpers, DB model and migration, analytics API endpoints and views, licensing integration, analytics upload fallback to mTLS, settings, and unit tests.

Changes

Cohort / File(s) Summary
Management Command
awx/main/management/commands/candlepin_manage.py
New Django management command with register and renew subcommands, CLI overrides for URL/CA/proxy, --force/--dry-run, credential resolution, DB checks, and error reporting.
Candlepin REST Client
awx/main/utils/candlepin/client.py
New CandlepinClient with base URL normalization, TLS/CA and proxy handling, secure temp PEM file helpers, and methods: register_consumer, get_consumer, checkin, regenerate_cert with explicit error handling and JSON parsing.
Lifecycle Utilities
awx/main/utils/candlepin/lifecycle.py
PEM parsing, validity checks, renewal decision logic, run_candlepin_lifecycle orchestration (checkin, server serial comparison, regeneration), settings accessors, and monitoring helpers (get_certificate_info, check_certificate_health).
Package Init
awx/main/utils/candlepin/__init__.py
Package initializer re-exporting check_certificate_health and get_certificate_info and defining __all__.
Licensing Integration
awx/main/utils/licensing.py
DB-backed helpers to fetch/persist Candlepin cert/key/UUID, registration/renewal wiring, and new get_or_generate_candlepin_certificate integrating lifecycle and persistence.
Analytics Upload
awx/main/analytics/core.py
Upload flow now attempts mTLS using temporary cert/key files, then falls back to basic auth and finally OIDC; adds _temp_cert_files context manager and updates error handling.
API Routes & Views
awx/api/urls/analytics.py, awx/api/views/analytics.py
Added certificate_health and certificate_status endpoints and permission-protected views that call lifecycle helpers and return health/status payloads with appropriate HTTP codes and error handling.
DB Model & Migration
awx/main/models/candlepin.py, awx/main/migrations/0206_candlepin_certificate.py, awx/main/models/__init__.py
New CandlepinCertificate model (singleton) with consumer_uuid, cert_pem, key_pem, serial_number, expires_at; migration to create table; singleton accessors, update_certificate, has_valid_data, and model exported via package import.
Settings Defaults
awx/settings/defaults.py
Added AWX_ANALYTICS_CANDLEPIN_URL, AWX_ANALYTICS_CANDLEPIN_CA, AWX_ANALYTICS_CANDLEPIN_RENEWAL_THRESHOLD_DAYS, AWX_ANALYTICS_CANDLEPIN_PROXY_URL.
Client Tests
awx/main/tests/unit/utils/test_candlepin_client.py
Unit tests for CandlepinClient initialization, TLS/proxy behavior, temp PEM handling, file permissions/lifecycle, and mocked HTTP interactions for register/get/checkin/regenerate.
Lifecycle Tests
awx/main/tests/unit/utils/test_candlepin_lifecycle.py
Unit tests for parsing, validity/renewal logic, config accessors, and run_candlepin_lifecycle scenarios: no-renewal, renewal, serial-mismatch, and healthy serial-match.
Licensing Tests
awx/main/tests/unit/utils/test_candlepin_licensing.py
Unit tests for DB helpers, credential fetching, save/persist flows, registration orchestration, and lifecycle error handling.

Sequence Diagrams

sequenceDiagram
    participant CLI as CLI (register)
    participant DB as Database
    participant CC as CandlepinClient
    participant CP as Candlepin Server

    CLI->>DB: Check existing cert/key
    alt No cert or --force
        CLI->>DB: Fetch stored credentials
        CLI->>CC: Instantiate with URL/CA/proxy
        CC->>CP: POST /consumers?owner={org}
        CP-->>CC: JSON {uuid, idCert}
        CC-->>CLI: (cert_pem, key_pem, consumer_uuid)
        CLI->>CLI: Parse & display cert details
        alt Not --dry-run
            CLI->>DB: Save cert/key/UUID
        end
    else Cert exists and no force
        CLI->>CLI: Exit (no-op)
    end
Loading
sequenceDiagram
    participant CLI as CLI (renew)
    participant DB as Database
    participant CC as CandlepinClient
    participant CP as Candlepin Server

    CLI->>DB: Fetch stored cert/key/UUID
    alt UUID missing or placeholder
        CLI->>CLI: Fail with error
    else Valid UUID
        CLI->>CLI: Parse current cert
        CLI->>CC: Instantiate client
        CC->>CP: PUT /consumers/{uuid} (checkin)
        CP-->>CC: 200/204
        CLI->>CLI: Evaluate needs_renewal (unless --force)
        alt Renewal needed or --force
            CLI->>CC: POST /consumers/{uuid} (regenerate)
            CC->>CP: POST /consumers/{uuid}
            CP-->>CC: JSON {idCert}
            CC-->>CLI: (new_cert_pem, new_key_pem)
            CLI->>CLI: Display old/new cert details
            alt Not --dry-run
                CLI->>DB: Save renewed cert/key
            end
        else No renewal needed
            CLI->>CLI: Exit (no changes)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement Candlepin certificate integration' clearly and accurately summarizes the main change: integrating Candlepin for certificate management in the codebase, which is the primary objective across all modified and added files.
Docstring Coverage ✅ Passed Docstring coverage is 83.70% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

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: 5

🤖 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/management/commands/candlepin_manage.py`:
- Around line 79-80: Do not accept the subscription password as a command-line
argument; update the argument handling around reg.add_argument('--password',
...) to prevent exposing the secret in shell history/process listings by
replacing the current --password argv option with a secure input method: either
remove the --password flag and call getpass.getpass() (importing getpass)
wherever the command builds credentials (e.g., in the management command's
argument parsing/handle logic), or add a --password-stdin boolean flag and
implement reading the password from stdin when that flag is set; ensure the code
that previously read args.password now reads the secure source and no longer
registers a plain --password argument.
- Around line 220-226: The manual renew command is ignoring the result of
client.checkin, allowing failures to be masked; update the code around
client.checkin(consumer_uuid, cert_pem, key_pem) in candlepin_manage.py to
capture its return or handle exceptions (e.g., checkin_result =
client.checkin(...)), verify success before proceeding to compute
renewal_needed, and if checkin fails raise a CommandError or return a
non-zero/failure value so the CLI exits with an error instead of printing "No
renewal needed"; ensure any exceptions from client.checkin are caught and
surfaced to the operator rather than being discarded.

In `@awx/main/tests/unit/utils/test_candlepin_licensing.py`:
- Around line 35-36: Replace the mock.patch target from the non-existent module
attribute to the real Django DB connection: change
mock.patch('awx.main.utils.licensing.connection') to
mock.patch('django.db.connection') in the affected tests (e.g.,
test_fetch_candlepin_lifecycle_from_db and the other two tests on the same
file), so the functions that import connection locally will receive the mocked
django.db.connection.

In `@awx/main/utils/licensing.py`:
- Around line 651-658: The current call to _upsert_conf_settings followed by
logger.info treats a failed DB upsert as success; update the callers (the block
using _upsert_conf_settings that logs "Renewed Candlepin cert and key saved to
conf_setting." and the other similar sites around the file) to check the boolean
result and stop/propagate the failure instead of continuing. Specifically, if
_upsert_conf_settings(...) returns False, either raise an exception or return an
error from the surrounding function (or perform compensating action such as
deleting or rolling back the remote Candlepin consumer/credential you just
created via the consumer POST) so that the remote mutation is not left live
while local state remains stale; ensure this behavior is applied to the
occurrences referencing _upsert_conf_settings, the logger.info lines, and their
surrounding registration/renewal flows.
- Around line 593-601: The raw SQL read/write in licensing.py (the blocks using
django.db.connection.cursor() and json.loads/json.dumps) bypasses Setting
encryption hooks and causes SUBSCRIPTIONS_PASSWORD (and SUBSCRIPTIONS_USERNAME)
to be stored/read incorrectly; replace those raw SQL accesses with the Setting
model or awx.conf.settings accessor (e.g., use Setting.get() /
Setting.objects.get()/awx.conf.settings[...] as appropriate) for keys registered
with encrypted=True so values are encrypted on write and decrypted on read,
while keeping raw SQL only for unregistered keys like
CANDLEPIN_CONSUMER_CERT/KEY/UUID; ensure the code paths that currently build
placeholders and call cursor.execute() are updated to use the Setting API for
the subscription keys and that json.dumps/json.loads usage is removed or applied
through the Setting API.
🪄 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: 6f24b248-43ab-4201-b26d-b81336830a51

📥 Commits

Reviewing files that changed from the base of the PR and between b83019b and 90bf988.

📒 Files selected for processing (8)
  • awx/main/management/commands/candlepin_manage.py
  • awx/main/tests/unit/utils/test_candlepin_client.py
  • awx/main/tests/unit/utils/test_candlepin_licensing.py
  • awx/main/tests/unit/utils/test_candlepin_lifecycle.py
  • awx/main/utils/candlepin/__init__.py
  • awx/main/utils/candlepin/client.py
  • awx/main/utils/candlepin/lifecycle.py
  • awx/main/utils/licensing.py

Comment thread awx/main/management/commands/candlepin_manage.py Outdated
Comment thread awx/main/management/commands/candlepin_manage.py
Comment thread awx/main/tests/unit/utils/test_candlepin_licensing.py Outdated
Comment thread awx/main/utils/licensing.py Outdated
Comment thread awx/main/utils/licensing.py Outdated
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: 9

♻️ Duplicate comments (3)
awx/main/tests/unit/utils/test_candlepin_licensing.py (1)

70-80: ⚠️ Potential issue | 🔴 Critical

Patch django.db.connection, not a missing module attribute.

_fetch_registration_credentials_from_db() imports connection inside the function, so @mock.patch('awx.main.utils.licensing.connection') will raise before the test runs. Patch django.db.connection instead so the local import binds to the mock.

Run this to confirm the helper uses a function-local import while the test patches a nonexistent module attribute:

#!/bin/bash
set -euo pipefail

echo "Helper implementation:"
rg -n -C2 "def _fetch_registration_credentials_from_db|from django\\.db import connection" awx/main/utils/licensing.py

echo
echo "Current test patch target:"
rg -n -C1 "@mock\\.patch\\('awx\\.main\\.utils\\.licensing\\.connection'\\)" awx/main/tests/unit/utils/test_candlepin_licensing.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@awx/main/tests/unit/utils/test_candlepin_licensing.py` around lines 70 - 80,
The test is patching awx.main.utils.licensing.connection but
_fetch_registration_credentials_from_db imports connection locally, so the patch
never applies; update the test decorator to patch django.db.connection instead
(change the `@mock.patch` target used on the
test_fetch_registration_credentials_from_db function) so the local import inside
_fetch_registration_credentials_from_db binds to the mock_connection used in the
test.
awx/main/management/commands/candlepin_manage.py (2)

81-82: ⚠️ Potential issue | 🟠 Major

Don't accept the subscription password on argv.

Line 82 still exposes the RHSM secret via shell history and process listings. Please switch this to an interactive prompt or a --password-stdin style flow instead of a plain --password option.

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

In `@awx/main/management/commands/candlepin_manage.py` around lines 81 - 82, The
command currently adds a plain-text password flag via
reg.add_argument('--password') which exposes RHSM secrets; change the
implementation in the candlepin_manage management command to remove the
'--password' CLI option and instead support a secure input flow: accept either a
'--password-stdin' flag that reads the password from STDIN or, when no password
provided and a terminal is attached, prompt interactively (e.g., using
getpass.getpass) before using the value; update any code paths that reference
the parsed 'password' argument accordingly and preserve the existing
'--username' behavior.

224-225: ⚠️ Potential issue | 🟠 Major

Fail the manual renew command when check-in fails.

CandlepinClient.checkin() returns False on network/API failure, but Line 225 discards the result and can still fall through to "No renewal needed" with exit code 0. This should surface as command failure instead of silently succeeding.

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

In `@awx/main/management/commands/candlepin_manage.py` around lines 224 - 225, The
checkin result from CandlepinClient.checkin is ignored; change the call in the
command's handle method so you capture its return value (e.g. result =
client.checkin(consumer_uuid, cert_pem, key_pem)) and if result is False treat
it as a failure: write an error to stderr and fail the management command (raise
django.core.management.base.CommandError or exit non‑zero) instead of continuing
to the "No renewal needed" path. Ensure you keep the existing self.stdout.write
message and reference consumer_uuid/cert_pem/key_pem when logging.
🧹 Nitpick comments (1)
awx/main/analytics/core.py (1)

414-414: Raising requests.RequestException with a string message is unconventional.

requests.RequestException typically wraps actual request errors. Using it as a control flow mechanism with a custom message string may cause confusion. Consider using a different approach such as a simple flag variable or a custom exception.

♻️ Alternative using a flag variable
+                use_basic_auth = False
+
                 if cert_path and key_path:
                     logger.debug("Using certificate-based authentication for analytics upload")
                     try:
                         response = s.post(url, files=files, cert=(cert_path, key_path), verify=settings.INSIGHTS_CERT_PATH, headers=s.headers, timeout=(31, 31))
                     except requests.RequestException as e:
                         logger.warning(f"Certificate-based authentication failed: {e}, falling back to basic auth")
-                        raise
+                        use_basic_auth = True
                 else:
                     # No certificate available, skip to basic auth
                     logger.debug("No Candlepin certificate available, falling back to basic authentication")
-                    raise requests.RequestException("No certificate available")
+                    use_basic_auth = True

-            except requests.RequestException:
+                if use_basic_auth:
                 # Second attempt: Basic authentication
                 try:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@awx/main/analytics/core.py` at line 414, The code currently raises
requests.RequestException with a custom message via the statement `raise
requests.RequestException("No certificate available")`; replace this
unconventional use with either a simple control-flow flag/return or a dedicated
exception type: define a local custom exception (e.g., NoCertificateError) or
use a built-in like ValueError/RuntimeError, then raise that instead with
contextual details, or refactor the surrounding function to return a
boolean/None to indicate the missing certificate; update any callers that expect
the old RequestException to handle the new return/exception accordingly.
🤖 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/api/views/analytics.py`:
- Line 414: The view references AWX_ANALYTICS_CERTIFICATE_AUTH_ENABLED via
getattr(settings, ...); add an explicit default constant named
AWX_ANALYTICS_CERTIFICATE_AUTH_ENABLED = True in the project's defaults settings
module (next to the other Candlepin/analytics settings) with a short comment
like "Enable certificate-based authentication for analytics" so the setting is
documented and consistent with other defaults; no changes needed to the view
code.
- Line 417: The has_redhat_credentials flag only checks REDHAT_USERNAME or
SUBSCRIPTIONS_USERNAME; update the expression that sets has_redhat_credentials
to also return True when both service account settings are present by checking
getattr(settings, 'SUBSCRIPTIONS_CLIENT_ID', None) and getattr(settings,
'SUBSCRIPTIONS_CLIENT_SECRET', None) (i.e., OR the existing basic-auth check
with a check that both client id and client secret exist). Locate the assignment
for has_redhat_credentials in analytics.py and modify it to include this
additional service-account check.

In `@awx/main/analytics/core.py`:
- Around line 406-410: The exception handler currently sets cert_path and
key_path to None before re-raising (in the except requests.RequestException as e
block), which prevents the finally cleanup from deleting the temporary files;
revert that change so the original cert_path and key_path remain intact for
cleanup: either remove the assignment cert_path, key_path = None from the except
block or capture original paths into temp variables (e.g., orig_cert_path,
orig_key_path) before trying mTLS and use those for cleanup in the finally,
ensuring the exception is still re-raised without losing the file path
references used by the finally block.

In `@awx/main/management/commands/candlepin_manage.py`:
- Around line 178-183: The parse_cert() call used solely for display may raise
ValueError and currently aborts the command before the new cert/consumer is
persisted; change the flow in the management command (candlepin_manage.Command
methods where parse_cert is called) to persist/save the certificate/consumer
first and then attempt to parse for operator output, wrapping parse_cert(...) in
a try/except ValueError so parsing failures are best-effort: on success print
the existing formatted lines (consumer_uuid, info["serial"], info["cn"],
info["not_after"], info["days_remaining"]); on failure still report "Registered
successfully" and consumer_uuid and emit a concise fallback message like
"Certificate metadata unavailable: <error message>" to stdout/stderr. Apply the
same try/except pattern to the other block around the parse_cert call referenced
at lines 240-244.
- Around line 188-189: The CLI prints "saved to database" unconditionally
despite `_save_candlepin_registration_to_db` and `_save_candlepin_cert_to_db`
swallowing errors; change the flow so the command only reports success when a
save actually succeeded: modify the two utility functions to either raise on
failure or return a boolean/explicit status (e.g., True/False or a saved
object), then update the management command calls in `candlepin_manage.py` (the
places that call `_save_candlepin_registration_to_db(cert_pem, key_pem,
consumer_uuid)` and the `_save_candlepin_cert_to_db(...)` call around lines with
the stdout.write success messages) to check the returned status or catch
exceptions and only write the "saved to database" message on success, otherwise
propagate or print a clear failure status. Ensure you update both call sites.

In `@awx/main/models/candlepin.py`:
- Around line 31-35: The model currently relies on consumer_uuid.unique which
doesn't prevent multiple rows; change the model to enforce a DB-level singleton
(e.g., add a fixed primary key or a singleton_guard field with unique=True) and
update the singleton accessors to use that key explicitly. Concretely, add a
non-changeable singleton field (for example an Integer/PositiveSmallInteger
primary key with default=1 and editable=False or a Boolean/Char field defaulting
to a constant with unique=True) in the CandlepinCertificate model, and modify
get_instance() and get_or_create_instance() to call get()/get_or_create() using
that primary key or singleton field (e.g., pk=1 or singleton_guard=True) so the
DB will only ever allow one row and the accessors always target that single
record; apply the same change pattern to the other model code referenced around
lines 65-87.
- Around line 23-47: regenerate_secret_key.Command.handle is missing support for
re-encrypting CandlepinCertificate.PASSWORD_FIELDS so existing cert_pem/key_pem
remain encrypted with the old SECRET_KEY; add a private method
_candlepin_certificates() to the Command class following the same pattern used
for _unified_job and _credential (iterate CandlepinCertificate objects, decrypt
then re-encrypt each field in CandlepinCertificate.PASSWORD_FIELDS and save),
and call this method from handle() alongside the UnifiedJob and Credential calls
to ensure cert_pem and key_pem are rotated.

In `@awx/main/tests/unit/utils/test_candlepin_licensing.py`:
- Around line 96-109: The test stubs for parse_cert are returning 'expires_at'
but the code under test reads 'not_after', so update the mocks in
test_candlepin_licensing.py (the parse_cert return value used by tests that call
_save_candlepin_cert_to_db and _save_candlepin_registration_to_db) to include a
'not_after' key with the ISO timestamp (or include both 'not_after' and
'expires_at' to be explicit), ensuring the expiry parsing path is exercised and
the assertions that 'expires_at' (or parsed expiry) is persisted are meaningful.

In `@awx/main/utils/candlepin/lifecycle.py`:
- Around line 81-88: The renewal threshold is off-by-one in needs_renewal:
change the comparison so certificates with exactly days_before_expiry remaining
are considered needing renewal; update the return in needs_renewal (which calls
parse_cert) to use <= instead of < so it matches the docstring and the existing
logic in get_certificate_info.

---

Duplicate comments:
In `@awx/main/management/commands/candlepin_manage.py`:
- Around line 81-82: The command currently adds a plain-text password flag via
reg.add_argument('--password') which exposes RHSM secrets; change the
implementation in the candlepin_manage management command to remove the
'--password' CLI option and instead support a secure input flow: accept either a
'--password-stdin' flag that reads the password from STDIN or, when no password
provided and a terminal is attached, prompt interactively (e.g., using
getpass.getpass) before using the value; update any code paths that reference
the parsed 'password' argument accordingly and preserve the existing
'--username' behavior.
- Around line 224-225: The checkin result from CandlepinClient.checkin is
ignored; change the call in the command's handle method so you capture its
return value (e.g. result = client.checkin(consumer_uuid, cert_pem, key_pem))
and if result is False treat it as a failure: write an error to stderr and fail
the management command (raise django.core.management.base.CommandError or exit
non‑zero) instead of continuing to the "No renewal needed" path. Ensure you keep
the existing self.stdout.write message and reference
consumer_uuid/cert_pem/key_pem when logging.

In `@awx/main/tests/unit/utils/test_candlepin_licensing.py`:
- Around line 70-80: The test is patching awx.main.utils.licensing.connection
but _fetch_registration_credentials_from_db imports connection locally, so the
patch never applies; update the test decorator to patch django.db.connection
instead (change the `@mock.patch` target used on the
test_fetch_registration_credentials_from_db function) so the local import inside
_fetch_registration_credentials_from_db binds to the mock_connection used in the
test.

---

Nitpick comments:
In `@awx/main/analytics/core.py`:
- Line 414: The code currently raises requests.RequestException with a custom
message via the statement `raise requests.RequestException("No certificate
available")`; replace this unconventional use with either a simple control-flow
flag/return or a dedicated exception type: define a local custom exception
(e.g., NoCertificateError) or use a built-in like ValueError/RuntimeError, then
raise that instead with contextual details, or refactor the surrounding function
to return a boolean/None to indicate the missing certificate; update any callers
that expect the old RequestException to handle the new return/exception
accordingly.
🪄 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 Plus

Run ID: d03e6907-713c-41fe-8f95-6c06fc79bb03

📥 Commits

Reviewing files that changed from the base of the PR and between 90bf988 and fa35d55.

📒 Files selected for processing (15)
  • awx/api/urls/analytics.py
  • awx/api/views/analytics.py
  • awx/main/analytics/core.py
  • awx/main/management/commands/candlepin_manage.py
  • awx/main/migrations/0206_candlepin_certificate.py
  • awx/main/models/__init__.py
  • awx/main/models/candlepin.py
  • awx/main/tests/unit/utils/test_candlepin_client.py
  • awx/main/tests/unit/utils/test_candlepin_licensing.py
  • awx/main/tests/unit/utils/test_candlepin_lifecycle.py
  • awx/main/utils/candlepin/__init__.py
  • awx/main/utils/candlepin/client.py
  • awx/main/utils/candlepin/lifecycle.py
  • awx/main/utils/licensing.py
  • awx/settings/defaults.py
✅ Files skipped from review due to trivial changes (1)
  • awx/main/utils/candlepin/init.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • awx/main/utils/licensing.py

Comment thread awx/api/views/analytics.py Outdated
Comment thread awx/api/views/analytics.py Outdated
Comment thread awx/main/analytics/core.py Outdated
Comment thread awx/main/management/commands/candlepin_manage.py Outdated
Comment thread awx/main/management/commands/candlepin_manage.py Outdated
Comment thread awx/main/models/candlepin.py Outdated
Comment thread awx/main/models/candlepin.py Outdated
Comment thread awx/main/tests/unit/utils/test_candlepin_licensing.py Outdated
Comment thread awx/main/utils/candlepin/lifecycle.py Outdated
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: 1

♻️ Duplicate comments (1)
awx/main/tests/unit/utils/test_candlepin_licensing.py (1)

94-97: ⚠️ Potential issue | 🟠 Major

Mock return value uses incorrect key — expiry persistence is untested.

Per a prior review, the implementation reads parse_cert(...)[\"not_after\"] to extract the expiry date, but these mocks return expires_at instead. The tests pass but do not verify that expiry is correctly persisted.

Proposed fix
         mock_parse_cert.return_value = {
             'serial': '123456',
-            'expires_at': '2027-01-01T00:00:00+00:00',
+            'not_after': '2027-01-01T00:00:00+00:00',
         }

And similarly for line 116-119:

         mock_parse_cert.return_value = {
             'serial': '789012',
-            'expires_at': '2027-01-01T00:00:00+00:00',
+            'not_after': '2027-01-01T00:00:00+00:00',
         }

Also applies to: 116-119

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

In `@awx/main/tests/unit/utils/test_candlepin_licensing.py` around lines 94 - 97,
The unit tests mock parse_cert to return {'serial': '123456', 'expires_at': ...}
but the code under test reads parse_cert(... )["not_after"], so update the mock
return values in test_candlepin_licensing (the mock named mock_parse_cert) to
use the 'not_after' key with the same timestamp string (and replicate the same
change for the second mock instance), ensuring the expiry field is correctly
exercised and persisted by the code paths that read parse_cert()["not_after"].
🤖 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/tests/unit/utils/test_candlepin_lifecycle.py`:
- Around line 32-39: The test assumes get_candlepin_url() returns the production
URL by default but the implementation of get_candlepin_url() (function
get_candlepin_url) returns None when the AWX_ANALYTICS_CANDLEPIN_URL setting is
absent; update the test in test_candlepin_lifecycle.py to assert None for the
default case (or alternatively set mock_settings.AWX_ANALYTICS_CANDLEPIN_URL to
the production URL before calling get_candlepin_url()), referencing
get_candlepin_url to locate the behavior to match.

---

Duplicate comments:
In `@awx/main/tests/unit/utils/test_candlepin_licensing.py`:
- Around line 94-97: The unit tests mock parse_cert to return {'serial':
'123456', 'expires_at': ...} but the code under test reads parse_cert(...
)["not_after"], so update the mock return values in test_candlepin_licensing
(the mock named mock_parse_cert) to use the 'not_after' key with the same
timestamp string (and replicate the same change for the second mock instance),
ensuring the expiry field is correctly exercised and persisted by the code paths
that read parse_cert()["not_after"].
🪄 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 Plus

Run ID: c9bf36f6-58e7-43d1-8924-6ea9ededc437

📥 Commits

Reviewing files that changed from the base of the PR and between fa35d55 and d6f5287.

📒 Files selected for processing (3)
  • awx/main/tests/unit/utils/test_candlepin_client.py
  • awx/main/tests/unit/utils/test_candlepin_licensing.py
  • awx/main/tests/unit/utils/test_candlepin_lifecycle.py

Comment thread awx/main/tests/unit/utils/test_candlepin_lifecycle.py Outdated
@jessicamack jessicamack force-pushed the candlepin-integration branch from d6f5287 to 4258d86 Compare April 15, 2026 16:02
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/analytics/core.py`:
- Around line 421-425: In the basic-auth fallback except block inside ship(),
wrap the OIDCClient(rh_id, rh_secret).make_request call in its own try/except so
any exceptions from client.make_request are caught; on exception log the failure
(including exception details) and ensure the code sets response to a falsy value
(or returns False) so ship() does not raise but returns the failure result to
the gather path. Reference OIDCClient, client.make_request, and the existing
except requests.RequestException as e block when adding the inner try/except and
logging.
- Around line 394-425: The multipart upload reuses the same open file handle in
the files payload so retries send an empty stream; before each alternative POST
(the certificate-based attempt in the cert_path branch, the basic-auth s.post
call, and the OIDCClient.make_request call) rewind or recreate the files
payload: for any open file objects referenced in files (used by the s.post calls
and OIDCClient.make_request), call .seek(0) on each file descriptor or rebuild
the files tuple/list from fresh open file handles so the multipart encoder
re-reads the archive; update the blocks around
get_or_generate_candlepin_certificate, the basic auth s.post, and the
OIDCClient(...) call to reset the files stream before each request.

In `@awx/main/utils/licensing.py`:
- Around line 592-597: The code returns encrypted blobs for
CandlepinCertificate.cert_pem and key_pem; instead, call decrypt_field on both
before returning so callers get raw PEM bytes/strings: fetch instance via
CandlepinCertificate.get_instance(), validate as before, then replace returning
instance.cert_pem and instance.key_pem with decrypt_field(instance, "cert_pem")
and decrypt_field(instance, "key_pem") (keep returning instance.consumer_uuid
unchanged); also add the import for decrypt_field from awx.main.utils.encryption
near the other imports.
🪄 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 Plus

Run ID: 9f2eb09d-8483-4e16-8c18-151c8fed5522

📥 Commits

Reviewing files that changed from the base of the PR and between d6f5287 and 4258d86.

📒 Files selected for processing (15)
  • awx/api/urls/analytics.py
  • awx/api/views/analytics.py
  • awx/main/analytics/core.py
  • awx/main/management/commands/candlepin_manage.py
  • awx/main/migrations/0206_candlepin_certificate.py
  • awx/main/models/__init__.py
  • awx/main/models/candlepin.py
  • awx/main/tests/unit/utils/test_candlepin_client.py
  • awx/main/tests/unit/utils/test_candlepin_licensing.py
  • awx/main/tests/unit/utils/test_candlepin_lifecycle.py
  • awx/main/utils/candlepin/__init__.py
  • awx/main/utils/candlepin/client.py
  • awx/main/utils/candlepin/lifecycle.py
  • awx/main/utils/licensing.py
  • awx/settings/defaults.py
✅ Files skipped from review due to trivial changes (4)
  • awx/main/models/init.py
  • awx/api/urls/analytics.py
  • awx/main/utils/candlepin/init.py
  • awx/main/tests/unit/utils/test_candlepin_lifecycle.py

Comment thread awx/main/analytics/core.py Outdated
Comment thread awx/main/analytics/core.py Outdated
Comment thread awx/main/utils/licensing.py Outdated
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3 Security Hotspots
45.7% Coverage on New Code (required ≥ 80%)
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copy link
Copy Markdown
Member

@tvo318 tvo318 left a comment

Choose a reason for hiding this comment

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

Code Review

This PR adds Candlepin mTLS certificate integration for analytics authentication — a significant feature with solid test coverage and clean module organization. Found one HIGH issue (temporary certificate file leak on mTLS failure in core.py), two MEDIUM issues (misleading docstring about TLS verification defaults, race condition in singleton creation), and one LOW style nit. The temp file leak is the most important to fix before merge as it can leave private key material on disk.

Comment thread awx/main/analytics/core.py Outdated
Comment thread awx/main/analytics/core.py Outdated
Comment thread awx/main/utils/candlepin/lifecycle.py Outdated
Comment thread awx/main/models/candlepin.py Outdated
Comment thread awx/main/utils/licensing.py Outdated
@jessicamack jessicamack requested a review from tvo318 April 21, 2026 13:12
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: 4

♻️ Duplicate comments (2)
awx/main/management/commands/candlepin_manage.py (2)

220-249: ⚠️ Potential issue | 🟠 Major

Keep renew robust when current cert parsing fails.

After a parse_cert() failure, info is undefined, but later paths still use it for the no-renewal message and old serial output. Also, needs_renewal() can raise the same ValueError outside the earlier best-effort block.

Proposed fix
+        info = None
         try:
             info = parse_cert(cert_pem)
             self.stdout.write('Current certificate:')
@@
-        renewal_needed = force or needs_renewal(cert_pem, renewal_days)
+        try:
+            renewal_needed = force or needs_renewal(cert_pem, renewal_days)
+        except ValueError as e:
+            self.stderr.write(f'Could not determine renewal status from stored certificate: {e}')
+            return False
         if not renewal_needed:
-            self.stdout.write(f'Certificate has {info["days_remaining"]} days remaining (renewal threshold: {renewal_days} days). No renewal needed.')
+            self.stdout.write(f'Certificate has {info["days_remaining"]} days remaining (renewal threshold: {renewal_days} days). No renewal needed.')
             return True
@@
         try:
             new_info = parse_cert(new_cert_pem)
-            self.stdout.write(f'  Old serial    : {info["serial"]}')
+            self.stdout.write(f'  Old serial    : {info["serial"] if info else "unknown"}')
             self.stdout.write(f'  New serial    : {new_info["serial"]}')
             self.stdout.write(f'  Valid until   : {new_info["not_after"]} ({new_info["days_remaining"]} days remaining)')

Also applies to: 272-279

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

In `@awx/main/management/commands/candlepin_manage.py` around lines 220 - 249,
parse_cert() failures leave info undefined and later code (the "no renewal
needed" message and the old-serial output) and the needs_renewal() call assume
info exists or can re-raise; fix by initializing info to a safe default dict
(e.g., keys "serial", "cn", "not_after", "days_remaining" with fallback values
like "unknown" or 0) before the try/except around parse_cert(), and change the
needs_renewal(cert_pem, renewal_days) call to be wrapped in a try/except
ValueError that treats parsing errors as "renewal needed" (or logs and forces
renewal when --force is not set); update any uses of info (e.g., the no-renewal
message and the earlier old serial output) to reference the safe defaults so
they never reference an undefined variable; apply the same pattern to the other
occurrence that uses parse_cert/needs_renewal (the later block around
consumer_uuid/CandlepinClient.checkin).

50-52: ⚠️ Potential issue | 🟠 Major

Don’t accept the subscription password on argv.

--password exposes the Red Hat subscription secret through shell history and process listings. Use stdin or an interactive prompt instead.

Proposed fix using stdin
-                    '            LICENSE.account_number).  Pass --username / --password /  ',
+                    '            LICENSE.account_number).  Pass --username / --password-stdin /',
                     '            --org to override.',
@@
-        reg.add_argument('--password', help='Red Hat subscription password (overrides REDHAT_PASSWORD from database)')
+        reg.add_argument('--password-stdin', dest='password_stdin', action='store_true', help='Read Red Hat subscription password from stdin')
@@
-        password = options.get('password') or db_password
+        password = sys.stdin.readline().rstrip('\n') if options.get('password_stdin') else db_password
@@
-            missing.append('password (pass --password or set REDHAT_PASSWORD in database)')
+            missing.append('password (pass --password-stdin or set REDHAT_PASSWORD in database)')

Also applies to: 81-83, 131-139

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

In `@awx/main/management/commands/candlepin_manage.py` around lines 50 - 52, The
command currently accepts the subscription secret via the --password CLI option
(and reads options['password']) which leaks secrets in process listings; remove
or stop using the --password argv value and instead obtain the password via a
secure prompt or stdin: stop adding/using the --password argument (or treat it
as deprecated) and, where the code references options.get('password') /
args.password (e.g., in the candlepin management command handler and any helper
methods that consume password), replace that usage with a secure read: if stdin
is not a tty read the password bytes from sys.stdin (strip newline), otherwise
call getpass.getpass(prompt) to interactively prompt; apply this change
consistently for all occurrences that currently accept --password (the blocks
around the current --password handling and subsequent uses).
🧹 Nitpick comments (2)
awx/main/utils/candlepin/client.py (1)

212-230: Minor: redundant chmod (tempfile.mkstemp already creates the file with mode 0600).

tempfile.mkstemp() on POSIX already creates the file with permissions 0o600 owned by the current user, so the explicit os.chmod(path, 0o600) is a no-op. Leaving it is harmless, but the ordering (chmod before write) is slightly misleading. Also note the os.close(fd) in the except path can double-close if the with os.fdopen(fd, 'w') context opened successfully and then write() raised — the with already closed fd — but the OSError is caught so no functional bug. Optional cleanup only.

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

In `@awx/main/utils/candlepin/client.py` around lines 212 - 230, The
_write_temp_pem function has a redundant os.chmod call and a potential
double-close of the raw fd: remove the os.chmod(path, 0o600) (tempfile.mkstemp
already sets 0o600) and ensure the raw fd is not closed twice by setting fd =
None as soon as os.fdopen(fd, 'w') successfully takes ownership (or otherwise
only calling os.close(fd) in the except branch when the fd still refers to an
open descriptor). Update the code in _write_temp_pem to eliminate os.chmod and
adjust the exception handling around os.fdopen / os.close / os.unlink so you
only close the raw fd if it was never wrapped by os.fdopen.
awx/main/analytics/core.py (1)

35-67: Duplicate temp-cert helper — consolidate with CandlepinClient._temp_cert_files.

awx/main/utils/candlepin/client.py already defines an equivalent _temp_cert_files context manager with the same contract (write PEM cert+key to secure temp files, 0o600 on the key, cleanup on exit). Duplicating it here doubles the surface for future cert-handling changes (permissions, error handling, disk cleanup on crash). Consider importing the existing helper or extracting a single shared utility.

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

In `@awx/main/analytics/core.py` around lines 35 - 67, The file defines a
duplicate _temp_cert_files context manager; replace this local duplicate by
reusing the existing helper from CandlepinClient._temp_cert_files (or extract a
shared utility) so there is a single implementation to maintain permissions,
writing and cleanup behavior; update imports to import the helper from
awx.main.utils.candlepin.client (or move both implementations into a new shared
module and import it from both places), then remove the local _temp_cert_files
definition in awx/main/analytics/core.py and ensure callers now use the
imported/shared _temp_cert_files.
🤖 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/analytics/core.py`:
- Around line 429-460: When the certificate-based POST via s.post in the block
that uses get_or_generate_candlepin_certificate and _temp_cert_files returns a
non-success HTTP status, add a logger.warning that includes the
response.status_code and response.text so cert-present-but-rejected cases are
visible (i.e., after the if response.status_code < 300: return True add an else:
logger.warning(...)). Do the same for the basic-auth s.post call (the block that
calls s.post with auth=(rh_id, rh_secret)): if response.status_code >= 300 log a
warning with status and body before falling through to OIDC; keep existing
exception handlers unchanged.

In `@awx/main/management/commands/candlepin_manage.py`:
- Around line 90-104: The --dry-run flag currently still performs non-idempotent
Candlepin actions; update the command handlers so the dry-run check happens
before any Candlepin write operations (specifically ensure you exit early when
options.dry_run is true prior to calling register_consumer() in the register
flow and prior to calling regenerate_cert() (or whatever function rotates the
server cert) in the renew flow), or alternatively rename the flag to --no-save
and document it as unsafe; make the change in the command logic that processes
the 'register' and 'renew' subparsers so no external Candlepin writes occur when
the flag is set.

In `@awx/main/utils/candlepin/lifecycle.py`:
- Around line 225-246: get_certificate_info currently only considers expiry and
can label future-not_before certificates as "valid"; update the logic (after
parse_cert and before expiry checks in get_certificate_info) to detect
certificates that are not yet valid (use cert_info['not_before'] or a
days-until-valid value from parse_cert, or call is_cert_valid(cert_pem) to reuse
existing validation) and mark them unhealthy: set info['status'] =
'not_yet_valid' (or 'invalid'), info['needs_renewal'] = True and provide an
explanatory info['message'] like "Certificate not valid until <not_before>".
Ensure this check runs prior to the existing days_until_expiry /
renewal_threshold branch so future-dated certs are not reported as 'valid'.

In `@awx/main/utils/licensing.py`:
- Around line 778-820: get_or_generate_candlepin_certificate accepts
username/password but never uses them; wire them through to registration so
caller-supplied credentials are honored. Change the call to
_register_candlepin_consumer(username, password) in
get_or_generate_candlepin_certificate, update _register_candlepin_consumer to
accept optional username/password parameters and pass them into
client.register_consumer(username, password, org, install_uuid) (falling back to
the existing settings/db lookup when None), and update docstring/usage to
reflect that caller credentials will be used for Candlepin POST /consumers.

---

Duplicate comments:
In `@awx/main/management/commands/candlepin_manage.py`:
- Around line 220-249: parse_cert() failures leave info undefined and later code
(the "no renewal needed" message and the old-serial output) and the
needs_renewal() call assume info exists or can re-raise; fix by initializing
info to a safe default dict (e.g., keys "serial", "cn", "not_after",
"days_remaining" with fallback values like "unknown" or 0) before the try/except
around parse_cert(), and change the needs_renewal(cert_pem, renewal_days) call
to be wrapped in a try/except ValueError that treats parsing errors as "renewal
needed" (or logs and forces renewal when --force is not set); update any uses of
info (e.g., the no-renewal message and the earlier old serial output) to
reference the safe defaults so they never reference an undefined variable; apply
the same pattern to the other occurrence that uses parse_cert/needs_renewal (the
later block around consumer_uuid/CandlepinClient.checkin).
- Around line 50-52: The command currently accepts the subscription secret via
the --password CLI option (and reads options['password']) which leaks secrets in
process listings; remove or stop using the --password argv value and instead
obtain the password via a secure prompt or stdin: stop adding/using the
--password argument (or treat it as deprecated) and, where the code references
options.get('password') / args.password (e.g., in the candlepin management
command handler and any helper methods that consume password), replace that
usage with a secure read: if stdin is not a tty read the password bytes from
sys.stdin (strip newline), otherwise call getpass.getpass(prompt) to
interactively prompt; apply this change consistently for all occurrences that
currently accept --password (the blocks around the current --password handling
and subsequent uses).

---

Nitpick comments:
In `@awx/main/analytics/core.py`:
- Around line 35-67: The file defines a duplicate _temp_cert_files context
manager; replace this local duplicate by reusing the existing helper from
CandlepinClient._temp_cert_files (or extract a shared utility) so there is a
single implementation to maintain permissions, writing and cleanup behavior;
update imports to import the helper from awx.main.utils.candlepin.client (or
move both implementations into a new shared module and import it from both
places), then remove the local _temp_cert_files definition in
awx/main/analytics/core.py and ensure callers now use the imported/shared
_temp_cert_files.

In `@awx/main/utils/candlepin/client.py`:
- Around line 212-230: The _write_temp_pem function has a redundant os.chmod
call and a potential double-close of the raw fd: remove the os.chmod(path,
0o600) (tempfile.mkstemp already sets 0o600) and ensure the raw fd is not closed
twice by setting fd = None as soon as os.fdopen(fd, 'w') successfully takes
ownership (or otherwise only calling os.close(fd) in the except branch when the
fd still refers to an open descriptor). Update the code in _write_temp_pem to
eliminate os.chmod and adjust the exception handling around os.fdopen / os.close
/ os.unlink so you only close the raw fd if it was never wrapped by os.fdopen.
🪄 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 Plus

Run ID: 398f6649-1d6e-4b78-ab28-6ba16a5e2037

📥 Commits

Reviewing files that changed from the base of the PR and between 4258d86 and a258c00.

📒 Files selected for processing (12)
  • awx/api/views/analytics.py
  • awx/main/analytics/core.py
  • awx/main/management/commands/candlepin_manage.py
  • awx/main/migrations/0206_candlepin_certificate.py
  • awx/main/models/candlepin.py
  • awx/main/tests/unit/utils/test_candlepin_client.py
  • awx/main/tests/unit/utils/test_candlepin_licensing.py
  • awx/main/tests/unit/utils/test_candlepin_lifecycle.py
  • awx/main/utils/candlepin/client.py
  • awx/main/utils/candlepin/lifecycle.py
  • awx/main/utils/licensing.py
  • awx/settings/defaults.py
✅ Files skipped from review due to trivial changes (2)
  • awx/settings/defaults.py
  • awx/main/tests/unit/utils/test_candlepin_lifecycle.py

Comment thread awx/main/analytics/core.py
Comment thread awx/main/management/commands/candlepin_manage.py
Comment thread awx/main/utils/candlepin/lifecycle.py Outdated
Comment thread awx/main/utils/licensing.py Outdated
Comment thread awx/main/utils/licensing.py Outdated
Comment thread awx/main/analytics/core.py Outdated
Comment thread awx/main/analytics/core.py Outdated
Comment thread awx/main/analytics/core.py Outdated
Comment thread awx/main/utils/licensing.py Outdated
Comment thread awx/main/analytics/core.py Outdated
Comment thread awx/main/conf.py
)

register(
'CANDLEPIN_EXPIRES_AT',
Copy link
Copy Markdown
Member

@fosterseth fosterseth Apr 27, 2026

Choose a reason for hiding this comment

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

does this need to be its own setting or can this trivially be obtained by parsing the certificate?

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.

4 participants