chore: import upstream snapshot with attribution
This commit is contained in:
+214
@@ -0,0 +1,214 @@
|
||||
#!/bin/bash
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Build and Push Docker Image to ECR for AgentCore Runtime
|
||||
# =============================================================================
|
||||
#
|
||||
# NOTE: This script is OPTIONAL. Running `terraform apply` with docker mode
|
||||
# automatically builds and pushes the image. Use this script only if you
|
||||
# prefer to build separately (e.g., in CI/CD pipelines) or need to rebuild
|
||||
# the image without a full terraform apply.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-and-push-image.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# -p, --pattern Agent pattern to build (default: strands-single-agent)
|
||||
# -r, --region AWS region (default: from terraform.tfvars or us-east-1)
|
||||
# -s, --stack Stack name (default: from terraform.tfvars)
|
||||
# -h, --help Show this help message
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default values
|
||||
PATTERN="strands-single-agent"
|
||||
REGION=""
|
||||
STACK_NAME=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TERRAFORM_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
PROJECT_ROOT="$(dirname "$TERRAFORM_DIR")"
|
||||
|
||||
# Print usage
|
||||
usage() {
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -p, --pattern Agent pattern to build (default: strands-single-agent)"
|
||||
echo " -r, --region AWS region (default: from terraform.tfvars or us-east-1)"
|
||||
echo " -s, --stack Stack name (default: from terraform.tfvars)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Use defaults from terraform.tfvars"
|
||||
echo " $0 -p langgraph-single-agent # Build LangGraph agent"
|
||||
echo " $0 -s my-stack -r us-west-2 # Custom stack and region"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-p|--pattern)
|
||||
PATTERN="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--region)
|
||||
REGION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-s|--stack)
|
||||
STACK_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unknown option $1${NC}"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Read values from terraform.tfvars if not provided
|
||||
if [[ -z "$STACK_NAME" || -z "$REGION" ]]; then
|
||||
TFVARS_FILE="$TERRAFORM_DIR/terraform.tfvars"
|
||||
if [[ -f "$TFVARS_FILE" ]]; then
|
||||
echo -e "${BLUE}Reading configuration from terraform.tfvars...${NC}"
|
||||
|
||||
if [[ -z "$STACK_NAME" ]]; then
|
||||
STACK_NAME=$(grep -E '^stack_name_base\s*=' "$TFVARS_FILE" | awk -F'"' '{print $2}')
|
||||
fi
|
||||
|
||||
# Region is resolved from AWS_REGION env var or AWS CLI profile (not in tfvars)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check deployment type - this script is for docker mode only
|
||||
TFVARS_FILE="$TERRAFORM_DIR/terraform.tfvars"
|
||||
if [[ -f "$TFVARS_FILE" ]]; then
|
||||
DEPLOYMENT_TYPE=$(grep -E '^backend_deployment_type\s*=' "$TFVARS_FILE" | awk -F'"' '{print $2}')
|
||||
if [[ "$DEPLOYMENT_TYPE" == "zip" ]]; then
|
||||
echo -e "${YELLOW}===========================================${NC}"
|
||||
echo -e "${YELLOW} backend_deployment_type is set to 'zip' ${NC}"
|
||||
echo -e "${YELLOW}===========================================${NC}"
|
||||
echo ""
|
||||
echo -e "This script is only needed for ${GREEN}docker${NC} deployment mode."
|
||||
echo -e "With ${GREEN}zip${NC} mode, agent code is packaged automatically during ${BLUE}terraform apply${NC}."
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Resolve region: CLI flag > AWS_REGION env > AWS_DEFAULT_REGION env > AWS CLI config
|
||||
if [[ -z "$REGION" ]]; then
|
||||
REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-$(aws configure get region 2>/dev/null || echo "")}}"
|
||||
fi
|
||||
|
||||
# Validate required values
|
||||
if [[ -z "$STACK_NAME" ]]; then
|
||||
echo -e "${RED}Error: Stack name not found. Please specify with -s or set in terraform.tfvars${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$REGION" ]]; then
|
||||
echo -e "${RED}Error: AWS region not found. Set AWS_REGION environment variable or configure via 'aws configure'.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get AWS account ID
|
||||
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text 2>/dev/null)
|
||||
if [[ -z "$AWS_ACCOUNT_ID" ]]; then
|
||||
echo -e "${RED}Error: Could not get AWS account ID. Check your AWS credentials.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Construct ECR repository URL
|
||||
ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${STACK_NAME}-agent-runtime"
|
||||
DOCKERFILE="patterns/${PATTERN}/Dockerfile"
|
||||
|
||||
# Print configuration
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} Docker Image Build & Push for ECR ${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo -e " Stack Name: ${GREEN}${STACK_NAME}${NC}"
|
||||
echo -e " AWS Account: ${GREEN}${AWS_ACCOUNT_ID}${NC}"
|
||||
echo -e " Region: ${GREEN}${REGION}${NC}"
|
||||
echo -e " Pattern: ${GREEN}${PATTERN}${NC}"
|
||||
echo -e " ECR Repo: ${GREEN}${ECR_REPO}${NC}"
|
||||
echo ""
|
||||
|
||||
# Verify Dockerfile exists
|
||||
if [[ ! -f "$PROJECT_ROOT/$DOCKERFILE" ]]; then
|
||||
echo -e "${RED}Error: Dockerfile not found at $PROJECT_ROOT/$DOCKERFILE${NC}"
|
||||
echo -e "${YELLOW}Available patterns:${NC}"
|
||||
ls -1 "$PROJECT_ROOT/patterns/" 2>/dev/null || echo " No patterns found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: Docker is not running. Please start Docker Desktop.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Login to ECR
|
||||
echo -e "${BLUE}Step 1/3: Logging into ECR...${NC}"
|
||||
aws ecr get-login-password --region "$REGION" | docker login --username AWS --password-stdin "${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${RED}Error: ECR login failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ ECR login successful${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 2: Build Docker image with ARM64 architecture (required by AgentCore Runtime)
|
||||
echo -e "${BLUE}Step 2/3: Building Docker image (ARM64 architecture)...${NC}"
|
||||
cd "$PROJECT_ROOT"
|
||||
docker build \
|
||||
--platform linux/arm64 \
|
||||
-f "$DOCKERFILE" \
|
||||
-t "${ECR_REPO}:latest" \
|
||||
.
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${RED}Error: Docker build failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Docker build successful${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 3: Push to ECR
|
||||
echo -e "${BLUE}Step 3/3: Pushing image to ECR...${NC}"
|
||||
docker push "${ECR_REPO}:latest"
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${RED}Error: Docker push failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Docker push successful${NC}"
|
||||
echo ""
|
||||
|
||||
# Success message
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} Image successfully pushed to ECR! ${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "Image URI: ${BLUE}${ECR_REPO}:latest${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next step:${NC} Run 'terraform apply' again to create the AgentCore Runtime"
|
||||
echo ""
|
||||
@@ -0,0 +1,614 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
Cross-platform frontend deployment script for Terraform deployments.
|
||||
|
||||
Deploys the Next.js frontend to AWS Amplify by:
|
||||
1. Fetching configuration from Terraform outputs
|
||||
2. Generating aws-exports.json
|
||||
3. Building the frontend
|
||||
4. Packaging and uploading to S3
|
||||
5. Triggering Amplify deployment
|
||||
|
||||
Requires: Python 3.8+, AWS CLI, npm, Node.js, Terraform
|
||||
No external Python dependencies - uses standard library only.
|
||||
|
||||
Usage:
|
||||
cd infra-terraform
|
||||
python scripts/deploy-frontend.py
|
||||
|
||||
# Or with pattern override
|
||||
python scripts/deploy-frontend.py --pattern langgraph-single-agent
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess # nosec B404 - subprocess used securely with explicit parameters
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
# Minimum Python version check
|
||||
if sys.version_info < (3, 8):
|
||||
print("Error: Python 3.8 or higher is required")
|
||||
sys.exit(1)
|
||||
|
||||
# Constants
|
||||
BRANCH_NAME = "main"
|
||||
NEXT_BUILD_DIR = "build"
|
||||
CLEANUP_FILES: list = []
|
||||
|
||||
|
||||
# --- Logging helpers ---
|
||||
|
||||
|
||||
def log_info(message: str) -> None:
|
||||
"""Print an info message."""
|
||||
print(f"ℹ {message}")
|
||||
|
||||
|
||||
def log_success(message: str) -> None:
|
||||
"""Print a success message."""
|
||||
print(f"✓ {message}")
|
||||
|
||||
|
||||
def log_error(message: str) -> None:
|
||||
"""Print an error message to stderr."""
|
||||
print(f"✗ {message}", file=sys.stderr)
|
||||
|
||||
|
||||
def log_warning(message: str) -> None:
|
||||
"""Print a warning message."""
|
||||
print(f"⚠ {message}")
|
||||
|
||||
|
||||
# --- Utility functions ---
|
||||
|
||||
|
||||
def cleanup() -> None:
|
||||
"""Remove temporary files created during deployment."""
|
||||
for filepath in CLEANUP_FILES:
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
log_info(f"Cleaned up {filepath}")
|
||||
|
||||
|
||||
def run_command(
|
||||
command: list,
|
||||
capture_output: bool = True,
|
||||
check: bool = True,
|
||||
cwd: Optional[str] = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Execute a command securely via subprocess.
|
||||
|
||||
Args:
|
||||
command: List of command arguments
|
||||
capture_output: Whether to capture stdout/stderr
|
||||
check: Whether to raise on non-zero exit
|
||||
cwd: Working directory for the command
|
||||
|
||||
Returns:
|
||||
CompletedProcess instance with command results
|
||||
"""
|
||||
return subprocess.run( # nosec B603 - command constructed from safe list
|
||||
command,
|
||||
capture_output=capture_output,
|
||||
text=True,
|
||||
check=check,
|
||||
shell=False,
|
||||
timeout=300,
|
||||
cwd=cwd,
|
||||
)
|
||||
|
||||
|
||||
def check_prerequisite(command: str) -> bool:
|
||||
"""
|
||||
Check if a command is available in PATH.
|
||||
|
||||
Args:
|
||||
command: Name of the command to check
|
||||
|
||||
Returns:
|
||||
True if command exists, False otherwise
|
||||
"""
|
||||
return shutil.which(command) is not None
|
||||
|
||||
|
||||
def parse_tfvars(tfvars_path: Path) -> Dict[str, str]:
|
||||
"""
|
||||
Parse terraform.tfvars using regex (no HCL parser dependency).
|
||||
|
||||
Args:
|
||||
tfvars_path: Path to terraform.tfvars file
|
||||
|
||||
Returns:
|
||||
Dictionary with parsed values
|
||||
"""
|
||||
config = {"backend_pattern": "strands-single-agent"}
|
||||
|
||||
if not tfvars_path.exists():
|
||||
return config
|
||||
|
||||
content = tfvars_path.read_text()
|
||||
|
||||
# Extract backend_pattern
|
||||
match = re.search(r'^backend_pattern\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
||||
if match:
|
||||
config["backend_pattern"] = match.group(1)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_file_size_human(filepath: str) -> str:
|
||||
"""
|
||||
Get human-readable file size.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file
|
||||
|
||||
Returns:
|
||||
Human-readable size string (e.g., "1.5MB")
|
||||
"""
|
||||
size = os.path.getsize(filepath)
|
||||
for unit in ["B", "KB", "MB", "GB"]:
|
||||
if size < 1024:
|
||||
return f"{size:.1f}{unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f}TB"
|
||||
|
||||
|
||||
# --- Terraform output functions ---
|
||||
|
||||
|
||||
def get_terraform_outputs(terraform_dir: Path) -> Dict[str, str]:
|
||||
"""
|
||||
Fetch Terraform outputs via terraform output command.
|
||||
|
||||
Args:
|
||||
terraform_dir: Path to the Terraform directory
|
||||
|
||||
Returns:
|
||||
Dictionary mapping output keys to values
|
||||
"""
|
||||
result = run_command(["terraform", "output", "-json"], cwd=str(terraform_dir))
|
||||
|
||||
raw_outputs = json.loads(result.stdout)
|
||||
|
||||
# Flatten the Terraform output format (each value has a "value" key)
|
||||
outputs = {}
|
||||
for key, data in raw_outputs.items():
|
||||
if isinstance(data, dict) and "value" in data:
|
||||
outputs[key] = data["value"]
|
||||
else:
|
||||
outputs[key] = data
|
||||
|
||||
return outputs
|
||||
|
||||
|
||||
# --- AWS CLI wrappers ---
|
||||
|
||||
|
||||
def upload_to_s3(local_path: str, bucket: str, key: str) -> None:
|
||||
"""
|
||||
Upload a file to S3 via AWS CLI.
|
||||
|
||||
Args:
|
||||
local_path: Path to local file
|
||||
bucket: S3 bucket name
|
||||
key: S3 object key
|
||||
"""
|
||||
run_command(
|
||||
["aws", "s3", "cp", local_path, f"s3://{bucket}/{key}", "--no-progress"]
|
||||
)
|
||||
|
||||
|
||||
def start_amplify_deployment(app_id: str, branch: str, source_url: str) -> Dict:
|
||||
"""
|
||||
Start an Amplify deployment via AWS CLI.
|
||||
|
||||
Args:
|
||||
app_id: Amplify application ID
|
||||
branch: Branch name to deploy
|
||||
source_url: S3 URL of deployment package
|
||||
|
||||
Returns:
|
||||
Deployment response as dictionary
|
||||
"""
|
||||
result = run_command(
|
||||
[
|
||||
"aws",
|
||||
"amplify",
|
||||
"start-deployment",
|
||||
"--app-id",
|
||||
app_id,
|
||||
"--branch-name",
|
||||
branch,
|
||||
"--source-url",
|
||||
source_url,
|
||||
"--output",
|
||||
"json",
|
||||
]
|
||||
)
|
||||
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def get_amplify_job_status(app_id: str, branch: str, job_id: str) -> str:
|
||||
"""
|
||||
Get the status of an Amplify deployment job.
|
||||
|
||||
Args:
|
||||
app_id: Amplify application ID
|
||||
branch: Branch name
|
||||
job_id: Deployment job ID
|
||||
|
||||
Returns:
|
||||
Job status string
|
||||
"""
|
||||
result = run_command(
|
||||
[
|
||||
"aws",
|
||||
"amplify",
|
||||
"get-job",
|
||||
"--app-id",
|
||||
app_id,
|
||||
"--branch-name",
|
||||
branch,
|
||||
"--job-id",
|
||||
job_id,
|
||||
"--output",
|
||||
"json",
|
||||
]
|
||||
)
|
||||
|
||||
return json.loads(result.stdout)["job"]["summary"]["status"]
|
||||
|
||||
|
||||
def get_amplify_app_domain(app_id: str) -> str:
|
||||
"""
|
||||
Get the default domain for an Amplify app.
|
||||
|
||||
Args:
|
||||
app_id: Amplify application ID
|
||||
|
||||
Returns:
|
||||
Default domain string
|
||||
"""
|
||||
result = run_command(
|
||||
[
|
||||
"aws",
|
||||
"amplify",
|
||||
"get-app",
|
||||
"--app-id",
|
||||
app_id,
|
||||
"--query",
|
||||
"app.defaultDomain",
|
||||
"--output",
|
||||
"text",
|
||||
]
|
||||
)
|
||||
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
# --- Main deployment logic ---
|
||||
|
||||
|
||||
def generate_aws_exports(
|
||||
outputs: Dict[str, str], pattern: str, frontend_dir: Path
|
||||
) -> None:
|
||||
"""
|
||||
Generate aws-exports.json configuration file.
|
||||
|
||||
Args:
|
||||
outputs: Terraform outputs dictionary
|
||||
pattern: Agent pattern name
|
||||
frontend_dir: Path to frontend directory
|
||||
"""
|
||||
# Map Terraform output names to required values
|
||||
required_mappings = {
|
||||
"cognito_web_client_id": "client_id",
|
||||
"cognito_user_pool_id": "user_pool_id",
|
||||
"amplify_app_url": "app_url",
|
||||
"runtime_arn": "runtime_arn",
|
||||
"feedback_api_url": "feedback_api_url",
|
||||
"copilotkit_runtime_url": "copilotkit_runtime_url",
|
||||
}
|
||||
|
||||
missing = [k for k in required_mappings.keys() if k not in outputs]
|
||||
if missing:
|
||||
raise ValueError(f"Missing required Terraform outputs: {', '.join(missing)}")
|
||||
|
||||
# Get region from deployment_summary or default
|
||||
region = "us-east-1"
|
||||
if "deployment_summary" in outputs and isinstance(
|
||||
outputs["deployment_summary"], dict
|
||||
):
|
||||
region = outputs["deployment_summary"].get("region", region)
|
||||
|
||||
aws_exports = {
|
||||
"authority": f"https://cognito-idp.{region}.amazonaws.com/{outputs['cognito_user_pool_id']}",
|
||||
"client_id": outputs["cognito_web_client_id"],
|
||||
"redirect_uri": outputs["amplify_app_url"],
|
||||
"post_logout_redirect_uri": outputs["amplify_app_url"],
|
||||
"response_type": "code",
|
||||
"scope": "email openid profile",
|
||||
"automaticSilentRenew": True,
|
||||
"agentRuntimeArn": outputs["runtime_arn"],
|
||||
"awsRegion": region,
|
||||
"feedbackApiUrl": outputs["feedback_api_url"],
|
||||
"copilotKitRuntimeUrl": outputs["copilotkit_runtime_url"],
|
||||
"agentPattern": pattern,
|
||||
}
|
||||
|
||||
public_dir = frontend_dir / "public"
|
||||
public_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
output_path = public_dir / "aws-exports.json"
|
||||
output_path.write_text(json.dumps(aws_exports, indent=2))
|
||||
|
||||
log_success(f"Generated aws-exports.json at {output_path}")
|
||||
|
||||
|
||||
def create_deployment_zip(build_dir: Path, output_path: Path) -> None:
|
||||
"""
|
||||
Create a zip archive of the build directory.
|
||||
|
||||
Args:
|
||||
build_dir: Path to the build directory
|
||||
output_path: Path for the output zip file (without .zip extension)
|
||||
"""
|
||||
# shutil.make_archive adds .zip automatically
|
||||
shutil.make_archive(
|
||||
str(output_path.with_suffix("")), "zip", root_dir=str(build_dir)
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Deploy frontend to AWS Amplify using Terraform outputs",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python scripts/deploy-frontend.py
|
||||
python scripts/deploy-frontend.py --pattern langgraph-single-agent
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--pattern",
|
||||
"-p",
|
||||
type=str,
|
||||
help="Override agent pattern (default: from terraform.tfvars)",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
Main deployment function.
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
atexit.register(cleanup)
|
||||
args = parse_args()
|
||||
|
||||
# Determine paths
|
||||
script_dir = Path(__file__).parent.resolve()
|
||||
terraform_dir = script_dir.parent
|
||||
project_root = terraform_dir.parent
|
||||
frontend_dir = project_root / "frontend"
|
||||
tfvars_path = terraform_dir / "terraform.tfvars"
|
||||
|
||||
print()
|
||||
print("========================================")
|
||||
print(" Frontend Deployment (Terraform) ")
|
||||
print("========================================")
|
||||
print()
|
||||
|
||||
log_info("🚀 Starting frontend deployment process...")
|
||||
print()
|
||||
|
||||
# Validate prerequisites
|
||||
log_info("Validating prerequisites...")
|
||||
prerequisites = ["npm", "aws", "node", "terraform"]
|
||||
for prereq in prerequisites:
|
||||
if not check_prerequisite(prereq):
|
||||
log_error(f"{prereq} is not installed")
|
||||
return 1
|
||||
log_success("All prerequisites found")
|
||||
|
||||
# Verify AWS credentials are configured
|
||||
log_info("Verifying AWS credentials...")
|
||||
try:
|
||||
run_command(["aws", "sts", "get-caller-identity"], capture_output=True)
|
||||
log_success("AWS credentials configured")
|
||||
except subprocess.CalledProcessError:
|
||||
log_error("AWS credentials not configured or invalid")
|
||||
log_info("Run 'aws configure' to set up your AWS credentials")
|
||||
return 1
|
||||
|
||||
# Verify Terraform state exists
|
||||
if not (terraform_dir / "terraform.tfstate").exists():
|
||||
log_error("Terraform state not found. Run 'terraform apply' first.")
|
||||
return 1
|
||||
|
||||
# Fetch Terraform outputs
|
||||
log_info("Fetching configuration from Terraform outputs...")
|
||||
try:
|
||||
outputs = get_terraform_outputs(terraform_dir)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"Failed to fetch Terraform outputs: {e.stderr}")
|
||||
return 1
|
||||
except json.JSONDecodeError as e:
|
||||
log_error(f"Failed to parse Terraform outputs: {e}")
|
||||
return 1
|
||||
|
||||
# Validate required outputs
|
||||
app_id = outputs.get("amplify_app_id")
|
||||
deployment_bucket = outputs.get("amplify_staging_bucket")
|
||||
region = "us-east-1"
|
||||
|
||||
if "deployment_summary" in outputs and isinstance(
|
||||
outputs["deployment_summary"], dict
|
||||
):
|
||||
region = outputs["deployment_summary"].get("region", region)
|
||||
|
||||
if not app_id:
|
||||
log_error("Could not find amplify_app_id in Terraform outputs")
|
||||
return 1
|
||||
if not deployment_bucket:
|
||||
log_error("Could not find amplify_staging_bucket in Terraform outputs")
|
||||
return 1
|
||||
|
||||
log_success(f"App ID: {app_id}")
|
||||
log_success(f"Staging Bucket: {deployment_bucket}")
|
||||
log_success(f"Region: {region}")
|
||||
|
||||
# Get agent pattern
|
||||
if args.pattern:
|
||||
pattern = args.pattern
|
||||
else:
|
||||
tfvars = parse_tfvars(tfvars_path)
|
||||
pattern = tfvars.get("backend_pattern", "strands-single-agent")
|
||||
|
||||
log_info(f"Agent pattern: {pattern}")
|
||||
|
||||
# Generate aws-exports.json
|
||||
log_info("Generating aws-exports.json...")
|
||||
try:
|
||||
generate_aws_exports(outputs, pattern, frontend_dir)
|
||||
except ValueError as e:
|
||||
log_error(str(e))
|
||||
return 1
|
||||
|
||||
# Change to frontend directory
|
||||
os.chdir(frontend_dir)
|
||||
log_info(f"Working directory: {frontend_dir}")
|
||||
|
||||
# Install dependencies if needed
|
||||
node_modules = frontend_dir / "node_modules"
|
||||
package_json = frontend_dir / "package.json"
|
||||
|
||||
if not node_modules.exists() or (
|
||||
node_modules.exists()
|
||||
and package_json.exists()
|
||||
and package_json.stat().st_mtime > node_modules.stat().st_mtime
|
||||
):
|
||||
log_info("Installing dependencies...")
|
||||
try:
|
||||
run_command(["npm", "install"], capture_output=False)
|
||||
log_success("Dependencies installed")
|
||||
except subprocess.CalledProcessError:
|
||||
log_error("Failed to install dependencies")
|
||||
return 1
|
||||
else:
|
||||
log_success("Dependencies are up to date")
|
||||
|
||||
# Build frontend
|
||||
log_info("Building Next.js app...")
|
||||
try:
|
||||
run_command(["npm", "run", "build"], capture_output=False)
|
||||
log_success("Build completed")
|
||||
except subprocess.CalledProcessError:
|
||||
log_error("Build failed")
|
||||
return 1
|
||||
|
||||
# Verify build directory
|
||||
build_dir = frontend_dir / NEXT_BUILD_DIR
|
||||
if not build_dir.exists():
|
||||
log_error(f"Build directory '{NEXT_BUILD_DIR}' not found")
|
||||
return 1
|
||||
|
||||
# Copy aws-exports.json to build
|
||||
aws_exports_src = frontend_dir / "public" / "aws-exports.json"
|
||||
aws_exports_dst = build_dir / "aws-exports.json"
|
||||
shutil.copy2(aws_exports_src, aws_exports_dst)
|
||||
log_success("Added aws-exports.json to build directory")
|
||||
|
||||
# Create deployment zip
|
||||
log_info("Creating deployment package...")
|
||||
zip_path = frontend_dir / "amplify-deploy.zip"
|
||||
CLEANUP_FILES.append(str(zip_path))
|
||||
|
||||
create_deployment_zip(build_dir, zip_path)
|
||||
zip_size = get_file_size_human(str(zip_path))
|
||||
log_success(f"Package created ({zip_size})")
|
||||
|
||||
# Upload to S3
|
||||
s3_key = f"amplify-deploy-{int(time.time())}.zip"
|
||||
log_info(f"Uploading to S3 (s3://{deployment_bucket}/{s3_key})...")
|
||||
try:
|
||||
upload_to_s3(str(zip_path), deployment_bucket, s3_key)
|
||||
log_success("Upload completed")
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"S3 upload failed: {e.stderr}")
|
||||
return 1
|
||||
|
||||
# Start Amplify deployment
|
||||
log_info("Starting Amplify deployment...")
|
||||
source_url = f"s3://{deployment_bucket}/{s3_key}"
|
||||
|
||||
try:
|
||||
deployment = start_amplify_deployment(app_id, BRANCH_NAME, source_url)
|
||||
job_id = deployment["jobSummary"]["jobId"]
|
||||
log_success(f"Deployment initiated (Job ID: {job_id})")
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"Amplify deployment failed: {e.stderr}")
|
||||
return 1
|
||||
|
||||
# Poll deployment status
|
||||
log_info("Monitoring deployment status...")
|
||||
while True:
|
||||
try:
|
||||
status = get_amplify_job_status(app_id, BRANCH_NAME, job_id)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"Failed to get deployment status: {e.stderr}")
|
||||
return 1
|
||||
|
||||
print(f" Status: {status}")
|
||||
|
||||
if status == "SUCCEED":
|
||||
log_success("Deployment completed successfully!")
|
||||
break
|
||||
elif status in ("FAILED", "CANCELLED"):
|
||||
log_error(f"Deployment {status.lower()}")
|
||||
return 1
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
# Cleanup
|
||||
log_info("Cleaned up temporary files")
|
||||
|
||||
# Print final info
|
||||
print()
|
||||
log_info(f"S3 Package: s3://{deployment_bucket}/{s3_key}")
|
||||
log_info("Console: https://console.aws.amazon.com/amplify/apps")
|
||||
try:
|
||||
app_domain = get_amplify_app_domain(app_id)
|
||||
log_info(f"App URL: https://{BRANCH_NAME}.{app_domain}")
|
||||
except subprocess.CalledProcessError:
|
||||
log_warning("Could not retrieve app URL - check Amplify console")
|
||||
|
||||
print()
|
||||
print("========================================")
|
||||
print(" Frontend Deployment Complete! ")
|
||||
print("========================================")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
#!/bin/bash
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# =============================================================================
|
||||
# Deploy Frontend to AWS Amplify
|
||||
# =============================================================================
|
||||
#
|
||||
# This script deploys the Next.js frontend to Amplify using Terraform outputs.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/deploy-frontend.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# -p, --pattern Agent pattern (default: from terraform.tfvars or strands-single-agent)
|
||||
# -h, --help Show this help message
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default values
|
||||
PATTERN=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TERRAFORM_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
PROJECT_ROOT="$(dirname "$TERRAFORM_DIR")"
|
||||
FRONTEND_DIR="$PROJECT_ROOT/frontend"
|
||||
BRANCH_NAME="main"
|
||||
BUILD_DIR="build"
|
||||
|
||||
# Print usage
|
||||
usage() {
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -p, --pattern Agent pattern (default: from terraform.tfvars or strands-single-agent)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Use defaults from Terraform"
|
||||
echo " $0 -p langgraph-single-agent # Use LangGraph pattern"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-p|--pattern)
|
||||
PATTERN="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unknown option $1${NC}"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Helper functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}✗${NC} $1" >&2
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
# Get human-readable file size
|
||||
get_file_size() {
|
||||
local size=$(stat -f%z "$1" 2>/dev/null || stat --printf="%s" "$1" 2>/dev/null)
|
||||
if [[ $size -lt 1024 ]]; then
|
||||
echo "${size}B"
|
||||
elif [[ $size -lt 1048576 ]]; then
|
||||
echo "$(echo "scale=1; $size/1024" | bc)KB"
|
||||
else
|
||||
echo "$(echo "scale=1; $size/1048576" | bc)MB"
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} Frontend Deployment (Terraform) ${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
log_info "🚀 Starting frontend deployment process..."
|
||||
echo ""
|
||||
|
||||
# Validate prerequisites
|
||||
log_info "Validating prerequisites..."
|
||||
for cmd in npm aws node terraform jq; do
|
||||
if ! command -v $cmd &> /dev/null; then
|
||||
log_error "$cmd is not installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
log_success "All prerequisites found"
|
||||
|
||||
# Verify AWS credentials
|
||||
log_info "Verifying AWS credentials..."
|
||||
if ! aws sts get-caller-identity &> /dev/null; then
|
||||
log_error "AWS credentials not configured or invalid"
|
||||
log_info "Run 'aws configure' to set up your AWS credentials"
|
||||
exit 1
|
||||
fi
|
||||
log_success "AWS credentials configured"
|
||||
|
||||
# Change to Terraform directory to get outputs
|
||||
cd "$TERRAFORM_DIR"
|
||||
|
||||
# Verify Terraform state exists
|
||||
if [[ ! -f "terraform.tfstate" ]]; then
|
||||
log_error "Terraform state not found. Run 'terraform apply' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get Terraform outputs
|
||||
log_info "Fetching configuration from Terraform outputs..."
|
||||
|
||||
# Get all required outputs
|
||||
AMPLIFY_APP_ID=$(terraform output -raw amplify_app_id 2>/dev/null)
|
||||
STAGING_BUCKET=$(terraform output -raw amplify_staging_bucket 2>/dev/null)
|
||||
COGNITO_CLIENT_ID=$(terraform output -raw cognito_web_client_id 2>/dev/null)
|
||||
COGNITO_USER_POOL_ID=$(terraform output -raw cognito_user_pool_id 2>/dev/null)
|
||||
AMPLIFY_URL=$(terraform output -raw amplify_app_url 2>/dev/null)
|
||||
RUNTIME_ARN=$(terraform output -raw runtime_arn 2>/dev/null)
|
||||
FEEDBACK_API_URL=$(terraform output -raw feedback_api_url 2>/dev/null)
|
||||
COPILOTKIT_RUNTIME_URL=$(terraform output -raw copilotkit_runtime_url 2>/dev/null)
|
||||
AWS_REGION=$(terraform output -json deployment_summary 2>/dev/null | jq -r '.region')
|
||||
|
||||
# Validate required outputs
|
||||
if [[ -z "$AMPLIFY_APP_ID" ]]; then
|
||||
log_error "Could not find Amplify App ID in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$STAGING_BUCKET" ]]; then
|
||||
log_error "Could not find Staging Bucket in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$COGNITO_CLIENT_ID" ]]; then
|
||||
log_error "Could not find Cognito Client ID in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$RUNTIME_ARN" ]]; then
|
||||
log_error "Could not find Runtime ARN in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$COPILOTKIT_RUNTIME_URL" ]]; then
|
||||
log_error "Could not find CopilotKit runtime URL in Terraform outputs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "App ID: $AMPLIFY_APP_ID"
|
||||
log_success "Staging Bucket: $STAGING_BUCKET"
|
||||
log_success "Region: $AWS_REGION"
|
||||
|
||||
# Get pattern from terraform.tfvars if not provided
|
||||
if [[ -z "$PATTERN" ]]; then
|
||||
TFVARS_FILE="$TERRAFORM_DIR/terraform.tfvars"
|
||||
if [[ -f "$TFVARS_FILE" ]]; then
|
||||
PATTERN=$(grep -E '^backend_pattern\s*=' "$TFVARS_FILE" | awk -F'"' '{print $2}')
|
||||
fi
|
||||
fi
|
||||
PATTERN="${PATTERN:-strands-single-agent}"
|
||||
log_info "Agent pattern: $PATTERN"
|
||||
|
||||
# Generate aws-exports.json
|
||||
log_info "Generating aws-exports.json..."
|
||||
|
||||
AWS_EXPORTS=$(cat <<EOF
|
||||
{
|
||||
"authority": "https://cognito-idp.${AWS_REGION}.amazonaws.com/${COGNITO_USER_POOL_ID}",
|
||||
"client_id": "${COGNITO_CLIENT_ID}",
|
||||
"redirect_uri": "${AMPLIFY_URL}",
|
||||
"post_logout_redirect_uri": "${AMPLIFY_URL}",
|
||||
"response_type": "code",
|
||||
"scope": "email openid profile",
|
||||
"automaticSilentRenew": true,
|
||||
"agentRuntimeArn": "${RUNTIME_ARN}",
|
||||
"awsRegion": "${AWS_REGION}",
|
||||
"feedbackApiUrl": "${FEEDBACK_API_URL}",
|
||||
"copilotKitRuntimeUrl": "${COPILOTKIT_RUNTIME_URL}",
|
||||
"agentPattern": "${PATTERN}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Write aws-exports.json to frontend/public
|
||||
mkdir -p "$FRONTEND_DIR/public"
|
||||
echo "$AWS_EXPORTS" > "$FRONTEND_DIR/public/aws-exports.json"
|
||||
log_success "Generated aws-exports.json at $FRONTEND_DIR/public/aws-exports.json"
|
||||
|
||||
# Change to frontend directory
|
||||
cd "$FRONTEND_DIR"
|
||||
log_info "Working directory: $FRONTEND_DIR"
|
||||
|
||||
# Install dependencies if needed
|
||||
if [[ ! -d "node_modules" ]] || [[ "package.json" -nt "node_modules" ]]; then
|
||||
log_info "Installing dependencies..."
|
||||
npm install
|
||||
log_success "Dependencies installed"
|
||||
else
|
||||
log_success "Dependencies are up to date"
|
||||
fi
|
||||
|
||||
# Build frontend
|
||||
log_info "Building Next.js app..."
|
||||
npm run build
|
||||
log_success "Build completed"
|
||||
|
||||
# Verify build directory exists
|
||||
if [[ ! -d "$BUILD_DIR" ]]; then
|
||||
log_error "Build directory '$BUILD_DIR' not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy aws-exports.json to build directory
|
||||
cp "$FRONTEND_DIR/public/aws-exports.json" "$FRONTEND_DIR/$BUILD_DIR/aws-exports.json"
|
||||
log_success "Added aws-exports.json to build directory"
|
||||
|
||||
# Create deployment zip
|
||||
log_info "Creating deployment package..."
|
||||
ZIP_FILE="$FRONTEND_DIR/amplify-deploy.zip"
|
||||
cd "$FRONTEND_DIR/$BUILD_DIR"
|
||||
zip -r "$ZIP_FILE" . -x "*.DS_Store" > /dev/null
|
||||
cd "$FRONTEND_DIR"
|
||||
|
||||
ZIP_SIZE=$(get_file_size "$ZIP_FILE")
|
||||
log_success "Package created ($ZIP_SIZE)"
|
||||
|
||||
# Upload to S3
|
||||
S3_KEY="amplify-deploy-$(date +%s).zip"
|
||||
log_info "Uploading to S3 (s3://${STAGING_BUCKET}/${S3_KEY})..."
|
||||
aws s3 cp "$ZIP_FILE" "s3://${STAGING_BUCKET}/${S3_KEY}" --no-progress
|
||||
log_success "Upload completed"
|
||||
|
||||
# Start Amplify deployment
|
||||
log_info "Starting Amplify deployment..."
|
||||
DEPLOYMENT=$(aws amplify start-deployment \
|
||||
--app-id "$AMPLIFY_APP_ID" \
|
||||
--branch-name "$BRANCH_NAME" \
|
||||
--source-url "s3://${STAGING_BUCKET}/${S3_KEY}" \
|
||||
--output json)
|
||||
|
||||
JOB_ID=$(echo "$DEPLOYMENT" | jq -r '.jobSummary.jobId')
|
||||
log_success "Deployment initiated (Job ID: $JOB_ID)"
|
||||
|
||||
# Monitor deployment status
|
||||
log_info "Monitoring deployment status..."
|
||||
while true; do
|
||||
STATUS=$(aws amplify get-job \
|
||||
--app-id "$AMPLIFY_APP_ID" \
|
||||
--branch-name "$BRANCH_NAME" \
|
||||
--job-id "$JOB_ID" \
|
||||
--query 'job.summary.status' \
|
||||
--output text)
|
||||
|
||||
echo " Status: $STATUS"
|
||||
|
||||
if [[ "$STATUS" == "SUCCEED" ]]; then
|
||||
log_success "Deployment completed successfully!"
|
||||
break
|
||||
elif [[ "$STATUS" == "FAILED" ]] || [[ "$STATUS" == "CANCELLED" ]]; then
|
||||
log_error "Deployment ${STATUS,,}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Cleanup
|
||||
rm -f "$ZIP_FILE"
|
||||
log_info "Cleaned up temporary files"
|
||||
|
||||
# Print final info
|
||||
echo ""
|
||||
log_info "S3 Package: s3://${STAGING_BUCKET}/${S3_KEY}"
|
||||
log_info "Console: https://console.aws.amazon.com/amplify/apps"
|
||||
|
||||
# Get app domain
|
||||
APP_DOMAIN=$(aws amplify get-app --app-id "$AMPLIFY_APP_ID" --query 'app.defaultDomain' --output text 2>/dev/null || echo "")
|
||||
if [[ -n "$APP_DOMAIN" ]]; then
|
||||
log_info "App URL: https://${BRANCH_NAME}.${APP_DOMAIN}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} Frontend Deployment Complete! ${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
@@ -0,0 +1,366 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
Test Agent - AgentCore Runtime CLI Tester (Terraform)
|
||||
|
||||
Tests the deployed agent using Terraform outputs. Authenticates via Cognito
|
||||
and invokes the agent with streaming response display.
|
||||
|
||||
Prerequisites:
|
||||
- Terraform infrastructure deployed (terraform apply)
|
||||
- AgentCore Runtime created
|
||||
- Dependencies: pip install boto3 requests colorama
|
||||
|
||||
Usage:
|
||||
cd infra-terraform
|
||||
python scripts/test-agent.py [message]
|
||||
|
||||
Examples:
|
||||
python scripts/test-agent.py 'Hello' # Test with message
|
||||
python scripts/test-agent.py # Uses default message
|
||||
"""
|
||||
|
||||
import getpass
|
||||
import json
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple
|
||||
from urllib.parse import quote
|
||||
|
||||
import boto3
|
||||
import requests
|
||||
from colorama import Fore, Style, init
|
||||
|
||||
# Initialize colorama for cross-platform colored output
|
||||
init()
|
||||
|
||||
|
||||
def log_info(msg: str) -> None:
|
||||
"""Print info message."""
|
||||
print(f"{Fore.BLUE}ℹ{Style.RESET_ALL} {msg}")
|
||||
|
||||
|
||||
def log_success(msg: str) -> None:
|
||||
"""Print success message."""
|
||||
print(f"{Fore.GREEN}✓{Style.RESET_ALL} {msg}")
|
||||
|
||||
|
||||
def log_error(msg: str) -> None:
|
||||
"""Print error message."""
|
||||
print(f"{Fore.RED}✗{Style.RESET_ALL} {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def get_terraform_outputs() -> Dict[str, str]:
|
||||
"""
|
||||
Get outputs from Terraform state.
|
||||
|
||||
Returns:
|
||||
Dict with runtime_arn, cognito_user_pool_id, cognito_web_client_id, region
|
||||
"""
|
||||
terraform_dir = Path(__file__).parent.parent
|
||||
|
||||
try:
|
||||
# Get individual outputs
|
||||
result = subprocess.run( # nosec B603, B607
|
||||
["terraform", "output", "-json"],
|
||||
cwd=terraform_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
outputs = json.loads(result.stdout)
|
||||
|
||||
return {
|
||||
"runtime_arn": outputs.get("runtime_arn", {}).get("value", ""),
|
||||
"cognito_user_pool_id": outputs.get("cognito_user_pool_id", {}).get(
|
||||
"value", ""
|
||||
),
|
||||
"cognito_web_client_id": outputs.get("cognito_web_client_id", {}).get(
|
||||
"value", ""
|
||||
),
|
||||
"region": outputs.get("deployment_summary", {})
|
||||
.get("value", {})
|
||||
.get("region", "us-east-1"),
|
||||
}
|
||||
except subprocess.CalledProcessError as e:
|
||||
log_error(f"Failed to get Terraform outputs: {e.stderr}")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
log_error(f"Failed to parse Terraform outputs: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def authenticate_cognito(
|
||||
user_pool_id: str, client_id: str, username: str, password: str, region: str
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Authenticate with Cognito and return ID token.
|
||||
|
||||
Args:
|
||||
user_pool_id: Cognito User Pool ID
|
||||
client_id: Cognito Client ID
|
||||
username: User's email/username
|
||||
password: User's password
|
||||
region: AWS region
|
||||
|
||||
Returns:
|
||||
Tuple of (id_token, access_token)
|
||||
"""
|
||||
client = boto3.client("cognito-idp", region_name=region)
|
||||
|
||||
try:
|
||||
response = client.initiate_auth(
|
||||
AuthFlow="USER_PASSWORD_AUTH",
|
||||
ClientId=client_id,
|
||||
AuthParameters={
|
||||
"USERNAME": username,
|
||||
"PASSWORD": password,
|
||||
},
|
||||
)
|
||||
|
||||
# Handle NEW_PASSWORD_REQUIRED challenge
|
||||
if response.get("ChallengeName") == "NEW_PASSWORD_REQUIRED":
|
||||
log_info("New password required for first-time login")
|
||||
new_password = getpass.getpass("Set New Password: ")
|
||||
|
||||
response = client.respond_to_auth_challenge(
|
||||
ClientId=client_id,
|
||||
ChallengeName="NEW_PASSWORD_REQUIRED",
|
||||
ChallengeResponses={
|
||||
"USERNAME": username,
|
||||
"NEW_PASSWORD": new_password,
|
||||
},
|
||||
Session=response["Session"],
|
||||
)
|
||||
|
||||
auth_result = response.get("AuthenticationResult", {})
|
||||
id_token = auth_result.get("IdToken")
|
||||
access_token = auth_result.get("AccessToken")
|
||||
|
||||
if not id_token:
|
||||
log_error("Failed to get ID token from authentication response")
|
||||
sys.exit(1)
|
||||
|
||||
return id_token, access_token
|
||||
|
||||
except client.exceptions.NotAuthorizedException as e:
|
||||
log_error(f"Authentication failed: {e}")
|
||||
sys.exit(1)
|
||||
except client.exceptions.UserNotFoundException as e:
|
||||
log_error(f"User not found: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
log_error(f"Authentication error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def generate_session_id() -> str:
|
||||
"""Generate a session ID (must be >= 33 characters)."""
|
||||
return f"test-session-{int(time.time())}-{uuid.uuid4().hex[:16]}"
|
||||
|
||||
|
||||
def generate_trace_id() -> str:
|
||||
"""Generate X-Amzn-Trace-Id header value."""
|
||||
timestamp_hex = format(int(time.time()), "x")
|
||||
return f"1-{timestamp_hex}-{uuid.uuid4().hex[:24]}"
|
||||
|
||||
|
||||
def sanitize_user_id(email: str) -> str:
|
||||
"""
|
||||
Sanitize user ID for Memory API (replace @ and . with allowed characters).
|
||||
|
||||
Args:
|
||||
email: User's email address
|
||||
|
||||
Returns:
|
||||
Sanitized user ID matching regex [a-zA-Z0-9][a-zA-Z0-9-_/]*
|
||||
"""
|
||||
return email.replace("@", "-at-").replace(".", "-")
|
||||
|
||||
|
||||
def invoke_agent(
|
||||
runtime_arn: str,
|
||||
region: str,
|
||||
id_token: str,
|
||||
prompt: str,
|
||||
session_id: str,
|
||||
user_id: str,
|
||||
) -> None:
|
||||
"""
|
||||
Invoke the agent and stream the response.
|
||||
|
||||
Args:
|
||||
runtime_arn: AgentCore Runtime ARN
|
||||
region: AWS region
|
||||
id_token: Cognito ID token
|
||||
prompt: User's message
|
||||
session_id: Session ID for conversation
|
||||
user_id: Sanitized user ID
|
||||
"""
|
||||
# Build URL
|
||||
endpoint = f"https://bedrock-agentcore.{region}.amazonaws.com"
|
||||
encoded_arn = quote(runtime_arn, safe="")
|
||||
url = f"{endpoint}/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT"
|
||||
|
||||
# Generate trace ID
|
||||
trace_id = generate_trace_id()
|
||||
|
||||
# Headers
|
||||
headers = {
|
||||
"Authorization": f"Bearer {id_token}",
|
||||
"Content-Type": "application/json",
|
||||
"X-Amzn-Trace-Id": trace_id,
|
||||
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": session_id,
|
||||
}
|
||||
|
||||
# Payload
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"runtimeSessionId": session_id,
|
||||
"userId": user_id,
|
||||
}
|
||||
|
||||
log_info(f"Invoking agent at: {url}")
|
||||
print()
|
||||
print(f"{Fore.GREEN}Agent Response:{Style.RESET_ALL}")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Stream the response
|
||||
response = requests.post(
|
||||
url, headers=headers, json=payload, stream=True, timeout=120
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
log_error(f"HTTP {response.status_code}: {response.text}")
|
||||
return
|
||||
|
||||
# Process streaming response
|
||||
for line in response.iter_lines(decode_unicode=True):
|
||||
if line:
|
||||
# Parse SSE format
|
||||
if line.startswith("data: "):
|
||||
data = line[6:] # Remove "data: " prefix
|
||||
try:
|
||||
parsed = json.loads(data)
|
||||
# Check if parsed is a dict (not a string)
|
||||
if isinstance(parsed, dict):
|
||||
# Extract text from contentBlockDelta events
|
||||
if "event" in parsed:
|
||||
event = parsed["event"]
|
||||
if (
|
||||
isinstance(event, dict)
|
||||
and "contentBlockDelta" in event
|
||||
):
|
||||
delta = event["contentBlockDelta"].get("delta", {})
|
||||
if isinstance(delta, dict):
|
||||
text = delta.get("text", "")
|
||||
if text:
|
||||
print(text, end="", flush=True)
|
||||
# Check for final message
|
||||
if "message" in parsed:
|
||||
message = parsed["message"]
|
||||
if isinstance(message, dict):
|
||||
content = message.get("content", [])
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and "text" in block
|
||||
):
|
||||
# Don't print final message - we already streamed it
|
||||
pass
|
||||
except json.JSONDecodeError:
|
||||
# Not JSON, skip internal debug strings
|
||||
pass
|
||||
|
||||
print()
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log_error(f"Connection error: {e}")
|
||||
except requests.exceptions.Timeout:
|
||||
log_error("Request timed out")
|
||||
except Exception as e:
|
||||
log_error(f"Error: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
print()
|
||||
print(f"{Fore.BLUE}========================================{Style.RESET_ALL}")
|
||||
print(f"{Fore.BLUE} Agent Test (Terraform - Python) {Style.RESET_ALL}")
|
||||
print(f"{Fore.BLUE}========================================{Style.RESET_ALL}")
|
||||
print()
|
||||
|
||||
# Get message from args or use default
|
||||
message = sys.argv[1] if len(sys.argv) > 1 else "Hello! What are you capable of?"
|
||||
|
||||
# Get Terraform outputs
|
||||
log_info("Fetching configuration from Terraform outputs...")
|
||||
outputs = get_terraform_outputs()
|
||||
|
||||
runtime_arn = outputs["runtime_arn"]
|
||||
if not runtime_arn:
|
||||
log_error("Could not find Runtime ARN in Terraform outputs")
|
||||
sys.exit(1)
|
||||
|
||||
log_success(f"Runtime ARN: {runtime_arn}")
|
||||
log_success(f"Region: {outputs['region']}")
|
||||
|
||||
# Get credentials
|
||||
print()
|
||||
log_info("Enter your Cognito credentials (admin user created during deployment)")
|
||||
email = input("Email: ").strip()
|
||||
password = getpass.getpass("Password: ")
|
||||
|
||||
# Authenticate
|
||||
log_info("Authenticating with Cognito...")
|
||||
id_token, _ = authenticate_cognito(
|
||||
user_pool_id=outputs["cognito_user_pool_id"],
|
||||
client_id=outputs["cognito_web_client_id"],
|
||||
username=email,
|
||||
password=password,
|
||||
region=outputs["region"],
|
||||
)
|
||||
log_success("Authentication successful!")
|
||||
|
||||
# Generate session ID
|
||||
session_id = generate_session_id()
|
||||
|
||||
# Sanitize user ID
|
||||
user_id = sanitize_user_id(email)
|
||||
|
||||
# Invoke agent
|
||||
print()
|
||||
log_info("Sending message to agent...")
|
||||
print(f"{Fore.CYAN}You:{Style.RESET_ALL} {message}")
|
||||
print()
|
||||
|
||||
start_time = time.time()
|
||||
invoke_agent(
|
||||
runtime_arn=runtime_arn,
|
||||
region=outputs["region"],
|
||||
id_token=id_token,
|
||||
prompt=message,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
print()
|
||||
print(f"{Fore.CYAN}[Completed in {elapsed:.2f}s]{Style.RESET_ALL}")
|
||||
print()
|
||||
print(f"{Fore.GREEN}========================================{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN} Agent Test Complete! {Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}========================================{Style.RESET_ALL}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user