From cadbc49c5ee77acb20883ff6ca18c7ef452cb297 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Mon, 7 Sep 2026 13:17:41 -0400 Subject: [PATCH] Write Redis cache settings during post-deployment configuration postconfig.py provisioned and RBAC-granted a Redis cache but never wrote the settings the application reads, leaving a placeholder: # Scale > Redis Cache # todo support redis cache configuration The App Service only receives REDIS_ENTRA_TOKEN_SCOPE, and configure_sessions() in app.py reads enable_redis_cache and redis_url from the Cosmos settings document. Those stayed False and empty, so every deployment that enabled Redis silently fell back to filesystem sessions and in-memory caching. postconfig now writes enable_redis_cache, redis_url, redis_auth_type, redis_service_type, redis_port and redis_key when the deployment provisioned a cache. It is skipped when no cache was provisioned so an operator-configured external Redis is preserved. The Bicep redisCacheKind parameter uses managed/classic while the application uses azure_managed_redis/azure_cache_for_redis, so the value is translated rather than passed through. Writing it through unchanged would resolve the wrong TLS port (10000 vs 6380). Adds functional_tests/test_postconfig_redis_cache_configuration.py, which also cross-checks the identifiers against the SERVICE_TYPE constants in functions_redis_client.py so the two vocabularies cannot drift apart. Bumps deployers/version.txt to 1.0.30. --- deployers/bicep/postconfig.py | 15 ++- deployers/version.txt | 2 +- ...st_postconfig_redis_cache_configuration.py | 120 ++++++++++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 functional_tests/test_postconfig_redis_cache_configuration.py diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index 7dd9e30b1..6715c4e26 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -491,7 +491,20 @@ def get_core_service_keys( item["enable_appinsights_global_logging"] = True # Scale > Redis Cache -# todo support redis cache configuration +# Only written when this deployment provisioned a cache, so an operator-configured +# external Redis is not overwritten when deployRedisCache is false. +redis_cache_host_name = (var_redisCacheHostName or "").strip() +if redis_cache_host_name: + item["enable_redis_cache"] = True + item["redis_url"] = redis_cache_host_name + item["redis_auth_type"] = var_redisAuthenticationType + # The application uses different service-type identifiers than the Bicep redisCacheKind parameter. + item["redis_service_type"] = ( + "azure_managed_redis" if var_redisCacheKind == "managed" else "azure_cache_for_redis" + ) + item["redis_port"] = var_redisCachePort + # Empty under managed identity, which also clears a stale key from an earlier key-auth deployment. + item["redis_key"] = core_service_keys.get("redis_key", "") # Workspaces > Metadata Extraction item["enable_extract_meta_data"] = True diff --git a/deployers/version.txt b/deployers/version.txt index 4c24bf133..475bda9cf 100644 --- a/deployers/version.txt +++ b/deployers/version.txt @@ -1 +1 @@ -1.0.29 +1.0.30 diff --git a/functional_tests/test_postconfig_redis_cache_configuration.py b/functional_tests/test_postconfig_redis_cache_configuration.py new file mode 100644 index 000000000..a1d21351c --- /dev/null +++ b/functional_tests/test_postconfig_redis_cache_configuration.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# test_postconfig_redis_cache_configuration.py +""" +Functional test for post-deployment Redis cache configuration. +Version: 0.261.023 +Implemented in: 0.261.023 + +This test ensures the AZD post-deployment configuration script writes the Redis +cache settings that the application reads, so a deployed Azure Managed Redis +instance is actually used instead of silently falling back to filesystem +sessions and in-memory caching. +""" + +from pathlib import Path +import re +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +POSTCONFIG = REPO_ROOT / "deployers" / "bicep" / "postconfig.py" +REDIS_CLIENT = REPO_ROOT / "application" / "single_app" / "functions_redis_client.py" + + +def require_contains(content: str, expected: str, description: str) -> None: + if expected not in content: + raise AssertionError(f"Missing {description}: {expected}") + + +def require_not_contains(content: str, unexpected: str, description: str) -> None: + if unexpected in content: + raise AssertionError(f"Unexpected {description}: {unexpected}") + + +def test_postconfig_writes_redis_cache_settings() -> bool: + print("๐Ÿงช Testing postconfig Redis cache configuration") + print("=" * 70) + + content = POSTCONFIG.read_text(encoding="utf-8") + + require_not_contains(content, "todo support redis cache configuration", "unimplemented Redis configuration") + + for key in ( + "enable_redis_cache", + "redis_url", + "redis_auth_type", + "redis_service_type", + "redis_port", + "redis_key", + ): + require_contains(content, f'item["{key}"]', f"Redis setting assignment for {key}") + + require_contains(content, 'if redis_cache_host_name:', "guard so an operator-configured cache is preserved") + + print("โœ… postconfig writes every Redis setting the application reads") + print("โœ… postconfig only overwrites Redis settings when it provisioned a cache") + return True + + +def test_service_type_identifiers_match_application() -> bool: + """The deployer must emit the identifiers the application accepts. + + The Bicep parameter uses managed/classic while the application uses + azure_managed_redis/azure_cache_for_redis, so a mismatch here would leave the + port and TLS behavior resolving to the wrong Redis offering. + """ + print("\n๐Ÿงช Testing Redis service type identifiers match the application") + print("=" * 70) + + postconfig = POSTCONFIG.read_text(encoding="utf-8") + client = REDIS_CLIENT.read_text(encoding="utf-8") + + supported = dict( + re.findall( + r"^(SERVICE_TYPE_[A-Z_]+)\s*=\s*'([a-z_]+)'", + client, + flags=re.MULTILINE, + ) + ) + if not supported: + raise AssertionError("Could not read SERVICE_TYPE constants from functions_redis_client.py") + + managed = supported.get("SERVICE_TYPE_AZURE_MANAGED_REDIS") + classic = supported.get("SERVICE_TYPE_AZURE_CACHE_FOR_REDIS") + if not managed or not classic: + raise AssertionError(f"Missing expected SERVICE_TYPE constants, found: {sorted(supported)}") + + require_contains(postconfig, f'"{managed}"', "managed Redis service type identifier") + require_contains(postconfig, f'"{classic}"', "classic Redis service type identifier") + + # The Bicep vocabulary must be translated rather than written through unchanged. + assigned = re.search( + r'item\["redis_service_type"\]\s*=\s*\((.*?)\)', + postconfig, + flags=re.DOTALL, + ) + if not assigned: + raise AssertionError("Could not locate the redis_service_type assignment") + if '"managed"' not in assigned.group(1): + raise AssertionError("redis_service_type assignment does not branch on the Bicep redisCacheKind value") + + print(f"โœ… deployer emits '{managed}' and '{classic}'") + print("โœ… deployer translates the Bicep redisCacheKind vocabulary") + return True + + +if __name__ == "__main__": + tests = [ + test_postconfig_writes_redis_cache_settings, + test_service_type_identifiers_match_application, + ] + results = [] + for test in tests: + try: + results.append(test()) + except Exception as exc: + print(f"โŒ {test.__name__} failed: {exc}") + results.append(False) + + print(f"\n๐Ÿ“Š Results: {sum(1 for r in results if r)}/{len(results)} tests passed") + sys.exit(0 if all(results) else 1)