From 19d66a74179aea079aef1a3234f742d0d372f46f Mon Sep 17 00:00:00 2001 From: Motty Chen Date: Sun, 15 Feb 2026 17:11:40 -0600 Subject: [PATCH] fix(infra): manage topics table in infrastructure stacks Define purposepath-topics-{stack} in infrastructure Pulumi and export it so infra owns table lifecycle across environments. Also harden secret resolution fallbacks so renamed configured secrets still resolve via stage-aware and legacy candidates. Co-authored-by: Cursor --- coaching/src/core/config_multitenant.py | 45 ++++++++++++------------- infrastructure/pulumi/__main__.py | 24 +++++++++++++ 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/coaching/src/core/config_multitenant.py b/coaching/src/core/config_multitenant.py index 70cb9e82..469a5157 100644 --- a/coaching/src/core/config_multitenant.py +++ b/coaching/src/core/config_multitenant.py @@ -235,15 +235,17 @@ def get_openai_api_key() -> str | None: return settings.openai_api_key # Retrieve from Secrets Manager. - # Prefer configured value when provided, and keep legacy fallback for compatibility. - # Otherwise, use stage-aware naming and then legacy global fallback. - secret_candidates = ( - [settings.openai_api_key_secret, "purposepath/openai-api-key"] - if settings.openai_api_key_secret - else [ - f"purposepath/{settings.stage}/openai-api-key", - "purposepath/openai-api-key", - ] + # Always try configured name first, then stage-aware default, then legacy global. + # This keeps compatibility when secrets are renamed between conventions. + stage_default = f"purposepath/{settings.stage}/openai-api-key" + secret_candidates = list( + dict.fromkeys( + [ + settings.openai_api_key_secret, + stage_default, + "purposepath/openai-api-key", + ] + ) ) try: from shared.services.aws_helpers import get_secretsmanager_client @@ -282,20 +284,17 @@ def get_google_vertex_credentials() -> dict[str, Any] | None: settings = get_settings() - # Always retrieve from Secrets Manager (ignore GOOGLE_APPLICATION_CREDENTIALS) - # This ensures proper OAuth scopes are applied via service_account.Credentials. - # Prefer configured value when provided, and keep legacy fallback for compatibility. - # Otherwise, use stage-aware naming and then legacy global fallback. - secret_candidates = ( - [ - settings.google_vertex_credentials_secret, - "purposepath/google-vertex-credentials", - ] - if settings.google_vertex_credentials_secret - else [ - f"purposepath/{settings.stage}/google-vertex-credentials", - "purposepath/google-vertex-credentials", - ] + # Always retrieve from Secrets Manager (ignore GOOGLE_APPLICATION_CREDENTIALS). + # Try configured name, then stage-aware default, then legacy global fallback. + stage_default = f"purposepath/{settings.stage}/google-vertex-credentials" + secret_candidates = list( + dict.fromkeys( + [ + settings.google_vertex_credentials_secret, + stage_default, + "purposepath/google-vertex-credentials", + ] + ) ) try: import structlog diff --git a/infrastructure/pulumi/__main__.py b/infrastructure/pulumi/__main__.py index c65cf916..4ae5a99f 100644 --- a/infrastructure/pulumi/__main__.py +++ b/infrastructure/pulumi/__main__.py @@ -82,6 +82,27 @@ tags={**common_tags, "Name": "coaching_sessions", "Purpose": "Session-Tracking"}, ) +# Topic definitions table (master data consumed by coaching service runtime) +topics_table = aws.dynamodb.Table( + "topics", + name=f"purposepath-topics-{stack}", + billing_mode="PAY_PER_REQUEST", + hash_key="topic_id", + attributes=[ + aws.dynamodb.TableAttributeArgs(name="topic_id", type="S"), + aws.dynamodb.TableAttributeArgs(name="topic_type", type="S"), + ], + global_secondary_indexes=[ + aws.dynamodb.TableGlobalSecondaryIndexArgs( + name="type-index", + hash_key="topic_type", + projection_type="ALL", + ), + ], + point_in_time_recovery=aws.dynamodb.TablePointInTimeRecoveryArgs(enabled=True), + tags={**common_tags, "Name": "topics", "Purpose": "Topic-Definitions"}, +) + # S3 Bucket for LLM Prompts prompts_bucket = aws.s3.Bucket( "coaching-prompts-bucket", @@ -153,6 +174,7 @@ { "coachingConversations": conversations_table.name, "coachingSessions": coaching_sessions_table.name, + "topics": topics_table.name, }, ) pulumi.export( @@ -160,6 +182,7 @@ { "conversations": f"purposepath-coaching-conversations-{stack}", "sessions": f"purposepath-coaching-sessions-{stack}", + "topics": f"purposepath-topics-{stack}", }, ) pulumi.export("promptsBucket", prompts_bucket.bucket) @@ -175,5 +198,6 @@ { "conversations": conversations_table.arn, "sessions": coaching_sessions_table.arn, + "topics": topics_table.arn, }, )