feat(core): controller resolution refactor - single-source-of-truth - #896
feat(core): controller resolution refactor - single-source-of-truth#896oboehmer wants to merge 31 commits into
Conversation
Single-resolution design: resolve_controller() in core/controller.py is the sole detection point. PyATSOrchestrator receives ControllerContext as a typed parameter and owns serialization to NAC_TEST_CONTROLLER_CONTEXT before subprocess launch. EnvironmentValidator dissolved. Ref: #856
281549c to
5f20bc8
Compare
Single source of truth for controller detection with typed context. Changes: - Add ControllerContext dataclass to core/types.py with JSON serialization - Move utils/controller.py → core/controller.py with new API: - resolve_controller() returns ControllerContext, raises typed exceptions - get_controller_context() for subprocess access via env var - format_resolution_error() for user-friendly error messages - Move cli/validators/controller_auth.py → core/controller_auth.py - preflight_auth_check() now accepts ControllerContext - Dissolve EnvironmentValidator class (validate_controller_env removed) - Update CombinedOrchestrator to use resolve_controller() + store context - Update PyATSOrchestrator to accept controller_context parameter - Serializes to NAC_TEST_CONTROLLER_CONTEXT env var for subprocess - Wire base_test.py to use get_controller_context() (no more re-detection) - Make detect_controller_type() delegate to resolve_controller() (deprecated) - Add bridge-release shim at utils/controller.py (2 exports only) - Add ControllerContext fixtures (aci_context, sdwan_context) - Move tests/utils/test_controller.py → tests/unit/core/ - Parametrize TestResolveController tests - Add tests for env var population in PyATSOrchestrator Closes #856
5f20bc8 to
1b0b905
Compare
- Remove NAC_TEST_STRICT_CONTEXT (transitional, not needed) - Downgrade fallback warning to INFO log level
…import cleanup.py imported from discovery/test_type_resolver, which triggered the full pyats_core.discovery chain including common/types. This caused a circular import when utils/__init__.py was loaded early. Moving VALID_TEST_TYPES to pyats_core/constants.py breaks the cycle since that module only imports from core/constants and _env - no discovery deps. Note: Opportunistic fix discovered during PR #896 review, not directly related to the controller resolution refactor.
- Move clean_controller_env fixture to tests/conftest.py (global autouse) - Derive CONTROLLER_ENV_PREFIXES from CONTROLLER_REGISTRY (single source of truth) - Clear NAC_TEST_CONTROLLER_CONTEXT and _matched_credential_sets cache - Remove duplicate fixtures from tests/unit/ and tests/pyats_core/ - Remove redundant clean_environment fixture from test_controller.py Fixes test isolation issues when running with pytest-xdist (-n auto).
- Fix format_resolution_error() signature (single arg, not two) - Clarify dry-run semantics: note PyATSOrchestrator fallback behavior - Fix base_test.py status: should be updated in Phase 1, not 'has been' - Remove NAC_TEST_STRICT_CONTEXT references (feature removed)
…raction - Add deprecation docstrings to detect_controller_type() and get_matched_credential_set() - Clarify get_controller_context() docstring for subprocess usage - Extract is_env_var_set() to _env.py as public utility - Add parametrized unit tests for is_env_var_set() - Reorder controller.py: public API → deprecated → private helpers
- Remove sdwan_session_context fixture, unify to sdwan_context with session auth - Move test_controller_auth.py from tests/unit/cli/validators/ to tests/unit/core/ - Update docstring to remove CLI reference
- test_controller.py: Remove 4 happy-path tests covered by parametrized contract tests - test_controller_auth.py: Move TestClassifyAuthError and TestExtractHttpStatusCode to new test_error_classification.py (correct module ownership); remove duplicate URL test - test_orchestrator_config_error.py: Remove explicit-context test (duplicate of test_orchestrator_controller_param.py::test_orchestrator_uses_provided_controller_context)
Keep locally for reference but not needed in PR.
- Wrap get_controller_context() in setup() with try/except and log via self.logger.error before re-raising (ValueError, KeyError, JSONDecodeError) — aligns with every other failure path in base_test.py - Fix clean_controller_env fixture: use key.startswith(prefix) instead of prefix in key to avoid false matches on unrelated env vars (e.g. GCC_FLAGS matching CC_, NOISE_LEVEL matching ISE_) - Add parametrized tests for setup() error logging covering all three exception types (tests/pyats_core/common/test_base_test_controller_detection.py)
Patching logging.getLogger globally interfered with unittest.mock's own internal use of logging, causing the get_controller_context patch to silently fail — the real function ran and raised ValueError instead of the injected KeyError/JSONDecodeError. Scope the patch to nac_test.pyats_core.common.base_test.logging.getLogger so it only intercepts the logger setup inside setup(), not the mock machinery itself.
Patching logging.getLogger (even scoped to the base_test module) causes unittest.mock's own patch machinery to malfunction on Python 3.10 — the get_controller_context patch silently fails, the real function runs, and ValueError is raised instead of the injected KeyError/JSONDecodeError. Replace the logger mock + logging.getLogger patch with pytest's built-in caplog fixture. caplog captures log records at the handler level without touching getLogger, so the get_controller_context patch works correctly.
Patching 'nac_test.pyats_core.common.base_test.get_controller_context' consistently failed on Python 3.10 — the real function executed regardless of the patch, causing ValueError instead of the injected KeyError/JSONDecodeError. Root cause is still under investigation but appears to be a Python 3.10 interaction between pytest-xdist worker isolation and unittest.mock's attribute lookup for the patch target. Replace the patching approach with real env var injection through NAC_TEST_CONTROLLER_CONTEXT, which exercises the actual get_controller_context code paths: - absent env var + no controller creds → ValueError (fallback path) - JSON missing controller_type field → KeyError (from_json path) - malformed JSON → JSONDecodeError (from_json path) This is more robust: no mock machinery involved, tests the real execution path, and works identically across all Python versions.
Add CredentialSet.fields (a Mapping[CredentialKind, str]) as the single
source of truth for which env var backs which semantic credential kind
(url/username/password/token) per controller type and auth method;
env_vars and kinds are now derived properties so they can never drift
out of sync with each other.
Add get_connection_params(controller_type, auth_method) to resolve a
dict of {kind: value} from the environment, and get_insecure_flag
(controller_type, default) to resolve the SSL-verification-disable
flag via a new ControllerConfig.insecure_env_var field. Both raise
KeyError for an unregistered controller_type; get_connection_params
raises ValueError when no credential set matches the given auth_method
or when required env vars are missing, and picks the most-fully-
configured candidate among sets that share an auth_method (e.g.
IOSXE's IOSXE_URL vs IOSXE_HOST variants) so the error points at the
variant the caller actually started configuring.
NACTestBase.setup() now resolves self.auth_method and
self.connection_params via these new functions in addition to the
existing self.username/self.password, giving pyats_core-based test
suites (e.g. nac-test-pyats-common) a generic, non-username/password
credential shape to consume without re-reading os.environ themselves.
The abstract NACTestBase.get_connection_params() stub (previously
NotImplementedError, never implemented by any subclass) is removed in
favor of this instance attribute.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…drop dead code display_auth_failure_banner() no longer takes an env_var_prefix param; it now derives its "export VAR=<kind>" remediation lines directly from CONTROLLER_REGISTRY via a new _credential_remediation_lines() helper, so token-only credential sets (e.g. SDWAN's API token) are represented correctly instead of assuming every controller has USERNAME/PASSWORD vars. combined_orchestrator.py no longer needs to look up an env var prefix to pass in. validate_aci_defaults() reads CONTROLLER_REGISTRY["ACI"].url_env_var instead of a hardcoded ACI_URL_ENV_VAR constant, so an env var rename only touches nac_test/core/controller.py. Remove now-dead code with no remaining callers: - is_architecture_active() (nac_test/cli/validators/common.py, whole file) - superseded by controller-type resolution. - TerminalColors.format_env_var_error() (~94 lines) - the old auto-detection error banner this formatted has been replaced by get_connection_params()'s ValueError messages and the auth-failure banner above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… and API cleanup - Add from_json() validation (wraps JSONDecodeError → ValueError) - Clarify ControllerContext as identity-only (docstring + design intent) - Consolidate URL lookup: controller_auth.py uses core's get_controller_url() - Make base_test.py fail fast on get_connection_params() failure - Add DeprecationWarning to detect_controller_type() / get_matched_credential_set() - Remove detect_controller_type re-export from utils/__init__ - Move NAC_TEST_CONTROLLER_CONTEXT env write from __init__ to subprocess launch - Rename get_insecure_flag() → should_verify_ssl() (polarity flip, clearer API) - Keep URL normalization (rstrip) as adapter concern, not core - Add integration tests for URL trailing-slash normalization contract - Add unit tests for from_json error paths and primary context path - Update PRD_AND_ARCHITECTURE.md with new resolution design Co-Authored-By: Claude <noreply@anthropic.com>
1b75a18 to
0fade2e
Compare
aitestino
left a comment
There was a problem hiding this comment.
Hey @oboehmer, thank you for the PR — the SSOT direction is right, and the internal consolidation has genuinely landed. CONTROLLER_REGISTRY is the single source, CredentialSet.fields deriving env_vars/kinds closes the drift risk between those two, MappingProxyType on the frozen dataclass is the correct immutability pattern, and the ControllerContext-as-identity design (credentials stay in original env vars, only the identity crosses the subprocess boundary) is sound. The deprecation strategy on detect_controller_type()/get_matched_credential_set() is clean — call-time warnings.warn(stacklevel=2), and I verified the warning fires correctly through the utils.controller shim.
I did a thorough review. Most findings are finishing-pass or cross-repo migration items I'd rather track as issues. A handful are worth addressing before merge:
Things that need adjustment before merge:
-
HTML report gives wrong remediation for SDWAN token auth — this is the one real user-facing bug I found. Your
banners.py::_credential_remediation_lines()in this PR correctly derives credential vars fromCONTROLLER_REGISTRY, but the sibling HTML template atpyats_core/reporting/templates/auth_failure/report.html.j2:395-408, 449-454still hardcodes_USERNAME/_PASSWORDfor only ACI/SDWAN/CC. So an SDWAN token-auth user hitting a preflight failure seesexport SDWAN_API_TOKEN=…in the terminal banner butexport SDWAN_USERNAME= / SDWAN_PASSWORD=…in the HTML report — contradictory instructions for the same failure. MERAKI/FMC/ISE/IOSXE users get no guidance from that block at all. The fix is to expose_credential_remediation_lines()publicly and drive the template loops fromCONTROLLER_REGISTRYthe same waybanners.pydoes. -
ResolutionErrordoesn't inherit fromNacTestError— CLAUDE.md convention. One-line change atcore/controller.py:253; the three subclasses get it transitively.nac_test/exceptions.pyhas zero internal imports so there's no circular-import risk. Downstreamexcept NacTestErrorhandlers currently miss these. -
NAC_TEST_CONTROLLER_CONTEXTenv-var name repeated 4 times with no constant —core/controller.py:353,pyats_core/orchestrator.py:281, 369, 384. The precedent already exists:ENV_TEST_DIR: str = "NAC_TEST_TEST_DIR"atpyats_core/constants.py:124, same shape of problem (orchestrator writes, subprocess-side reads by env var name). AddENV_CONTROLLER_CONTEXTtocore/constants.py— it has to live incoresincecore/controller.pyis the reader andcoremustn't import frompyats_core. -
AuthMethodisn't a real enum —auth_method: strinCredentialSet:56,ControllerContext:48, andget_connection_params:534, compared by==at line 573. The only two values that ever flow are"session"and"token". Sincenac-test-pyats-commondoesn't consume these APIs yet (grep confirms zero external callers ofget_connection_params/get_controller_context), this is the cheapest possible moment to lock the enum in. AStrEnumwould catch a typo likeauth_method="Token"in a futureCredentialSetat mypy instead of silently returning an empty candidates list three calls downstream. -
Orphan
clean_controller_envfixtures shadow the new global one —tests/integration/test_cli_aci_validation.py:29-40, 209-223still have class-scoped autouse fixtures with the same name as the new global one you added. Pytest fixture resolution means these local copies override for those classes — and they useprefix in key(substring), miss theIOSXE_prefix, don't clearNAC_TEST_CONTROLLER_CONTEXT, and don't clear_matched_credential_sets. Same consolidation pattern you fixed twice in this PR, just missed in these two spots.
Follow-up issues to track (none block the merge):
I have opened:
- #910 — Wire up
should_verify_ssl()acrossnac-test-pyats-commonauth adapters (currently the function exists but has zero production callers on our side; the four hardcodedos.environ.get("*_INSECURE", "True")...sites in aci/sdwan/catc/api_test_base bypass it). Also captures the caveat that the eventual fix isn't a drop-innot get_bool_env(...)—should_verify_sslcalls.strip()whileget_bool_envdoesn't. Softening the docstring so it doesn't read as "already replaced" would be good in the meantime. - #911 — Delete
nac_test/utils/environment.py(grepped across nac-test AND nac-test-pyats-common:check_required_varsandformat_missing_vars_errorhave zero callers anywhere). - #912 — Move
AuthCachefrompyats_core/common/tocore/(core/controller_auth.pynow imports it, which inverts the layering; AuthCache's own docstring says it's controller-agnostic). - #913 — End-to-end
ControllerContextsubprocess round-trip test — the serialization side (orchestrator.py:281) and the deserialization side (base_test.py::setup()) are each tested in isolation but never chained. Extending the existingtest_orchestrator_env_var.pycapture to feed its own output into a realNACTestBase.setup()proves the round-trip without needing an actual OS subprocess. - #914 — Add
nac_test.utils.controllershim totests/unit/test_compat_shims.py::SHIMS— theDeprecationWarningdoes fire, but there's no test that either re-exported symbol is still resolvable ifcore/controller.pyrenames one in Phase 3. - #915 — Sanitize
controller_urlon display:banners.py:295, 332, 335pass it verbatim to_wrap_url_lines, soACI_URL=https://admin:pw@apic.localleaks credentials to terminal/CI logs/screenshots. AlsoAuthCheckResult.controller_urlno longer trims trailing slashes (the old_get_controller_urldid) — small cosmetic regression, worth locking down. - #916 — Migrate
tests/unit/core/test_controller_auth.pytomockerfixture (from unittest.mock import MagicMock, patchat line 9, 15 usage sites; brand-new file so no legacy excuse). - #917 — Rewrite
TestControllerEdgeCases(13 tests, lines 413-618) to exerciseresolve_controller()— currently only calls the deprecateddetect_controller_type(), so between now and Phase 3 those edge cases are unverified against the production code path. - #918 — Add public
reset_controller_cache()oncore/controller— the newclean_controller_envfixture attests/conftest.py:73reaches intocontroller._matched_credential_sets.clear(). A public reset function keeps future refactors of that private dict from silently breaking test isolation. - #919 — Adopt
aci_context/sdwan_context/cc_contextfixtures at the seven remaining inlineControllerContext(...)construction sites (also collapses four two-line field-by-field assertions into single dataclass equality checks).
PyATSTestDirs + pyats_test_dirs fixture is also duplicated verbatim between tests/pyats_core/conftest.py:18-65 and tests/unit/conftest.py:20-25, 126-139, but you already documented that in-file as tracked for #541 — happy to defer to that.
Design decisions I checked and agree with:
- dataclass over Pydantic for
ControllerContext— right call for a 2-field IPC envelope; adding pydantic just for that would be over-engineering. One caveat worth doing regardless of container type:from_jsoncurrently accepts any string forcontroller_type/auth_method, so a corrupted env var or version-skew subprocess silently constructs an invalidControllerContextthat only fails much later at a bareKeyErrorin the registry. Aget_args(ControllerTypeKey)check with a clearValueErrorat the deserialization boundary is a couple of lines and prevents that. Happy to include this in the "before merge" list if you'd rather — I put it here because you might already be planning it under a different heading. - Env-var handoff over stdin/file/IPC — right choice;
pyats run jobowns stdin, and config files add lifecycle overhead. Consistent with the existingDEVICE_INFO/MERGED_DATA_MODEL_TEST_VARIABLES_FILEPATHpatterns. - Bridge shim — thin, time-boxed, Phase-3 removal documented. Solid.
What do you think?
P.S. — This comment was drafted using voice-to-text via Claude Code. If the tone comes across as overly direct or terse, please know that's just how it tends to phrase things. No offense or criticism is intended — this is purely an objective technical review of the PR. Thanks for understanding! 🙂
ResolutionError and its subclasses (NoCredentialsFound, MultipleControllersFound, IncompleteCredentials) now inherit from NacTestError instead of bare Exception. This ensures downstream 'except NacTestError' handlers catch controller resolution failures consistently. Ref: PR #896 review point 2
Replace 11 raw 'NAC_TEST_CONTROLLER_CONTEXT' string literals with a single named constant, following the ENV_TEST_DIR precedent in pyats_core/constants.py. Constant lives in core/constants.py (not pyats_core/) because core/controller.py is the reader and core must not import from pyats_core. Ref: PR #896 review point 3
Introduce AuthMethod(str, Enum) with SESSION and TOKEN values as the single source of truth for authentication method identifiers. Key changes: - ControllerContext.auth_method typed as AuthMethod (was bare str) - CredentialSet.auth_method typed as AuthMethod with SESSION default - get_connection_params() and _infer_auth_method() use AuthMethod - from_json() validates auth_method via enum constructor — catches typos like 'Token' at deserialization with clear ValueError - Uses str,Enum pattern (not StrEnum) for Python 3.10 mypy compat No breaking change for consumers: AuthMethod values ARE strings (via str inheritance), so existing == comparisons continue to work. JSON wire format unchanged. Ref: PR #896 review point 4
Remove 4 redundant/buggy fixtures: - TestCliAciValidationIntegration.clean_controller_env (substring match, missing IOSXE_, no NAC_TEST_CONTROLLER_CONTEXT clear, no cache clear) - TestCliAciSubprocessIntegration.clean_controller_env (same issues + raw os.environ instead of monkeypatch) - TestOrchestratorUnsupportedPythonExit._clean_env (redundant delegate) - TestCombinedOrchestratorController._clean_env (redundant delegate) The global autouse clean_controller_env in tests/conftest.py already runs for every test function, uses key.startswith(prefix) (correct), includes IOSXE_, clears ENV_CONTROLLER_CONTEXT, and clears the _matched_credential_sets cache. Ref: PR #896 review point 5
Add get_args(ControllerTypeKey) check at from_json() so a corrupted NAC_TEST_CONTROLLER_CONTEXT env var (version skew, partial write, CI artifact from older branch) fails fast with a clear ValueError listing valid values, instead of silently constructing an invalid context that explodes later with an unhelpful KeyError in the registry. Combined with the AuthMethod enum validation (previous commit), both fields are now fully validated at the deserialization boundary. Ref: PR #896 review hardening suggestion
check_required_vars() and format_missing_vars_error() have zero callers anywhere in nac-test or nac-test-pyats-common. utils/__init__.py does not re-export them. Safe to delete. Closes #911
Restores correct layering: core/controller_auth.py now imports from core/auth_cache instead of reaching into pyats_core/common/. Changes: - Move auth_cache.py to nac_test/core/auth_cache.py - Move AUTH_CACHE_DIR constant to core/constants.py - Re-export shim at old location for nac-test-pyats-common main branch compat (removal tracked in Phase 3 #897) - Update test mock paths to new module location Closes #912
Chain orchestrator serialization (to_json → env dict) with subprocess deserialization (get_controller_context → from_json) and verify the resulting ControllerContext is identical to the original. Parametrized across ACI/session, SDWAN/token, CC/session to cover both auth methods and multiple controller types. Closes #913
…es (#919) Replace inline ControllerContext(...) with existing conftest fixtures (aci_context, sdwan_context, cc_context) in 4 test files. Reduces boilerplate and ensures fixture consistency. Left unchanged: test_orchestrator_controller_param.py (uses TOKEN auth, no matching fixture) and test_controller_auth.py (intentional UNKNOWN_CONTROLLER case). Closes #919
Replace unittest.mock (MagicMock, patch context managers) with pytest-mock's mocker fixture. 8 test methods updated, all 14 tests pass. No logic changes — only mocking mechanism. Closes #916
Replace hardcoded ACI/SDWAN/CC credential remediation in the auth failure HTML report with CONTROLLER_REGISTRY-driven content: - Detection section: removed redundant terminal-block that duplicated the error detail (which already lists all controllers with correct alternatives via _format_no_credentials_error). Remediation now references the error detail above. - Bad-credentials section: uses get_credential_vars() to derive credential variable names from the registry, so SDWAN token auth shows SDWAN_API_TOKEN (not USERNAME/PASSWORD). - Grep verification pattern: dynamically derived from registry keys. - Add get_credential_vars() to core/controller.py as the SSOT primitive for credential field iteration. Both banners.py and combined_generator.py now delegate to it. - Remove _build_all_controller_env_examples() (had IOSXE alternative bug and was a DRY violation). Ref: PR #896 review point 1
oboehmer
left a comment
There was a problem hiding this comment.
Thanks for the thorough review! Addressed all 5 before-merge points plus several follow-ups in this push. Summary below.
Before-merge points
1. HTML report gives wrong remediation for SDWAN token auth
Fixed. The auth_failure template now derives credential remediation from CONTROLLER_REGISTRY via a new get_credential_vars() primitive in core/controller.py. The detection section no longer duplicates the error detail (which already shows all controllers with correct alternatives from _format_no_credentials_error()). The bad-credentials section correctly shows SDWAN_API_TOKEN for token auth. The grep verification pattern is also dynamically derived from registry keys. → 3d70583
2. ResolutionError does not inherit from NacTestError
Fixed. One-line change + import. Subclasses get it transitively. → 870afca
3. NAC_TEST_CONTROLLER_CONTEXT repeated with no constant
Fixed. ENV_CONTROLLER_CONTEXT added to core/constants.py, all 11 programmatic uses replaced. Docstrings left as human-readable text. → cd319b8
4. AuthMethod is not a real enum
Fixed. Introduced AuthMethod(str, Enum) with SESSION/TOKEN values. Uses str, Enum (not StrEnum) for Python 3.10 mypy compat. from_json() now validates via enum constructor — catches typos like "Token" at deserialization. No breaking change for consumers: AuthMethod.SESSION == "session" is True. JSON wire format unchanged. → 89cc192
5. Orphan clean_controller_env fixtures
Fixed. Removed 4 fixtures: 2 buggy orphans in test_cli_aci_validation.py (substring match, missing IOSXE_, no cache clear) + 2 redundant _clean_env wrappers in unit tests (global autouse already covers them). → ce80549
Follow-up issues addressed in this PR
| Issue | Status | Commit |
|---|---|---|
#911 — Delete orphaned utils/environment.py |
✅ Closed | 2aea719 |
#912 — Move AuthCache to core/ |
✅ Closed (re-export shim at old path for main-branch compat) | 04935f6 |
| #913 — Round-trip subprocess test | ✅ Closed (3 parametrized cases: ACI/session, SDWAN/token, CC/session) | acf80b1 |
#916 — Migrate test_controller_auth.py to mocker |
✅ Closed | 7c4473c |
| #919 — Adopt context fixtures | ✅ Closed (4 files, 8 inline sites replaced) | 9f2a992 |
Follow-up issues deferred to Phase 3 (#897)
Comment added to #897 with the following items:
- #910 —
should_verify_ssl()wiring (callers are in nac-test-pyats-common, not this repo) - #914 — Shim regression test (shim goes away in Phase 3 anyway)
- #915 — URL credential sanitization (pre-existing condition, not introduced by this PR)
- #917 — Rewrite
TestControllerEdgeCasesto useresolve_controller()(Phase 3 scope) - #918 — Public
reset_controller_cache()API - Auth cache re-export shim removal (
pyats_core/common/auth_cache.py)
Hardening
Added from_json() validation at the deserialization boundary for both fields:
controller_type: validated againstget_args(ControllerTypeKey)— catches unknown types with clearValueErrorauth_method: validated viaAuthMethodenum constructor — catches typos immediately
→ 11b02ca + 89cc192
Design decisions
Agreed with all three assessments. On the from_json validation caveat — implemented as suggested (see hardening above).
Test results
1218 passed, 0 failures (1 pre-existing flaky broker integration test excluded).
Replace string literals with AuthMethod.SESSION/TOKEN in 11 get_connection_params() call sites to satisfy mypy after the AuthMethod enum introduction. Ref: PR #896 point 4 follow-up
Description
Problem
Controller credentials were validated and resolved across multiple layers — CLI validators, orchestrators, PyATS subprocesses — with each layer independently reading
os.environ, re-detecting the controller type, and making its own assumptions about credential shapes. This created:SDWAN_URL,ACI_USERNAME, etc. hardcoded across bothnac-testandnac-test-pyats-commonApproach
This PR establishes
nac_test/core/controller.pyas the single source of truth for all controller metadata and provides a typed resolution flow that eliminates redundant detection.Architecture (both repos)
nac-test (this PR) owns the SSOT registry and resolution primitives.
nac-test-pyats-common (#46) consumes them — auth adapters no longer read env vars directly for controller config.
Key design decisions
ControllerContextis identity, not state — It carries controller type + auth method selection. Credentials remain in env vars (already there, set by the caller). No duplication into another transport mechanism.CredentialSet.fields— AMapping[CredentialKind, str]that is the single source for "which env var backs which semantic kind".env_varsandkindsare derived properties that cannot drift. The order of controller env variables in the definition bears no meaning anymore (previously the controller URL had to be the first value in the list).get_connection_params(controller_type, auth_method)— Generic resolver that returns{"url": "...", "username": "...", ...}keyed by semantic kind. Adapters never need to know env var names.should_verify_ssl(controller_type)— Centralized SSL flag resolution from{PREFIX}_INSECUREenv vars. ReturnsTruewhen SSL should be verified.URL normalization is an adapter concern —
get_controller_url()returns the raw value (whitespace-stripped only). Each architecture-specific adapter ownsrstrip("/")as needed. Integration tests enforce the trailing-slash contract.Benefits
CombinedOrchestratorCONTROLLER_REGISTRY{PREFIX}_INSECUREshould_verify_ssl("ACI")— one callauth_methodCONTROLLER_REGISTRYCloses
Related Issue(s)
Type of Change
Both packages published in lockstep.
Test Framework Affected
Network as Code (NaC) Architecture Affected
Key Changes
Core SSOT primitives (
nac_test/core/)ControllerContextdataclass incore/types.py— identity-only, JSON serializationresolve_controller()→ typedResolutionErrorhierarchy (replacesValueError)get_controller_context()— subprocess accessor (deserializes from env var)get_connection_params(controller_type, auth_method)→dict[CredentialKind, str]should_verify_ssl(controller_type, default=False)→boolCredentialSet.fields: Mapping[CredentialKind, str]replaces the oldenv_varstupleOrchestration wiring
CombinedOrchestratorcallsresolve_controller()once, passes context downstreamPyATSOrchestratoracceptscontroller_contextparameter; serializes at subprocess launch (not__init__)NACTestBase.setup()consumesget_controller_context()+get_connection_params()(fails fast)Cleanup
controller_auth.py: Consolidated URL lookup to useget_controller_url()from coreEnvironmentValidatordissolved (redundant layer)is_architecture_active()andTerminalColors.format_env_var_error()removed (dead code)detect_controller_type()/get_matched_credential_set()deprecated withDeprecationWarningdetect_controller_typeremoved fromutils.__init__re-exportsutils/controller.pyretained for external consumers until Phase 3Deprecation path
detect_controller_type()resolve_controller()get_matched_credential_set()get_controller_context().auth_methodutils.controllershimcore.controllerdirect importsTesting Done
from_json()error paths, primary context path viaNAC_TEST_CONTROLLER_CONTEXT, URL trailing-slash normalization contract (integration)clean_controller_envfixture (registry-derived, xdist-safe)Opportunistic Fix (unrelated to #856)
Circular import:
utils/cleanup.py→discovery/test_type_resolver→ fullpyats_core.discoverychain. MovedVALID_TEST_TYPEStopyats_core/constants.py.Checklist
pre-commit run -apasses)dev-docs/PRD_AND_ARCHITECTURE.md)