Skip to content

Preserve SHOTGUN_API_CACERTS precedence in shotgun_api3 cert patch#1122

Open
stevelittlefish wants to merge 2 commits into
masterfrom
ticket/SG-44256-shotgun-api-cacerts-precedence
Open

Preserve SHOTGUN_API_CACERTS precedence in shotgun_api3 cert patch#1122
stevelittlefish wants to merge 2 commits into
masterfrom
ticket/SG-44256-shotgun-api-cacerts-precedence

Conversation

@stevelittlefish

@stevelittlefish stevelittlefish commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • _patch_shotgun_api3_certs() in tank_vendor/__init__.py currently returns the Core-extracted cacert.pem unconditionally whenever ca_certs isn't explicitly passed, silently bypassing the documented SHOTGUN_API_CACERTS environment variable.
  • On setups where the extracted cert lives on a slow network share, this made the first ShotGrid connection in a process pay a large one-time filesystem cost — reported by a studio as ~20-33s in Nuke 17.0 (vs <1s in Nuke 14.1), isolated experimentally to the cold read of the network-hosted cert path (see internal ticket SG-44256).
  • This restores shotgun_api3's own documented precedence: explicit ca_certs argument → SHOTGUN_API_CACERTS env var → extracted Core cert fallback.

Test plan

  • Added TestShotgunAPI3CertPatchPrecedence in tests/core_tests/test_tank_vendor.py, calling _patch_shotgun_api3_certs() directly against a throwaway fake pkgs.zip/certs/ layout so behavior can be verified without depending on the real build-time layout:
    • SHOTGUN_API_CACERTS set → wins over the extracted cert (the regression this fixes)
    • unset → unchanged fallback to the extracted cert
    • empty string → treated as unset
    • explicit ca_certs argument → still outranks everything, including the env var
  • Verified the new "wins over extracted cert" test fails against the pre-fix code and passes after the fix, confirming it actually exercises the bug rather than passing vacuously.
  • Full core_tests suite (510 tests) passes with the fix applied, no regressions.

Follow-up fix (review feedback)

CI/review caught that the new test's tearDown was leaking global state across test modules when the full suite ran in one process:

TypeError: _patched_get_certs_file() takes from 0 to 1 positional arguments but 2 were given

Root cause: setUp captured shotgun_api3.Shotgun._get_certs_file via plain attribute access, which unwraps the installed staticmethod descriptor down to a raw function. tearDown then reassigned that raw function directly as the class attribute, turning _get_certs_file back into a regular instance method. Any Shotgun(...) constructed by a later test in the same process (e.g. tests/util_tests/test_shotgun_connect.py) then called self._get_certs_file(ca_certs), which passed both self and ca_certs into a function that only accepts ca_certs.

Fix: snapshot the raw descriptor via shotgun_api3.Shotgun.__dict__["_get_certs_file"] instead of attribute access, so the staticmethod wrapper is preserved on restore.

  • Reproduced the failure by running core_tests.test_tank_vendor + util_tests.test_shotgun_connect in the same process (matches how the full suite runs).
  • Confirmed it's fixed: same two modules together now pass (35/35).
  • Full test suite (1253 tests) passes with no regressions.

_patch_shotgun_api3_certs() unconditionally returned the Core-extracted
cacert.pem whenever ca_certs wasn't explicitly passed, silently ignoring
the documented SHOTGUN_API_CACERTS environment variable. When the
extracted cert lives on a slow network share, this made every cold
ShotGrid connection pay a large one-time filesystem cost (~20-30s
observed in Nuke 17) that a local cert copy avoids.

Restore shotgun_api3's documented precedence: explicit ca_certs ->
SHOTGUN_API_CACERTS -> extracted Core cert fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@carlos-villavicencio-adsk carlos-villavicencio-adsk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Just one comment to be considered.

Comment on lines +206 to +207
environment_certs = os.environ.get("SHOTGUN_API_CACERTS")
if environment_certs:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Once we support Python 3.9, I wonder if it would be a good idea to start using the walrus operator 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I prefer not to. Not because there's anything wrong with it, but it's just a weird bit of syntax that lots of developers are not familiar with.

# Preserve shotgun_api3's documented SHOTGUN_API_CACERTS override.
# Without this branch, a network-hosted extracted cert_file can be
# dramatically slower to open than a local override (see SG-44256).
environment_certs = os.environ.get("SHOTGUN_API_CACERTS")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you think it's a good idea to validate if the env var value is an actual file path instead of anything?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, I'd leave it unvalidated. Two reasons:

  1. Consistency with upstream. Look at shotgun_api3's own original _get_certs_file: when ca_certs is passed explicitly, it also returns it as-is with zero validation (if ca_certs is not None: return ca_certs). Our env-var branch is meant to restore that same documented precedence, not add stricter behavior than the thing it's mimicking.
  2. Silent fallback would hide misconfiguration. If we validated and silently fell back to the extracted cert on a bad path, someone who typos SHOTGUN_API_CACERTS would get no error — just the slow network-cert behavior again, with no signal as to why their override "isn't working." Letting a bad path flow through means SSL fails loudly at connection time, pointing straight at the actual problem.

If we wanted to help catch typos without changing the fallback semantics, a logger.warning() when the path doesn't exist (but still returning it, not falling back) would be a reasonable middle ground — but that's a scope increase beyond this bug fix, and this module doesn't have a logger wired in already. I'd leave it as-is unless you want me to add that warning.

Human version - this is consistent with everything else, and handling the validation is probably going to cause more problems than it solves.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request restores shotgun_api3’s documented CA cert precedence in tank_vendor._patch_shotgun_api3_certs() so that SHOTGUN_API_CACERTS is honored when no explicit ca_certs argument is provided, avoiding unnecessary slow reads of the Core-extracted cert in some environments.

Changes:

  • Update the shotgun_api3 cert patch to respect SHOTGUN_API_CACERTS when ca_certs is not explicitly provided.
  • Add a new test suite validating precedence across: explicit ca_certs arg, env var (including empty string), and extracted cert fallback.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
tests/core_tests/test_tank_vendor.py Adds a dedicated test class to validate cert precedence behavior for the shotgun_api3 cert patch.
python/tank_vendor/init.py Adjusts _patched_get_certs_file() to preserve SHOTGUN_API_CACERTS precedence before falling back to the extracted cert.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/core_tests/test_tank_vendor.py Outdated
Comment on lines +297 to +301
# Snapshot the method currently installed (already patched once at
# module import time) so we can restore it after each test, and so
# repeated patch applications in this test class don't stack.
self._original_get_certs_file = shotgun_api3.Shotgun._get_certs_file

setUp() captured shotgun_api3.Shotgun._get_certs_file via attribute
access, which unwraps the staticmethod descriptor to a plain function.
Restoring that plain function in tearDown re-assigned it as a regular
instance method, so any Shotgun() constructed by a later test in the
same process passed both self and ca_certs into a function that only
accepts ca_certs, breaking unrelated tests (e.g.
util_tests/test_shotgun_connect.py) with:

  TypeError: _patched_get_certs_file() takes from 0 to 1 positional
  arguments but 2 were given

Snapshot the raw descriptor from Shotgun.__dict__ instead, so the
staticmethod wrapper is preserved on restore.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# turn _get_certs_file into a bound instance method for every
# Shotgun() call afterward, breaking unrelated tests with a
# "takes 0 to 1 positional arguments but 2 were given" TypeError.
self._original_get_certs_file = shotgun_api3.Shotgun.__dict__[

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Black would make changes.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.10%. Comparing base (43a5ded) to head (2d5f90f).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1122   +/-   ##
=======================================
  Coverage   80.10%   80.10%           
=======================================
  Files         203      203           
  Lines       19529    19529           
=======================================
  Hits        15643    15643           
  Misses       3886     3886           
Flag Coverage Δ
Linux 79.56% <ø> (ø)
Python-3.10 79.91% <ø> (+0.01%) ⬆️
Python-3.11 79.80% <ø> (-0.02%) ⬇️
Python-3.13 79.81% <ø> (+0.01%) ⬆️
Python-3.9 79.88% <ø> (+0.01%) ⬆️
Windows 79.56% <ø> (ø)
macOS 79.53% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants