chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -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())
@@ -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()