diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 5ccde3cd..2bdaff15 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -60,7 +60,7 @@ The GitHub Actions user needs these permissions: - Lambda full access - DynamoDB full access - API Gateway full access -- S3 access for SAM artifacts +- S3 access for deployment artifacts (e.g. Lambda bundles, Pulumi state backups as configured) - Secrets Manager read access ## Deployment Flow diff --git a/.gitignore b/.gitignore index 8cf3b40a..62a253f9 100644 --- a/.gitignore +++ b/.gitignore @@ -406,6 +406,7 @@ security-reports/ vulnerability-reports/ .snyk .artifacts/* +.artifacts-downloaded/ !.artifacts/security/ .artifacts/security/* !.artifacts/security/.gitignore diff --git a/coaching/pyproject.toml b/coaching/pyproject.toml index 4b3a4dda..61c7bd25 100644 --- a/coaching/pyproject.toml +++ b/coaching/pyproject.toml @@ -67,8 +67,8 @@ dependencies = [ [project.optional-dependencies] dev = [ # Testing - "pytest>=7.4.0,<9.0.0", - "pytest-asyncio>=0.21.0,<1.0.0", + "pytest>=9.0.3,<10.0.0", + "pytest-asyncio>=1.3.0,<2.0.0", "pytest-mock>=3.11.1,<4.0.0", "pytest-cov>=4.1.0,<6.0.0", "pytest-xdist>=3.3.1,<4.0.0", @@ -109,8 +109,8 @@ packages = ["src"] [tool.uv] dev-dependencies = [ - "pytest>=7.4.0", - "pytest-asyncio>=0.21.0", + "pytest>=9.0.3", + "pytest-asyncio>=1.3.0", "pytest-mock>=3.11.1", "black>=23.7.0", "ruff>=0.1.0", diff --git a/coaching/requirements-dev.txt b/coaching/requirements-dev.txt index ba78be52..3715f1d1 100644 --- a/coaching/requirements-dev.txt +++ b/coaching/requirements-dev.txt @@ -1,17 +1,17 @@ --r requirements.txt - -# Additional runtime extras -email-validator==2.1.0.post1 -python-multipart==0.0.20 - -# Development tools -pytest==9.0.3 -pytest-asyncio==1.3.0 -pytest-mock==3.14.0 -pytest-cov==6.0.0 -black==23.7.0 -ruff==0.8.4 -pylint==2.17.4 -mypy==1.4.1 -types-PyYAML -types-requests +-r requirements.txt + +# Additional runtime extras +email-validator==2.1.0.post1 +python-multipart==0.0.20 + +# Development tools +pytest==9.0.3 +pytest-asyncio==1.3.0 +pytest-mock==3.14.0 +pytest-cov==6.0.0 +black==23.7.0 +ruff==0.8.4 +pylint==2.17.4 +mypy==1.4.1 +types-PyYAML +types-requests diff --git a/coaching/src/scripts/README.md b/coaching/src/scripts/README.md index 3a8b1b74..b0d422aa 100644 --- a/coaching/src/scripts/README.md +++ b/coaching/src/scripts/README.md @@ -1,105 +1,125 @@ -# Coaching Topics Seed Scripts - -Scripts for seeding coaching topics to the DynamoDB topics table. - -## Overview - -These scripts seed coaching topics into the `purposepath-topics-{env}` DynamoDB table. - -## Scripts - -### `seed_topics.py` -Main seeding script that: -- Creates all coaching topics in DynamoDB from the endpoint registry -- Supports 44 topics total -- Is idempotent (safe to run multiple times) -- Can update existing topics or skip them - -## Usage - -```bash -# Set environment variables -export AWS_PROFILE=purposepath-dev -export AWS_REGION=us-east-1 -export STAGE=dev - -# Run seed script -cd coaching -python -m src.scripts.seed_topics - -# Run with options -python -m src.scripts.seed_topics --force-update # Update existing -python -m src.scripts.seed_topics --dry-run # Preview changes -python -m src.scripts.seed_topics --validate-only # Validate only -``` - -## Prerequisites - -1. **Infrastructure Deployed**: DynamoDB topics table must exist -2. **AWS Credentials**: Configure AWS profile or credentials -3. **Dependencies**: Install Python dependencies -- Old YAML files remain untouched (cleaned up later) - -## Idempotency - -The seed script is **idempotent**: -- ✅ Safe to run multiple times -- ✅ Skips existing topics -- ✅ Does not overwrite data -- ✅ Reports what was created vs skipped - -## Troubleshooting - -### "Topic already exists, skipping" -This is normal if you've run the script before. The script won't overwrite existing topics. - -### "No existing prompts found for migration" -This means no old YAML file exists in S3. Admin must create prompts via the API endpoints. - -### DynamoDB Access Denied -Ensure your AWS credentials have DynamoDB PutItem permission for the table. - -### S3 Access Denied -Ensure your AWS credentials have S3 GetObject and PutObject permissions for the bucket. - -## Verification - -After seeding, verify with: - -```bash -# Check DynamoDB table -aws dynamodb scan \ - --table-name purposepath-llm-prompts-dev \ - --index-name topic_type-index \ - --expression-attribute-values '{":type":{"S":"conversation_coaching"}}' \ - --filter-expression "topic_type = :type" - -# Check S3 prompts -aws s3 ls s3://purposepath-coaching-prompts-dev/prompts/ --recursive - -# Or use verification script -./scripts/verify_topics.sh dev -``` - -## Next Steps - -After seeding: -1. **Verify**: Run verification script -2. **Test**: Test conversation endpoints work with new topics -3. **Create Prompts**: If no prompts were migrated, create them via admin API -4. **Deploy**: Topics are now ready for use - -## Related Issues - -- #78: Infrastructure setup -- #79: Domain models and repository -- #80: Admin API endpoints -- #81: Service layer migration -- #82: This seed script (final epic task) - -## Notes - -- Seed script should be run **once per environment** after infrastructure deployment -- The CoachingTopic enum remains for backward compatibility -- Old YAML prompts can be removed in a future cleanup issue -- Prompts can be updated via the admin API after seeding +# Coaching Topics Seed Scripts + +Scripts for seeding coaching topics to the DynamoDB topics table. + +## Overview + +These scripts seed coaching topics into the `purposepath-topics-{env}` DynamoDB table. + +## Scripts + +### `seed_topics.py` +Main seeding script that: +- Creates all coaching topics in DynamoDB from the endpoint registry +- Seeds every topic defined in the endpoint registry / topic seed data +- Is idempotent (safe to run multiple times) +- Can update existing topics or skip them + +## Usage + +The script imports `coaching.src.*`. Set **`PYTHONPATH` to the repository root** (the parent folder of the `coaching` package), then run the module as `coaching.src.scripts.seed_topics`. + +```bash +# Set environment variables +export AWS_PROFILE=purposepath-dev +export AWS_REGION=us-east-1 +export STAGE=dev + +# Repository root = PurposePath_AI (folder that contains the `coaching` directory) +export PYTHONPATH="$(pwd)" + +# Run seed script (from repo root or from coaching — see below) +cd coaching +uv run python -m coaching.src.scripts.seed_topics + +# Run with options +uv run python -m coaching.src.scripts.seed_topics --force-update # Update existing +uv run python -m coaching.src.scripts.seed_topics --dry-run # Preview changes +uv run python -m coaching.src.scripts.seed_topics --validate-only # Validate only + +# Seed a single topic (example: strategy_alignment_evaluation, issue #296) +uv run python -m coaching.src.scripts.seed_topics --topic-id strategy_alignment_evaluation --force-update +``` + +**PowerShell (Windows), from repository root:** + +```powershell +$env:AWS_PROFILE = "purposepath-dev" +$env:AWS_REGION = "us-east-1" +$env:STAGE = "dev" +$env:PYTHONPATH = "C:\path\to\PurposePath_AI" +cd coaching +uv run python -m coaching.src.scripts.seed_topics --topic-id strategy_alignment_evaluation --force-update +``` + +Legacy path `python -m src.scripts.seed_topics` only works if your environment exposes the same import layout as CI (see `pyproject.toml` `pythonpath` for tests). + +## Prerequisites + +1. **Infrastructure Deployed**: DynamoDB topics table must exist +2. **AWS Credentials**: Configure AWS profile or credentials +3. **Dependencies**: Install Python dependencies (`uv sync` in `coaching/`) + +## Idempotency + +The seed script is **idempotent**: +- ✅ Safe to run multiple times +- ✅ Skips existing topics +- ✅ Does not overwrite data +- ✅ Reports what was created vs skipped + +## Troubleshooting + +### "Topic already exists, skipping" +This is normal if you've run the script before. The script won't overwrite existing topics. + +### "No existing prompts found for migration" +This means no old YAML file exists in S3. Admin must create prompts via the API endpoints. + +### DynamoDB Access Denied +Ensure your AWS credentials have DynamoDB PutItem permission for the table. + +### S3 Access Denied +Ensure your AWS credentials have S3 GetObject and PutObject permissions for the bucket. + +## Verification + +After seeding, verify with: + +```bash +# Check DynamoDB table +aws dynamodb scan \ + --table-name purposepath-llm-prompts-dev \ + --index-name topic_type-index \ + --expression-attribute-values '{":type":{"S":"conversation_coaching"}}' \ + --filter-expression "topic_type = :type" + +# Check S3 prompts +aws s3 ls s3://purposepath-coaching-prompts-dev/prompts/ --recursive + +# Or use verification script +./scripts/verify_topics.sh dev +``` + +## Next Steps + +After seeding: +1. **Verify**: Run verification script +2. **Test**: Test conversation endpoints work with new topics +3. **Create Prompts**: If no prompts were migrated, create them via admin API +4. **Deploy**: Topics are now ready for use + +## Related Issues + +- #78: Infrastructure setup +- #79: Domain models and repository +- #80: Admin API endpoints +- #81: Service layer migration +- #82: This seed script (final epic task) + +## Notes + +- Seed script should be run **once per environment** after infrastructure deployment +- The CoachingTopic enum remains for backward compatibility +- Old YAML prompts can be removed in a future cleanup issue +- Prompts can be updated via the admin API after seeding diff --git a/coaching/src/scripts/seed_topics.py b/coaching/src/scripts/seed_topics.py index 14ae04c1..6d54a2a8 100644 --- a/coaching/src/scripts/seed_topics.py +++ b/coaching/src/scripts/seed_topics.py @@ -15,6 +15,7 @@ --deactivate-orphans Deactivate topics that no longer have endpoints Examples: + # Set PYTHONPATH to the repository root (parent of the `coaching` package), then: # Seed all new topics (skip existing) python -m coaching.src.scripts.seed_topics @@ -32,6 +33,8 @@ # Deactivate orphaned topics python -m coaching.src.scripts.seed_topics --deactivate-orphans + + See coaching/src/scripts/README.md for PowerShell / uv invocations. """ import argparse diff --git a/coaching/tests/integration/test_unified_ai_engine.py b/coaching/tests/integration/test_unified_ai_engine.py index 09a740a2..d35eadee 100644 --- a/coaching/tests/integration/test_unified_ai_engine.py +++ b/coaching/tests/integration/test_unified_ai_engine.py @@ -117,7 +117,8 @@ def test_all_topics_have_required_fields(self): assert topic_data.topic_name assert topic_data.topic_type in ["single_shot", "conversation_coaching"] assert topic_data.category - assert topic_data.model_code + assert topic_data.basic_model_code + assert topic_data.premium_model_code assert 0.0 <= topic_data.temperature <= 2.0 assert topic_data.max_tokens > 0 diff --git a/deployment/README.md b/deployment/README.md index d78290dc..ff77e9d1 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -1,202 +1,10 @@ -# PurposePath Deployment Architecture - -## Overview - -This deployment architecture solves the "update nightmare" by separating shared infrastructure from service-specific resources. The architecture consists of: - -1. **Shared Infrastructure** - One-time deployment of common resources -2. **Service Templates** - Lightweight Lambda-only templates that reference shared resources -3. **Unified Deployment Scripts** - Automated deployment with dependency management - -## Architecture Benefits - -✅ **Eliminates Update Nightmares** - Shared resources are deployed once and rarely changed -✅ **Preserves Existing Infrastructure** - Custom domains, certificates, and databases remain intact -✅ **Enables Runtime Switching** - Deploy Python or .NET to same infrastructure seamlessly -✅ **Simplifies Maintenance** - Service deployments are fast and focused -✅ **Ensures Consistency** - All services use same VPC, databases, and configuration - -## Directory Structure - -``` -deployment/ -├── shared-infrastructure/ -│ ├── template.yaml # All shared AWS resources -│ └── deploy-shared-infrastructure.ps1 # One-time deployment script -└── account-service/ - └── template-dotnet.yaml # .NET Lambda template (legacy) - -# Current streamlined structure: -account/ -└── template.yaml # Streamlined .NET Lambda template -deploy-account.ps1 # Single deployment script -``` - -## Shared Infrastructure Components - -The `shared-infrastructure/template.yaml` includes: - -### Networking & Security -- **VPC** with public/private subnets -- **NAT Gateway** for Lambda internet access -- **Security Groups** for Lambda and Redis -- **VPC Endpoints** for DynamoDB and S3 (cost optimization) - -### Storage & Caching -- **DynamoDB Tables** (Users, Tokens, Tenants, Sessions, etc.) -- **ElastiCache Redis** cluster for session management -- **S3 Buckets** for application data and logs - -### Authentication & Secrets -- **JWT Secret** in AWS Secrets Manager (auto-generated or imported) -- **IAM Policies** and permissions for services - -### Custom Domain & SSL -- **Route53 DNS** records -- **SSL Certificates** via AWS Certificate Manager -- **API Gateway Custom Domain** with proper routing - -### Monitoring & Observability -- **CloudWatch Dashboard** with key metrics -- **CloudWatch Alarms** for error monitoring - -## Service Templates - -Both `template-dotnet.yaml` and `template-python.yaml` are lightweight and include only: - -- **Lambda Function** with runtime-specific configuration -- **API Gateway** for the service -- **API Routes** and event mappings -- **Custom Domain Mapping** to shared domain -- **IAM Policies** referencing shared resources via ImportValue - -## Deployment Process - -### 1. Deploy Shared Infrastructure (One-time) - -```powershell -cd deployment/shared-infrastructure -.\deploy-shared-infrastructure.ps1 -Stage dev -HostedZoneId Z123456789ABCDEF -``` - -**Parameters:** -- `Stage` - dev, staging, or prod -- `HostedZoneId` - Route53 zone for custom domain (optional) -- `JwtSecretArn` - Existing JWT secret (optional, will create if not provided) -- `RedisNodeType` - ElastiCache instance type -- `EmailFrom` - SES sender email - -### 2. Deploy Account Service - -```powershell -.\deploy-account.ps1 -Stage dev -``` - -**Parameters:** -- `Stage` - deployment stage (default: dev) - -**Note**: Current implementation deploys .NET Lambda using existing shared DynamoDB tables from `purposepath-api-dev` stack. - -## Key Features - -### Automatic Dependency Management -The service deployment script automatically: -- ✅ Checks for shared infrastructure -- ✅ Offers to deploy it if missing -- ✅ Validates all required exports exist -- ✅ Builds and tests the service -- ✅ Deploys with proper configuration - -### Resource Import/Export Pattern -Shared resources are exported with consistent naming: -```yaml -# Shared template exports -Outputs: - UsersTableName: - Value: !Ref UsersTable - Export: - Name: !Sub purposepath-users-table-${Stage} -``` - -Service templates import via `Fn::ImportValue`: -```yaml -# Service template imports -Environment: - Variables: - USERS_TABLE: - Fn::ImportValue: !Sub purposepath-users-table-${Stage} -``` - -### Custom Domain Configuration -The architecture uses existing custom domain: -- **Domain:** `api.dev.purposepath.app` -- **SSL Certificate:** Managed by ACM (existing) -- **API Mappings:** - - `/account/*` → .NET Account Service - - `/coaching/*` → Python Coaching Service - - `/traction/*` → Python Traction Service -- **Shared Infrastructure:** Uses existing DynamoDB tables and JWT secrets - -## Migration from Existing Infrastructure - -To migrate from existing deployments: - -1. **Deploy Shared Infrastructure** (preserves existing resources) -2. **Deploy New Service** using either runtime -3. **Update Domain Mapping** to point to new API Gateway -4. **Verify Endpoints** work correctly -5. **Delete Old Stack** once satisfied - -## Current Deployment Status - -**Active Services:** -- ✅ .NET Account Service: `purposepath-account-dotnet-dev` -- ✅ Python Coaching Service: `purposepath-coaching-api-dev` -- ✅ Python Traction Service: `purposepath-traction-api-dev` -- ✅ Shared Infrastructure: `purposepath-api-dev` (DynamoDB tables, JWT secrets) -- ✅ Custom Domain: `purposepath-api-domain-dev` - -**Deployment Command:** -```powershell -.\deploy-account.ps1 -Stage dev -``` - -## Monitoring & Troubleshooting - -### CloudWatch Resources -- **Dashboard:** `purposepath-dashboard-dev` -- **Log Groups:** `/aws/lambda/purposepath-account-api-dev` -- **Metrics:** Lambda errors, duration, throttles -- **Alarms:** Automatic notifications on issues - -### Troubleshooting Commands -```powershell -# Check shared infrastructure status -aws cloudformation describe-stacks --stack-name purposepath-shared-infrastructure-dev - -# List available exports -aws cloudformation list-exports | grep purposepath - -# Check service deployment -aws cloudformation describe-stacks --stack-name purposepath-account-api-dev - -# View recent logs -aws logs tail /aws/lambda/purposepath-account-api-dev --follow -``` - -## Security Considerations - -- **VPC Isolation** - Lambdas run in private subnets -- **Secrets Management** - JWT secrets stored in AWS Secrets Manager -- **IAM Least Privilege** - Minimal permissions for each service -- **Encryption** - Redis and S3 encryption enabled in production -- **Network Security** - Security groups restrict access - -## Cost Optimization - -- **VPC Endpoints** - Reduce NAT Gateway usage for AWS services -- **On-Demand DynamoDB** - Pay per request pricing -- **Shared Resources** - Multiple services share VPC, Redis, etc. -- **Efficient Lambda** - Right-sized memory and timeout settings - -This architecture provides a robust, scalable, and maintainable deployment solution that eliminates infrastructure drift while preserving existing resources and enabling seamless runtime migrations. \ No newline at end of file +# Deployment + +AWS infrastructure and application deployment use **Pulumi**, not AWS SAM or CloudFormation templates in this repository. + +- Coaching Lambda/API: `coaching/pulumi/` +- Broader platform infra: `infrastructure/pulumi/` (and related docs) + +Legacy SAM templates and `sam deploy` helper scripts previously under `deployment/account-service/` and `deployment/shared-infrastructure/` have been removed as unused. + +See `PULUMI_DEPLOYMENT.md` at the repository root for the current workflow. diff --git a/deployment/account-service/deploy-account.ps1 b/deployment/account-service/deploy-account.ps1 deleted file mode 100644 index 430c7325..00000000 --- a/deployment/account-service/deploy-account.ps1 +++ /dev/null @@ -1,463 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS -Deploy PurposePath Account Service - -.DESCRIPTION -Deploys the PurposePath Account Service Lambda function using either Python or .NET runtime. -The deployment uses runtime-specific templates that reference shared infrastructure. -Automatically checks for and optionally deploys shared infrastructure if not present. - -.PARAMETER Runtime -The runtime to deploy (python or dotnet) - -.PARAMETER Stage -The deployment stage (dev, staging, prod) - -.PARAMETER SkipTests -Skip running tests before deployment - -.PARAMETER SkipBuild -Skip building the application before deployment - -.PARAMETER SkipSharedInfraCheck -Skip checking for shared infrastructure (assume it exists) - -.EXAMPLE -.\deploy-account.ps1 -Runtime dotnet -Stage dev - -.EXAMPLE -.\deploy-account.ps1 -Runtime python -Stage dev -SkipTests -#> - -param( - [Parameter(Mandatory = $true)] - [ValidateSet("python", "dotnet")] - [string]$Runtime, - - [Parameter(Mandatory = $false)] - [ValidateSet("dev", "staging", "prod")] - [string]$Stage = "dev", - - [switch]$SkipTests, - [switch]$SkipBuild, - [switch]$SkipSharedInfraCheck, - [switch]$SkipConfirmation -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -# Colors for output -$script:Green = "`e[32m" -$script:Yellow = "`e[33m" -$script:Red = "`e[31m" -$script:Blue = "`e[34m" -$script:Reset = "`e[0m" - -function Write-Info { - param([string]$Message) - Write-Host "${Blue}[INFO]${Reset} $Message" -} - -function Write-Success { - param([string]$Message) - Write-Host "${Green}[SUCCESS]${Reset} $Message" -} - -function Write-Warning { - param([string]$Message) - Write-Host "${Yellow}[WARNING]${Reset} $Message" -} - -function Write-Error { - param([string]$Message) - Write-Host "${Red}[ERROR]${Reset} $Message" -} - -function Test-Prerequisites { - # Test AWS credentials - try { - $identity = aws sts get-caller-identity --output json | ConvertFrom-Json - Write-Info "AWS Identity: $($identity.Arn)" - } - catch { - Write-Error "AWS credentials not configured or invalid" - return $false - } - - # Test SAM CLI - try { - $samVersion = sam --version - Write-Info "SAM CLI Version: $samVersion" - } - catch { - Write-Error "AWS SAM CLI not installed or not in PATH" - return $false - } - - return $true -} - -function Test-SharedInfrastructure { - param([string]$Stage) - - $stackName = "purposepath-shared-infrastructure-$Stage" - - try { - $stack = aws cloudformation describe-stacks --stack-name $stackName --output json 2>$null | ConvertFrom-Json - $status = $stack.Stacks[0].StackStatus - - if ($status -eq "CREATE_COMPLETE" -or $status -eq "UPDATE_COMPLETE") { - Write-Success "Shared infrastructure stack '$stackName' is available" - - # Verify key exports exist - $exports = aws cloudformation list-exports --output json | ConvertFrom-Json - $requiredExports = @( - "purposepath-users-table-$Stage", - "purposepath-jwt-secret-arn-$Stage", - "purposepath-domain-name-$Stage" - ) - - $missingExports = @() - foreach ($exportName in $requiredExports) { - $found = $exports.Exports | Where-Object { $_.Name -eq $exportName } - if (-not $found) { - $missingExports += $exportName - } - } - - if ($missingExports.Count -gt 0) { - Write-Warning "Missing required exports: $($missingExports -join ', ')" - return $false - } - - return $true - } else { - Write-Warning "Shared infrastructure stack '$stackName' exists but status is: $status" - return $false - } - } - catch { - Write-Warning "Shared infrastructure stack '$stackName' not found" - return $false - } -} - -function Deploy-SharedInfrastructureIfNeeded { - param([string]$Stage) - - if (-not (Test-SharedInfrastructure -Stage $Stage)) { - Write-Info "Shared infrastructure not ready for stage '$Stage'" - - if (-not $SkipConfirmation) { - $deployShared = Read-Host "Would you like to deploy shared infrastructure now? (y/N)" - if ($deployShared -ne 'y' -and $deployShared -ne 'Y') { - Write-Error "Shared infrastructure is required for service deployment" - Write-Info "Run the following command first:" - Write-Info " cd ..\shared-infrastructure" - Write-Info " .\deploy-shared-infrastructure.ps1 -Stage $Stage -HostedZoneId " - return $false - } - } - - Write-Info "Deploying shared infrastructure..." - $sharedInfraPath = Join-Path $PSScriptRoot "..\shared-infrastructure" - - if (-not (Test-Path $sharedInfraPath)) { - Write-Error "Shared infrastructure directory not found: $sharedInfraPath" - Write-Info "Expected path: $((Resolve-Path $sharedInfraPath -ErrorAction SilentlyContinue))" - return $false - } - - Push-Location $sharedInfraPath - try { - # Deploy with minimal configuration for service deployment - # Note: This may fail without HostedZoneId for custom domain - .\deploy-shared-infrastructure.ps1 -Stage $Stage -SkipConfirmation - - if ($LASTEXITCODE -ne 0) { - throw "Shared infrastructure deployment failed" - } - } - finally { - Pop-Location - } - } - - return $true -} - -function Build-DotNetService { - Write-Info "Building .NET Account Service..." - - $servicePath = "..\..\pp_api\Services\PurposePath.Account.Lambda" - - if (-not (Test-Path $servicePath)) { - Write-Error ".NET service path not found: $servicePath" - Write-Info "Expected path: $((Resolve-Path $servicePath -ErrorAction SilentlyContinue))" - return $false - } - - Push-Location $servicePath - try { - Write-Info "Restoring NuGet packages..." - dotnet restore - - if ($LASTEXITCODE -ne 0) { - throw "Package restore failed" - } - - Write-Info "Building project..." - dotnet build --configuration Release --no-restore - - if ($LASTEXITCODE -ne 0) { - throw "Build failed" - } - - Write-Success ".NET build completed successfully" - return $true - } - finally { - Pop-Location - } -} - -function Test-DotNetService { - Write-Info "Running .NET tests..." - - $testPath = "..\..\pp_api" - - if (-not (Test-Path $testPath)) { - Write-Warning ".NET project path not found: $testPath - skipping tests" - return $true - } - - Push-Location $testPath - try { - dotnet test --configuration Release --no-build --verbosity normal - - if ($LASTEXITCODE -ne 0) { - throw "Tests failed" - } - - Write-Success ".NET tests passed" - return $true - } - finally { - Pop-Location - } -} - -function Test-PythonService { - Write-Info "Running Python tests..." - - $pythonPath = "..\..\pp_ai_submodule\account" - - if (-not (Test-Path $pythonPath)) { - Write-Warning "Python service path not found: $pythonPath - skipping tests" - return $true - } - - Push-Location $pythonPath - try { - # Check if pytest is available - python -m pytest --version 2>$null - if ($LASTEXITCODE -eq 0) { - python -m pytest tests/ -v - - if ($LASTEXITCODE -ne 0) { - throw "Python tests failed" - } - - Write-Success "Python tests passed" - } else { - Write-Warning "pytest not available - skipping Python tests" - } - - return $true - } - finally { - Pop-Location - } -} - -function Deploy-AccountService { - param( - [string]$Runtime, - [string]$Stage - ) - - $templateFile = "template-$Runtime.yaml" - $stackName = "purposepath-account-api-$Stage" - - Write-Info "Deploying Account Service ($Runtime runtime)..." - Write-Info "Template: $templateFile" - Write-Info "Stack: $stackName" - Write-Info "Stage: $Stage" - - # Validate template - Write-Info "Validating SAM template..." - sam validate --template-file $templateFile - - if ($LASTEXITCODE -ne 0) { - throw "Template validation failed" - } - - Write-Success "Template validation passed" - - # Build parameters - $parameters = @( - "Stage=$Stage" - ) - - if ($Runtime -eq "dotnet") { - $parameters += "LogLevel=Information" - } else { - $parameters += "LogLevel=INFO" - } - - $parameterOverrides = $parameters -join " " - - # Deploy - Write-Info "Deploying with parameters: $parameterOverrides" - - sam deploy ` - --template-file $templateFile ` - --stack-name $stackName ` - --capabilities CAPABILITY_IAM ` - --parameter-overrides $parameterOverrides ` - --no-fail-on-empty-changeset ` - --no-confirm-changeset - - if ($LASTEXITCODE -ne 0) { - throw "Deployment failed" - } - - Write-Success "Account Service deployment completed successfully!" - - # Display outputs - Write-Info "Retrieving stack outputs..." - try { - $outputs = aws cloudformation describe-stacks --stack-name $stackName --query "Stacks[0].Outputs" --output table - Write-Info "Stack Outputs:" - Write-Host $outputs - - # Show API endpoint - $endpoint = aws cloudformation describe-stacks --stack-name $stackName --query "Stacks[0].Outputs[?OutputKey=='AccountHttpApiEndpoint'].OutputValue" --output text - if ($endpoint) { - Write-Success "API Endpoint: $endpoint" - } - - # Show custom domain if available - try { - $domainName = aws cloudformation list-exports --query "Exports[?Name=='purposepath-domain-name-$Stage'].Value" --output text - if ($domainName -and $domainName -ne "None") { - Write-Success "Custom Domain: https://$domainName/account" - } - } catch { - # Ignore errors getting domain name - } - - } catch { - Write-Warning "Could not retrieve stack outputs" - } -} - -# Main script execution -try { - Write-Info "PurposePath Account Service Deployment" - Write-Info "======================================" - Write-Info "Runtime: $Runtime" - Write-Info "Stage: $Stage" - - # Change to script directory - $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path - Set-Location $scriptDir - Write-Info "Working directory: $((Get-Location).Path)" - - # Validate prerequisites - if (-not (Test-Prerequisites)) { - exit 1 - } - - # Ensure shared infrastructure is deployed (unless skipped) - if (-not $SkipSharedInfraCheck) { - if (-not (Deploy-SharedInfrastructureIfNeeded -Stage $Stage)) { - exit 1 - } - } else { - Write-Warning "Skipping shared infrastructure check - assuming it exists" - } - - # Build if needed - if (-not $SkipBuild) { - if ($Runtime -eq "dotnet") { - if (-not (Build-DotNetService)) { - exit 1 - } - } else { - Write-Info "Python service - no build step required" - } - } else { - Write-Warning "Skipping build step" - } - - # Run tests if needed - if (-not $SkipTests) { - if ($Runtime -eq "dotnet") { - if (-not (Test-DotNetService)) { - exit 1 - } - } else { - if (-not (Test-PythonService)) { - exit 1 - } - } - } else { - Write-Warning "Skipping tests" - } - - # Validate template exists - $templateFile = "template-$Runtime.yaml" - if (-not (Test-Path $templateFile)) { - Write-Error "Template file not found: $templateFile" - Write-Info "Available files: $((Get-ChildItem -Name '*.yaml').Name -join ', ')" - exit 1 - } - - # Final confirmation - if (-not $SkipConfirmation) { - Write-Warning "This will deploy the Account Service ($Runtime) to stage '$Stage'" - Write-Info "This will replace any existing Account Service deployment in this stage" - $confirmation = Read-Host "Are you sure you want to continue? (y/N)" - if ($confirmation -ne 'y' -and $confirmation -ne 'Y') { - Write-Info "Deployment cancelled by user" - exit 0 - } - } - - # Deploy the service - Deploy-AccountService -Runtime $Runtime -Stage $Stage - - Write-Success "Account Service ($Runtime) deployment completed successfully!" - - Write-Info "" - Write-Info "Next steps:" - Write-Info "- Test the API endpoints" - Write-Info "- Update custom domain mapping if needed" - Write-Info "- Monitor CloudWatch logs for any issues" - -} catch { - Write-Error "Deployment failed: $($_.Exception.Message)" - - # Show some troubleshooting info - Write-Info "" - Write-Info "Troubleshooting:" - Write-Info "- Check AWS credentials and permissions" - Write-Info "- Verify shared infrastructure is deployed" - Write-Info "- Check SAM CLI logs for detailed error information" - Write-Info "- Ensure all required resources are available in the target region" - - exit 1 -} \ No newline at end of file diff --git a/deployment/account-service/template-dotnet-fixed.yaml b/deployment/account-service/template-dotnet-fixed.yaml deleted file mode 100644 index bf88edd8..00000000 --- a/deployment/account-service/template-dotnet-fixed.yaml +++ /dev/null @@ -1,217 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: 'PurposePath Account Service - .NET Lambda' - -Parameters: - Stage: - Type: String - Default: dev - LogLevel: - Type: String - Default: Information - -Globals: - Function: - Runtime: dotnet8 - Timeout: 30 - MemorySize: 512 - Tracing: Active - Environment: - Variables: - STAGE: !Ref Stage - LOG_LEVEL: !Ref LogLevel - -Resources: - AccountHttpApi: - Type: AWS::Serverless::HttpApi - Properties: - Name: !Sub purposepath-account-api-dotnet-${Stage} - CorsConfiguration: - AllowOrigins: ['*'] - AllowMethods: [GET, POST, PUT, DELETE, OPTIONS] - AllowHeaders: ['*'] - - AccountApiFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Sub purposepath-account-api-dotnet-${Stage} - Handler: PurposePath.Account.Lambda::PurposePath.Account.Lambda.LambdaEntryPoint::FunctionHandlerAsync - CodeUri: ../../pp_api/Services/PurposePath.Account.Lambda/ - Environment: - Variables: - # Import existing shared resources - JWT_SECRET_ARN: - Fn::ImportValue: !Sub purposepath-jwt-secret-arn-${Stage} - REDIS_ENDPOINT: - Fn::ImportValue: !Sub purposepath-redis-endpoint-${Stage} - - # Use existing tables - USERS_TABLE: - Fn::ImportValue: !Sub purposepath-users-table-${Stage} - TENANTS_TABLE: - Fn::ImportValue: !Sub purposepath-tenants-table-${Stage} - SUBSCRIPTIONS_TABLE: - Fn::ImportValue: !Sub purposepath-subscriptions-table-${Stage} - - # Create only missing tables - REFRESH_TOKENS_TABLE: !Ref RefreshTokensTable - PASSWORD_RESETS_TABLE: !Ref PasswordResetsTable - VERIFICATION_TOKENS_TABLE: !Ref VerificationTokensTable - USER_PREFERENCES_TABLE: !Ref UserPreferencesTable - TIERS_TABLE: !Ref TiersTable - TENANT_OVERRIDES_TABLE: !Ref TenantOverridesTable - - API_PREFIX: /api/v1 - EMAIL_FROM: noreply@purposepath.ai - Policies: - - Statement: - - Effect: Allow - Action: [dynamodb:*] - Resource: - - !GetAtt RefreshTokensTable.Arn - - !GetAtt PasswordResetsTable.Arn - - !GetAtt VerificationTokensTable.Arn - - !GetAtt UserPreferencesTable.Arn - - !GetAtt TiersTable.Arn - - !GetAtt TenantOverridesTable.Arn - - !Sub "${RefreshTokensTable.Arn}/index/*" - - !Sub "${PasswordResetsTable.Arn}/index/*" - - !Sub "${VerificationTokensTable.Arn}/index/*" - - !Sub "${UserPreferencesTable.Arn}/index/*" - - !Sub "${TiersTable.Arn}/index/*" - - !Sub "${TenantOverridesTable.Arn}/index/*" - - Effect: Allow - Action: [secretsmanager:GetSecretValue] - Resource: - Fn::ImportValue: !Sub purposepath-jwt-secret-arn-${Stage} - - Effect: Allow - Action: [ses:SendEmail, ses:SendRawEmail] - Resource: '*' - Events: - ApiProxy: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/{proxy+} - Method: ANY - - # Only create missing tables - RefreshTokensTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-refresh-tokens-${Stage} - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: jti - AttributeType: S - - AttributeName: user_id - AttributeType: S - KeySchema: - - AttributeName: jti - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: user-index - KeySchema: - - AttributeName: user_id - KeyType: HASH - Projection: - ProjectionType: ALL - TimeToLiveSpecification: - AttributeName: ttl - Enabled: true - - PasswordResetsTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-password-resets-${Stage} - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: token_hash - AttributeType: S - - AttributeName: user_id - AttributeType: S - KeySchema: - - AttributeName: token_hash - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: user-index - KeySchema: - - AttributeName: user_id - KeyType: HASH - Projection: - ProjectionType: ALL - TimeToLiveSpecification: - AttributeName: ttl - Enabled: true - - VerificationTokensTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-verification-tokens-${Stage} - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: token_hash - AttributeType: S - - AttributeName: user_id - AttributeType: S - KeySchema: - - AttributeName: token_hash - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: user-index - KeySchema: - - AttributeName: user_id - KeyType: HASH - Projection: - ProjectionType: ALL - TimeToLiveSpecification: - AttributeName: ttl - Enabled: true - - UserPreferencesTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-user-preferences-${Stage} - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: user_id - AttributeType: S - KeySchema: - - AttributeName: user_id - KeyType: HASH - - TiersTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-tiers-${Stage}-v2 - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: tier_id - AttributeType: S - KeySchema: - - AttributeName: tier_id - KeyType: HASH - - TenantOverridesTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-tenant-overrides-${Stage}-v2 - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: tenant_id - AttributeType: S - - AttributeName: feature - AttributeType: S - KeySchema: - - AttributeName: tenant_id - KeyType: HASH - - AttributeName: feature - KeyType: RANGE - -Outputs: - AccountHttpApiId: - Value: !Ref AccountHttpApi - AccountHttpApiEndpoint: - Value: !Sub "https://${AccountHttpApi}.execute-api.${AWS::Region}.amazonaws.com" - AccountFunctionArn: - Value: !GetAtt AccountApiFunction.Arn \ No newline at end of file diff --git a/deployment/account-service/template-dotnet-lambda-only.yaml b/deployment/account-service/template-dotnet-lambda-only.yaml deleted file mode 100644 index 7692d53b..00000000 --- a/deployment/account-service/template-dotnet-lambda-only.yaml +++ /dev/null @@ -1,103 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: 'PurposePath .NET Account Lambda - Uses Existing Shared Resources' - -Parameters: - Stage: - Type: String - Default: dev - JwtSecretArn: - Type: String - Description: ARN of existing JWT Secret - -Globals: - Function: - Runtime: dotnet8 - Timeout: 30 - MemorySize: 512 - Tracing: Active - -Resources: - # New API Gateway for .NET service - AccountHttpApi: - Type: AWS::Serverless::HttpApi - Properties: - Name: !Sub purposepath-account-dotnet-api-${Stage} - CorsConfiguration: - AllowOrigins: ['*'] - AllowMethods: [GET, POST, PUT, DELETE, OPTIONS] - AllowHeaders: ['*'] - - # .NET Lambda Function - AccountApiFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Sub purposepath-account-dotnet-api-${Stage} - Handler: PurposePath.Account.Lambda::PurposePath.Account.Lambda.LambdaEntryPoint::FunctionHandlerAsync - CodeUri: ../../pp_api/Services/PurposePath.Account.Lambda/ - Environment: - Variables: - # Use existing shared resources - JWT_SECRET_ARN: !Ref JwtSecretArn - USERS_TABLE: - Fn::ImportValue: !Sub purposepath-users-table-${Stage} - TENANTS_TABLE: - Fn::ImportValue: !Sub purposepath-tenants-table-${Stage} - SUBSCRIPTIONS_TABLE: - Fn::ImportValue: !Sub purposepath-subscriptions-table-${Stage} - REFRESH_TOKENS_TABLE: - Fn::ImportValue: !Sub purposepath-refresh-tokens-table-${Stage} - PASSWORD_RESETS_TABLE: - Fn::ImportValue: !Sub purposepath-password-resets-table-${Stage} - VERIFICATION_TOKENS_TABLE: - Fn::ImportValue: !Sub purposepath-verification-tokens-table-${Stage} - USER_PREFERENCES_TABLE: - Fn::ImportValue: !Sub purposepath-user-preferences-table-${Stage} - - API_PREFIX: /api/v1 - EMAIL_FROM: noreply@purposepath.ai - Policies: - - Statement: - - Effect: Allow - Action: [dynamodb:*] - Resource: - - Fn::ImportValue: !Sub purposepath-users-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-tenants-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-subscriptions-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-refresh-tokens-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-password-resets-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-verification-tokens-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-user-preferences-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-users-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-tenants-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-subscriptions-table-arn-${Stage} - - Effect: Allow - Action: [secretsmanager:GetSecretValue] - Resource: !Ref JwtSecretArn - - Effect: Allow - Action: [ses:SendEmail, ses:SendRawEmail] - Resource: '*' - Events: - ApiProxy: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/{proxy+} - Method: ANY - -Outputs: - AccountHttpApiId: - Value: !Ref AccountHttpApi - AccountHttpApiEndpoint: - Value: !Sub "https://${AccountHttpApi}.execute-api.${AWS::Region}.amazonaws.com" - AccountFunctionArn: - Value: !GetAtt AccountApiFunction.Arn \ No newline at end of file diff --git a/deployment/account-service/template-dotnet.yaml b/deployment/account-service/template-dotnet.yaml deleted file mode 100644 index 5e848c83..00000000 --- a/deployment/account-service/template-dotnet.yaml +++ /dev/null @@ -1,575 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: 'PurposePath Account Service - .NET Lambda Function' - -Parameters: - Stage: - Type: String - Default: dev - Description: Deployment stage - - LogLevel: - Type: String - Default: Information - Description: Logging level for the application - -Globals: - Function: - Runtime: dotnet8 - Timeout: 30 - MemorySize: 512 - Tracing: Active - Architectures: - - x86_64 - Environment: - Variables: - STAGE: !Ref Stage - LOG_LEVEL: !Ref LogLevel - APPLICATION_NAME: PurposePath - -Resources: - # ========================================== - # API Gateway - # ========================================== - - AccountHttpApi: - Type: AWS::Serverless::HttpApi - Properties: - Name: !Sub purposepath-account-api-${Stage} - Description: !Sub PurposePath Account API (.NET) - CorsConfiguration: - AllowOrigins: - - "*" - AllowHeaders: - - Content-Type - - Authorization - - X-Amz-Date - - X-Api-Key - - X-Amz-Security-Token - AllowMethods: - - GET - - POST - - PUT - - DELETE - - OPTIONS - MaxAge: 600 - AllowCredentials: false - Tags: - Application: PurposePath - Service: Account - Stage: !Ref Stage - Runtime: dotnet8 - - # ========================================== - # Lambda Function - # ========================================== - - AccountApiFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Sub purposepath-account-api-${Stage} - Handler: PurposePath.Account.Lambda::PurposePath.Account.Lambda.LambdaEntryPoint::FunctionHandlerAsync - CodeUri: ../../pp_api/Services/PurposePath.Account.Lambda/ - Description: PurposePath Account API (.NET) - Environment: - Variables: - # Use existing shared exports where available - JWT_SECRET_ARN: - Fn::ImportValue: !Sub purposepath-jwt-secret-arn-${Stage} - REDIS_ENDPOINT: - Fn::ImportValue: !Sub purposepath-redis-endpoint-${Stage} - VPC_ID: - Fn::ImportValue: !Sub purposepath-vpc-id-${Stage} - - # Use local tables (will be created in this stack) - USERS_TABLE: !Ref UsersTable - REFRESH_TOKENS_TABLE: !Ref RefreshTokensTable - PASSWORD_RESETS_TABLE: !Ref PasswordResetsTable - VERIFICATION_TOKENS_TABLE: !Ref VerificationTokensTable - TENANTS_TABLE: !Ref TenantsTable - SUBSCRIPTIONS_TABLE: !Ref SubscriptionsTable - INVITATIONS_TABLE: !Ref InvitationsTable - USER_PREFERENCES_TABLE: !Ref UserPreferencesTable - TIERS_TABLE: !Ref TiersTable - TENANT_OVERRIDES_TABLE: !Ref TenantOverridesTable - - # Configuration - EMAIL_FROM: "noreply@purposepath.ai" - JWT_ACCESS_TOKEN_EXPIRES_MINUTES: 60 - JWT_REFRESH_TOKEN_EXPIRES_DAYS: 30 - PASSWORD_RESET_TOKEN_EXPIRES_HOURS: 24 - VERIFICATION_TOKEN_EXPIRES_DAYS: 7 - API_PREFIX: /api/v1 - CORS_ORIGINS: '["*"]' - Policies: - # DynamoDB Permissions for local tables - - Statement: - - Effect: Allow - Action: - - dynamodb:GetItem - - dynamodb:PutItem - - dynamodb:UpdateItem - - dynamodb:DeleteItem - - dynamodb:Query - - dynamodb:Scan - Resource: - - !GetAtt UsersTable.Arn - - !GetAtt RefreshTokensTable.Arn - - !GetAtt PasswordResetsTable.Arn - - !GetAtt VerificationTokensTable.Arn - - !GetAtt TenantsTable.Arn - - !GetAtt SubscriptionsTable.Arn - - !GetAtt InvitationsTable.Arn - - !GetAtt UserPreferencesTable.Arn - - !GetAtt TiersTable.Arn - - !GetAtt TenantOverridesTable.Arn - - !Sub "${UsersTable.Arn}/index/*" - - !Sub "${RefreshTokensTable.Arn}/index/*" - - !Sub "${PasswordResetsTable.Arn}/index/*" - - !Sub "${VerificationTokensTable.Arn}/index/*" - - !Sub "${TenantsTable.Arn}/index/*" - - !Sub "${SubscriptionsTable.Arn}/index/*" - - !Sub "${InvitationsTable.Arn}/index/*" - - !Sub "${UserPreferencesTable.Arn}/index/*" - - !Sub "${TiersTable.Arn}/index/*" - - !Sub "${TenantOverridesTable.Arn}/index/*" - # Secrets Manager Permissions - - Statement: - - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: - Fn::ImportValue: !Sub purposepath-jwt-secret-arn-${Stage} - # SES Permissions for Email - - Statement: - - Effect: Allow - Action: - - ses:SendEmail - - ses:SendRawEmail - - ses:SendTemplatedEmail - Resource: '*' - # CloudWatch Logs - - Statement: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: '*' - # X-Ray Tracing - - Statement: - - Effect: Allow - Action: - - xray:PutTraceSegments - - xray:PutTelemetryRecords - Resource: '*' - Events: - # Authentication endpoints - Register: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/register - Method: POST - Login: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/login - Method: POST - RefreshToken: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/refresh - Method: POST - Logout: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/logout - Method: POST - # Email verification - VerifyEmail: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/verify - Method: POST - ResendVerification: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/verify/resend - Method: POST - # Password reset - RequestPasswordReset: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/password/reset - Method: POST - ConfirmPasswordReset: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/password/confirm - Method: POST - # User profile - GetProfile: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/profile - Method: GET - UpdateProfile: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/profile - Method: PUT - DeleteAccount: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/account - Method: DELETE - # Health check - HealthCheck: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/health - Method: GET - # Additional endpoints (.NET specific) - UpdatePreferences: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/preferences - Method: PUT - GetTenants: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/tenants - Method: GET - CreateTenant: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/tenants - Method: POST - Tags: - Application: PurposePath - Service: Account - Stage: !Ref Stage - Runtime: dotnet8 - - # ========================================== - # Custom Domain Mapping (if using shared domain) - # ========================================== - - # AccountDomainMapping: - # Type: AWS::ApiGatewayV2::ApiMapping - # Properties: - # DomainName: - # Fn::ImportValue: !Sub purposepath-domain-name-${Stage} - # ApiId: !Ref AccountHttpApi - # Stage: '$default' - # ApiMappingKey: account - - # ========================================== - # DynamoDB Tables - # ========================================== - - UsersTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-users-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: id - AttributeType: S - - AttributeName: email - AttributeType: S - - AttributeName: tenant_id - AttributeType: S - KeySchema: - - AttributeName: id - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: email-index - KeySchema: - - AttributeName: email - KeyType: HASH - Projection: - ProjectionType: ALL - - IndexName: tenant-id-index - KeySchema: - - AttributeName: tenant_id - KeyType: HASH - Projection: - ProjectionType: ALL - Tags: - - Key: Application - Value: PurposePath - - Key: Service - Value: Account - - Key: Stage - Value: !Ref Stage - - RefreshTokensTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-refresh-tokens-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: jti - AttributeType: S - - AttributeName: user_id - AttributeType: S - KeySchema: - - AttributeName: jti - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: user-index - KeySchema: - - AttributeName: user_id - KeyType: HASH - Projection: - ProjectionType: ALL - TimeToLiveSpecification: - AttributeName: ttl - Enabled: true - Tags: - - Key: Application - Value: PurposePath - - Key: Service - Value: Account - - PasswordResetsTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-password-resets-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: token_hash - AttributeType: S - - AttributeName: user_id - AttributeType: S - KeySchema: - - AttributeName: token_hash - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: user-index - KeySchema: - - AttributeName: user_id - KeyType: HASH - Projection: - ProjectionType: ALL - TimeToLiveSpecification: - AttributeName: ttl - Enabled: true - Tags: - - Key: Application - Value: PurposePath - - VerificationTokensTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-verification-tokens-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: token_hash - AttributeType: S - - AttributeName: user_id - AttributeType: S - KeySchema: - - AttributeName: token_hash - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: user-index - KeySchema: - - AttributeName: user_id - KeyType: HASH - Projection: - ProjectionType: ALL - TimeToLiveSpecification: - AttributeName: ttl - Enabled: true - Tags: - - Key: Application - Value: PurposePath - - TenantsTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-tenants-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: tenant_id - AttributeType: S - - AttributeName: domain - AttributeType: S - KeySchema: - - AttributeName: tenant_id - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: domain-index - KeySchema: - - AttributeName: domain - KeyType: HASH - Projection: - ProjectionType: ALL - Tags: - - Key: Application - Value: PurposePath - - SubscriptionsTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-subscriptions-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: subscription_id - AttributeType: S - - AttributeName: tenant_id - AttributeType: S - KeySchema: - - AttributeName: subscription_id - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: tenant_id-index - KeySchema: - - AttributeName: tenant_id - KeyType: HASH - Projection: - ProjectionType: ALL - Tags: - - Key: Application - Value: PurposePath - - InvitationsTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-invitations-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: invitation_id - AttributeType: S - - AttributeName: tenant_id - AttributeType: S - - AttributeName: status - AttributeType: S - - AttributeName: email - AttributeType: S - KeySchema: - - AttributeName: invitation_id - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: tenant_id-status-index - KeySchema: - - AttributeName: tenant_id - KeyType: HASH - - AttributeName: status - KeyType: RANGE - Projection: - ProjectionType: ALL - - IndexName: email-status-index - KeySchema: - - AttributeName: email - KeyType: HASH - - AttributeName: status - KeyType: RANGE - Projection: - ProjectionType: ALL - TimeToLiveSpecification: - AttributeName: expires_at - Enabled: true - Tags: - - Key: Application - Value: PurposePath - - UserPreferencesTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-user-preferences-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: user_id - AttributeType: S - - AttributeName: tenant_id - AttributeType: S - KeySchema: - - AttributeName: user_id - KeyType: HASH - GlobalSecondaryIndexes: - - IndexName: tenant_id-index - KeySchema: - - AttributeName: tenant_id - KeyType: HASH - Projection: - ProjectionType: ALL - Tags: - - Key: Application - Value: PurposePath - - TiersTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-tiers-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: tier_id - AttributeType: S - KeySchema: - - AttributeName: tier_id - KeyType: HASH - Tags: - - Key: Application - Value: PurposePath - - TenantOverridesTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: !Sub purposepath-tenant-overrides-${Stage}-dotnet - BillingMode: PAY_PER_REQUEST - AttributeDefinitions: - - AttributeName: tenant_id - AttributeType: S - - AttributeName: feature - AttributeType: S - KeySchema: - - AttributeName: tenant_id - KeyType: HASH - - AttributeName: feature - KeyType: RANGE - Tags: - - Key: Application - Value: PurposePath - -Outputs: - AccountHttpApiId: - Description: Account HTTP API Gateway ID - Value: !Ref AccountHttpApi - Export: - Name: !Sub purposepath-account-api-id-dotnet-${Stage} - - AccountHttpApiEndpoint: - Description: Account HTTP API Gateway Endpoint - Value: !Sub "https://${AccountHttpApi}.execute-api.${AWS::Region}.amazonaws.com" - Export: - Name: !Sub purposepath-account-api-endpoint-dotnet-${Stage} - - AccountFunctionArn: - Description: Account Lambda function ARN - Value: !GetAtt AccountApiFunction.Arn - Export: - Name: !Sub purposepath-account-function-arn-dotnet-${Stage} - - Runtime: - Description: Deployed runtime - Value: dotnet8 - Export: - Name: !Sub purposepath-account-runtime-dotnet-${Stage} \ No newline at end of file diff --git a/deployment/account-service/template-python.yaml b/deployment/account-service/template-python.yaml deleted file mode 100644 index 0941cb00..00000000 --- a/deployment/account-service/template-python.yaml +++ /dev/null @@ -1,333 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: 'PurposePath Account Service - Python Lambda Function' - -Parameters: - Stage: - Type: String - Default: dev - Description: Deployment stage - - LogLevel: - Type: String - Default: INFO - Description: Logging level for the application - -Globals: - Function: - Runtime: python3.11 - Timeout: 30 - MemorySize: 1024 - Tracing: Active - Architectures: - - x86_64 - Environment: - Variables: - STAGE: !Ref Stage - LOG_LEVEL: !Ref LogLevel - APPLICATION_NAME: PurposePath - -Resources: - # ========================================== - # API Gateway - # ========================================== - - AccountHttpApi: - Type: AWS::Serverless::HttpApi - Properties: - Name: !Sub purposepath-account-api-${Stage} - Description: !Sub PurposePath Account API (Python) - CorsConfiguration: - AllowOrigins: - - "*" - AllowHeaders: - - Content-Type - - Authorization - - X-Amz-Date - - X-Api-Key - - X-Amz-Security-Token - AllowMethods: - - GET - - POST - - PUT - - DELETE - - OPTIONS - MaxAge: 600 - AllowCredentials: false - Tags: - Application: PurposePath - Service: Account - Stage: !Ref Stage - Runtime: python3.11 - - # ========================================== - # Lambda Function - # ========================================== - - AccountApiFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Sub purposepath-account-api-${Stage} - Handler: src.api.main.handler - CodeUri: ../../pp_ai_submodule/account/ - Description: PurposePath Account API (Python) - Environment: - Variables: - # Import shared resources - USERS_TABLE: - Fn::ImportValue: !Sub purposepath-users-table-${Stage} - REFRESH_TOKENS_TABLE: - Fn::ImportValue: !Sub purposepath-refresh-tokens-table-${Stage} - PASSWORD_RESETS_TABLE: - Fn::ImportValue: !Sub purposepath-password-resets-table-${Stage} - VERIFICATION_TOKENS_TABLE: - Fn::ImportValue: !Sub purposepath-verification-tokens-table-${Stage} - TENANTS_TABLE: - Fn::ImportValue: !Sub purposepath-tenants-table-${Stage} - SUBSCRIPTIONS_TABLE: - Fn::ImportValue: !Sub purposepath-subscriptions-table-${Stage} - USER_PREFERENCES_TABLE: - Fn::ImportValue: !Sub purposepath-user-preferences-table-${Stage} - EMAIL_FROM: - Fn::ImportValue: !Sub purposepath-email-from-${Stage} - JWT_SECRET_ARN: - Fn::ImportValue: !Sub purposepath-jwt-secret-arn-${Stage} - JWT_ACCESS_TOKEN_EXPIRES_MINUTES: 60 - JWT_REFRESH_TOKEN_EXPIRES_DAYS: 30 - PASSWORD_RESET_TOKEN_EXPIRES_HOURS: 24 - VERIFICATION_TOKEN_EXPIRES_DAYS: 7 - API_PREFIX: /api/v1 - CORS_ORIGINS: '["*"]' - Policies: - # DynamoDB Permissions for imported tables - - Statement: - - Effect: Allow - Action: - - dynamodb:GetItem - - dynamodb:PutItem - - dynamodb:UpdateItem - - dynamodb:DeleteItem - - dynamodb:Query - - dynamodb:Scan - Resource: - - Fn::ImportValue: !Sub purposepath-users-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-refresh-tokens-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-password-resets-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-verification-tokens-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-tenants-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-subscriptions-table-arn-${Stage} - - Fn::ImportValue: !Sub purposepath-user-preferences-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-users-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-refresh-tokens-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-password-resets-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-verification-tokens-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-tenants-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-subscriptions-table-arn-${Stage} - - !Sub - - "${TableArn}/index/*" - - TableArn: - Fn::ImportValue: !Sub purposepath-user-preferences-table-arn-${Stage} - # Secrets Manager Permissions - - Statement: - - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: - Fn::ImportValue: !Sub purposepath-jwt-secret-arn-${Stage} - # SES Permissions for Email - - Statement: - - Effect: Allow - Action: - - ses:SendEmail - - ses:SendRawEmail - - ses:SendTemplatedEmail - Resource: '*' - Condition: - StringEquals: - 'ses:FromAddress': - Fn::ImportValue: !Sub purposepath-email-from-${Stage} - # S3 Permissions (for application bucket) - - Statement: - - Effect: Allow - Action: - - s3:GetObject - - s3:PutObject - - s3:DeleteObject - Resource: - - !Sub - - "${BucketArn}/*" - - BucketArn: - Fn::ImportValue: !Sub purposepath-app-bucket-${Stage} - # CloudWatch Logs - - Statement: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: '*' - # X-Ray Tracing - - Statement: - - Effect: Allow - Action: - - xray:PutTraceSegments - - xray:PutTelemetryRecords - Resource: '*' - Events: - # Authentication endpoints - Register: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/register - Method: POST - Login: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/login - Method: POST - RefreshToken: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/refresh - Method: POST - Logout: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/logout - Method: POST - # Email verification - VerifyEmail: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/verify - Method: POST - ResendVerification: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/verify/resend - Method: POST - # Password reset - RequestPasswordReset: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/password/reset - Method: POST - ConfirmPasswordReset: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/password/confirm - Method: POST - # User profile - GetProfile: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/profile - Method: GET - UpdateProfile: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/profile - Method: PUT - DeleteAccount: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/account - Method: DELETE - # Health check - HealthCheck: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/health - Method: GET - # Additional endpoints (Python specific) - UpdatePreferences: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/auth/preferences - Method: PUT - GetTenants: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/tenants - Method: GET - CreateTenant: - Type: HttpApi - Properties: - ApiId: !Ref AccountHttpApi - Path: /api/v1/tenants - Method: POST - Tags: - Application: PurposePath - Service: Account - Stage: !Ref Stage - Runtime: python3.11 - - # ========================================== - # Custom Domain Mapping (if using shared domain) - # ========================================== - - AccountDomainMapping: - Type: AWS::ApiGatewayV2::ApiMapping - Properties: - DomainName: - Fn::ImportValue: !Sub purposepath-domain-name-${Stage} - ApiId: !Ref AccountHttpApi - Stage: '$default' - ApiMappingKey: account - -Outputs: - AccountHttpApiId: - Description: Account HTTP API Gateway ID - Value: !Ref AccountHttpApi - Export: - Name: !Sub purposepath-account-api-id-${Stage} - - AccountHttpApiEndpoint: - Description: Account HTTP API Gateway Endpoint - Value: !Sub "https://${AccountHttpApi}.execute-api.${AWS::Region}.amazonaws.com" - Export: - Name: !Sub purposepath-account-api-endpoint-${Stage} - - AccountFunctionArn: - Description: Account Lambda function ARN - Value: !GetAtt AccountApiFunction.Arn - Export: - Name: !Sub purposepath-account-function-arn-${Stage} - - Runtime: - Description: Deployed runtime - Value: python3.11 - Export: - Name: !Sub purposepath-account-runtime-${Stage} \ No newline at end of file diff --git a/deployment/shared-infrastructure/deploy-shared-infrastructure.ps1 b/deployment/shared-infrastructure/deploy-shared-infrastructure.ps1 deleted file mode 100644 index 48422231..00000000 --- a/deployment/shared-infrastructure/deploy-shared-infrastructure.ps1 +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS -Deploy PurposePath shared infrastructure (one-time deployment) - -.DESCRIPTION -Deploys the shared AWS infrastructure including VPC, DynamoDB tables, ElastiCache Redis, -S3 buckets, custom domains, certificates, and CloudWatch resources. This should be -deployed once and rarely updated. - -.PARAMETER Stage -The deployment stage (dev, staging, prod) - -.PARAMETER HostedZoneId -Route53 Hosted Zone ID for custom domain (required for domain creation) - -.PARAMETER JwtSecretArn -Existing JWT Secret ARN (optional - will create new if not provided) - -.PARAMETER RedisNodeType -ElastiCache Redis node type - -.PARAMETER SkipConfirmation -Skip confirmation prompts - -.EXAMPLE -.\deploy-shared-infrastructure.ps1 -Stage dev -HostedZoneId Z123456789ABCDEF - -.EXAMPLE -.\deploy-shared-infrastructure.ps1 -Stage dev -HostedZoneId Z123456789ABCDEF -JwtSecretArn "arn:aws:secretsmanager:us-east-1:123456789:secret:jwt-abc123" -#> - -param( - [Parameter(Mandatory = $true)] - [ValidateSet("dev", "staging", "prod")] - [string]$Stage, - - [Parameter(Mandatory = $false)] - [string]$HostedZoneId = "", - - [Parameter(Mandatory = $false)] - [string]$JwtSecretArn = "", - - [Parameter(Mandatory = $false)] - [ValidateSet("cache.t3.micro", "cache.t3.small", "cache.t3.medium")] - [string]$RedisNodeType = "cache.t3.micro", - - [Parameter(Mandatory = $false)] - [string]$EmailFrom = "noreply@purposepath.ai", - - [Parameter(Mandatory = $false)] - [string]$DomainName = "api.dev.purposepath.app", - - [switch]$SkipConfirmation -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -# Colors for output -$script:Green = "`e[32m" -$script:Yellow = "`e[33m" -$script:Red = "`e[31m" -$script:Blue = "`e[34m" -$script:Reset = "`e[0m" - -function Write-Info { - param([string]$Message) - Write-Host "${Blue}[INFO]${Reset} $Message" -} - -function Write-Success { - param([string]$Message) - Write-Host "${Green}[SUCCESS]${Reset} $Message" -} - -function Write-Warning { - param([string]$Message) - Write-Host "${Yellow}[WARNING]${Reset} $Message" -} - -function Write-Error { - param([string]$Message) - Write-Host "${Red}[ERROR]${Reset} $Message" -} - -function Test-AWSCredentials { - try { - $identity = aws sts get-caller-identity --output json | ConvertFrom-Json - Write-Info "AWS Identity: $($identity.Arn)" - return $true - } - catch { - Write-Error "AWS credentials not configured or invalid" - Write-Info "Run 'aws configure' to set up your credentials" - return $false - } -} - -function Test-SAMInstalled { - try { - $samVersion = sam --version - Write-Info "SAM CLI Version: $samVersion" - return $true - } - catch { - Write-Error "AWS SAM CLI not installed or not in PATH" - Write-Info "Install SAM CLI: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html" - return $false - } -} - -function Get-StackStatus { - param([string]$StackName) - - try { - $stack = aws cloudformation describe-stacks --stack-name $StackName --output json 2>$null | ConvertFrom-Json - return $stack.Stacks[0].StackStatus - } - catch { - return $null - } -} - -function Wait-ForStackCompletion { - param( - [string]$StackName, - [string]$Operation - ) - - Write-Info "Waiting for $Operation to complete..." - - do { - Start-Sleep -Seconds 15 - $status = Get-StackStatus -StackName $StackName - Write-Info "Stack status: $status" - - if ($status -like "*_COMPLETE") { - if ($status -like "*_FAILED" -or $status -eq "ROLLBACK_COMPLETE") { - throw "Stack $Operation failed with status: $status" - } - break - } - elseif ($status -like "*_FAILED") { - throw "Stack $Operation failed with status: $status" - } - } while ($status -like "*_IN_PROGRESS") -} - -# Main deployment function -function Deploy-SharedInfrastructure { - Write-Info "Starting PurposePath Shared Infrastructure deployment..." - Write-Info "Stage: $Stage" - Write-Info "Domain: $DomainName" - Write-Info "Redis Node Type: $RedisNodeType" - - if ($HostedZoneId) { - Write-Info "Hosted Zone ID: $HostedZoneId (custom domain will be created)" - } else { - Write-Warning "No Hosted Zone ID provided - custom domain will be skipped" - } - - if ($JwtSecretArn) { - Write-Info "Using existing JWT Secret: $JwtSecretArn" - } else { - Write-Info "JWT Secret will be created automatically" - } - - # Confirmation - if (-not $SkipConfirmation) { - Write-Warning "This will deploy shared infrastructure for stage '$Stage'" - Write-Warning "This includes VPC, DynamoDB tables, Redis cluster, S3 buckets, and custom domain" - $confirmation = Read-Host "Are you sure you want to continue? (y/N)" - if ($confirmation -ne 'y' -and $confirmation -ne 'Y') { - Write-Info "Deployment cancelled by user" - return - } - } - - # Check if stack already exists - $stackName = "purposepath-shared-infrastructure-$Stage" - $existingStatus = Get-StackStatus -StackName $stackName - - if ($existingStatus) { - Write-Warning "Stack '$stackName' already exists with status: $existingStatus" - if (-not $SkipConfirmation) { - $update = Read-Host "Do you want to update the existing stack? (y/N)" - if ($update -ne 'y' -and $update -ne 'Y') { - Write-Info "Deployment cancelled by user" - return - } - } - } - - # Build parameters - $parameters = @( - "Stage=$Stage", - "RedisNodeType=$RedisNodeType", - "EmailFrom=$EmailFrom", - "DomainName=$DomainName" - ) - - if ($HostedZoneId) { - $parameters += "HostedZoneId=$HostedZoneId" - } - - if ($JwtSecretArn) { - $parameters += "JwtSecretArn=$JwtSecretArn" - } - - $parameterOverrides = $parameters -join " " - - try { - Write-Info "Validating SAM template..." - sam validate --template-file template.yaml - Write-Success "Template validation passed" - - Write-Info "Deploying shared infrastructure..." - Write-Info "Parameters: $parameterOverrides" - - # Deploy with SAM - $deployCommand = @( - "sam", "deploy", - "--template-file", "template.yaml", - "--stack-name", $stackName, - "--capabilities", "CAPABILITY_IAM", - "--parameter-overrides", $parameterOverrides, - "--no-fail-on-empty-changeset", - "--no-confirm-changeset" - ) - - Write-Info "Executing: $($deployCommand -join ' ')" - & $deployCommand[0] $deployCommand[1..($deployCommand.Length-1)] - - if ($LASTEXITCODE -ne 0) { - throw "SAM deployment failed with exit code $LASTEXITCODE" - } - - Write-Success "Shared infrastructure deployment completed successfully!" - - # Display stack outputs - Write-Info "Retrieving stack outputs..." - $outputs = aws cloudformation describe-stacks --stack-name $stackName --query "Stacks[0].Outputs" --output table - Write-Info "Stack Outputs:" - Write-Host $outputs - - Write-Success "Shared infrastructure is ready for service deployments" - Write-Info "You can now deploy account and coaching services using their respective templates" - - } catch { - Write-Error "Deployment failed: $($_.Exception.Message)" - - # Show recent stack events for troubleshooting - Write-Info "Recent stack events:" - try { - $events = aws cloudformation describe-stack-events --stack-name $stackName --max-items 10 --output table - Write-Host $events - } catch { - Write-Warning "Could not retrieve stack events" - } - - throw - } -} - -# Main script execution -try { - Write-Info "PurposePath Shared Infrastructure Deployment" - Write-Info "===========================================" - - # Validate prerequisites - if (-not (Test-AWSCredentials)) { - exit 1 - } - - if (-not (Test-SAMInstalled)) { - exit 1 - } - - # Change to script directory - $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path - Set-Location $scriptDir - Write-Info "Working directory: $((Get-Location).Path)" - - # Validate template exists - if (-not (Test-Path "template.yaml")) { - Write-Error "template.yaml not found in current directory" - exit 1 - } - - # Execute deployment - Deploy-SharedInfrastructure - -} catch { - Write-Error "Script execution failed: $($_.Exception.Message)" - exit 1 -} \ No newline at end of file