Files
tracer-cloud--opensre/tests/shared/infrastructure_sdk/resources/lambda_.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

423 lines
12 KiB
Python

"""Lambda function management."""
import io
import os
import shutil
import subprocess
import tempfile
import time
import zipfile
from pathlib import Path
from typing import Any
from botocore.exceptions import ClientError
from tests.shared.infrastructure_sdk.deployer import (
DEFAULT_REGION,
get_boto3_client,
get_standard_tags,
)
def bundle_code(source_dir: Path, requirements_file: Path | None = None) -> bytes:
"""Create deployment zip with dependencies.
Args:
source_dir: Directory containing Lambda code.
requirements_file: Optional path to requirements.txt.
Returns:
Zip file contents as bytes.
"""
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
package_dir = tmp_path / "package"
package_dir.mkdir()
# Install dependencies if requirements file provided
if requirements_file and requirements_file.exists():
subprocess.run(
[
"python3",
"-m",
"pip",
"install",
"-q",
"-r",
str(requirements_file),
"-t",
str(package_dir),
"--platform",
"manylinux2014_x86_64",
"--only-binary=:all:",
],
check=True,
capture_output=True,
)
# Copy source files
for item in source_dir.iterdir():
if item.is_file() and item.suffix == ".py":
shutil.copy2(item, package_dir)
elif item.is_dir() and not item.name.startswith("."):
shutil.copytree(item, package_dir / item.name)
# Create zip
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
for root, _dirs, files in os.walk(package_dir):
for file in files:
file_path = Path(root) / file
arcname = file_path.relative_to(package_dir)
zf.write(file_path, arcname)
return zip_buffer.getvalue()
def bundle_single_file(handler_file: Path, requirements_file: Path | None = None) -> bytes:
"""Create deployment zip from a single handler file.
Args:
handler_file: Path to the handler Python file.
requirements_file: Optional path to requirements.txt.
Returns:
Zip file contents as bytes.
"""
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
package_dir = tmp_path / "package"
package_dir.mkdir()
# Install dependencies if requirements file provided
if requirements_file and requirements_file.exists():
subprocess.run(
[
"python3",
"-m",
"pip",
"install",
"-q",
"-r",
str(requirements_file),
"-t",
str(package_dir),
"--platform",
"manylinux2014_x86_64",
"--only-binary=:all:",
],
check=True,
capture_output=True,
)
# Copy handler file
shutil.copy2(handler_file, package_dir)
# Create zip
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
for root, _dirs, files in os.walk(package_dir):
for file in files:
file_path = Path(root) / file
arcname = file_path.relative_to(package_dir)
zf.write(file_path, arcname)
return zip_buffer.getvalue()
def create_function(
name: str,
role_arn: str,
handler: str,
code_zip: bytes,
runtime: str = "python3.11",
timeout: int = 30,
memory: int = 128,
environment: dict[str, str] | None = None,
stack_name: str | None = None,
region: str = DEFAULT_REGION,
layers: list[str] | None = None,
) -> dict[str, Any]:
"""Create Lambda function.
Args:
name: Function name.
role_arn: ARN of the execution role.
handler: Handler specification (e.g., "handler.lambda_handler").
code_zip: Zip file bytes.
runtime: Python runtime version.
timeout: Function timeout in seconds.
memory: Memory allocation in MB.
environment: Environment variables.
stack_name: Stack name for tagging.
region: AWS region.
layers: Optional list of layer ARNs.
Returns:
Dictionary with function info: arn, name, version.
"""
lambda_client = get_boto3_client("lambda", region)
# Prepare configuration
config: dict[str, Any] = {
"FunctionName": name,
"Runtime": runtime,
"Role": role_arn,
"Handler": handler,
"Code": {"ZipFile": code_zip},
"Timeout": timeout,
"MemorySize": memory,
}
if environment:
config["Environment"] = {"Variables": environment}
if stack_name:
config["Tags"] = {t["Key"]: t["Value"] for t in get_standard_tags(stack_name)}
if layers:
config["Layers"] = layers
# Retry logic for IAM propagation delays
max_retries = 5
retry_delay = 10 # seconds
response: dict[str, Any] = {}
for attempt in range(max_retries):
try:
response = lambda_client.create_function(**config)
break
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code == "ResourceConflictException":
# Function exists, update it
update_function_code(name, code_zip, region)
return update_function_configuration(
name,
environment=environment,
timeout=timeout,
memory=memory,
region=region,
)
elif (
error_code == "InvalidParameterValueException"
and "cannot be assumed" in str(e)
and attempt < max_retries - 1
):
# IAM role not ready yet, retry
time.sleep(retry_delay)
continue
raise
# Wait for function to be active
_wait_for_function_active(name, lambda_client)
return {
"arn": response["FunctionArn"],
"name": response["FunctionName"],
"version": response.get("Version", "$LATEST"),
}
def update_function_code(
name: str, code_zip: bytes, region: str = DEFAULT_REGION
) -> dict[str, Any]:
"""Update existing function code.
Args:
name: Function name.
code_zip: Zip file bytes.
region: AWS region.
Returns:
Dictionary with function info: arn, name, version.
"""
lambda_client = get_boto3_client("lambda", region)
response = lambda_client.update_function_code(
FunctionName=name,
ZipFile=code_zip,
)
# Wait for update to complete
_wait_for_function_active(name, lambda_client)
return {
"arn": response["FunctionArn"],
"name": response["FunctionName"],
"version": response.get("Version", "$LATEST"),
}
def update_function_configuration(
name: str,
environment: dict[str, str] | None = None,
timeout: int | None = None,
memory: int | None = None,
region: str = DEFAULT_REGION,
) -> dict[str, Any]:
"""Update function configuration.
Args:
name: Function name.
environment: New environment variables (replaces existing).
timeout: New timeout in seconds.
memory: New memory in MB.
region: AWS region.
Returns:
Updated function info.
"""
lambda_client = get_boto3_client("lambda", region)
config: dict[str, Any] = {"FunctionName": name}
if environment is not None:
try:
current_config = lambda_client.get_function_configuration(FunctionName=name)
current_env = current_config.get("Environment", {}).get("Variables", {})
merged_env = {**current_env, **environment}
except ClientError:
merged_env = dict(environment)
config["Environment"] = {"Variables": merged_env}
if timeout is not None:
config["Timeout"] = timeout
if memory is not None:
config["MemorySize"] = memory
response = lambda_client.update_function_configuration(**config)
_wait_for_function_active(name, lambda_client)
return {
"arn": response["FunctionArn"],
"name": response["FunctionName"],
"version": response.get("Version", "$LATEST"),
}
def delete_function(name: str, region: str = DEFAULT_REGION) -> None:
"""Delete Lambda function.
Args:
name: Function name.
region: AWS region.
"""
lambda_client = get_boto3_client("lambda", region)
try:
lambda_client.delete_function(FunctionName=name)
except ClientError as e:
if e.response["Error"]["Code"] != "ResourceNotFoundException":
raise
def add_permission(
function_name: str,
statement_id: str,
principal: str,
source_arn: str | None = None,
region: str = DEFAULT_REGION,
) -> None:
"""Add invoke permission to Lambda.
Args:
function_name: Function name.
statement_id: Unique statement ID.
principal: Service principal (e.g., "apigateway.amazonaws.com").
source_arn: Optional source ARN for condition.
region: AWS region.
"""
lambda_client = get_boto3_client("lambda", region)
params: dict[str, Any] = {
"FunctionName": function_name,
"StatementId": statement_id,
"Action": "lambda:InvokeFunction",
"Principal": principal,
}
if source_arn:
params["SourceArn"] = source_arn
try:
lambda_client.add_permission(**params)
except ClientError as e:
if e.response["Error"]["Code"] == "ResourceConflictException":
pass # Permission already exists
else:
raise
def invoke_function(
function_name: str,
payload: dict[str, Any] | None = None,
region: str = DEFAULT_REGION,
) -> dict[str, Any]:
"""Invoke a Lambda function synchronously.
Args:
function_name: Function name.
payload: Request payload.
region: AWS region.
Returns:
Response payload.
"""
import json
lambda_client = get_boto3_client("lambda", region)
params: dict[str, Any] = {
"FunctionName": function_name,
"InvocationType": "RequestResponse",
}
if payload:
params["Payload"] = json.dumps(payload)
response = lambda_client.invoke(**params)
response_payload = response["Payload"].read()
if response_payload:
result: dict[str, Any] = json.loads(response_payload)
return result
return {}
def get_function(name: str, region: str = DEFAULT_REGION) -> dict[str, Any] | None:
"""Get function details.
Args:
name: Function name.
region: AWS region.
Returns:
Function configuration or None if not found.
"""
lambda_client = get_boto3_client("lambda", region)
try:
response = lambda_client.get_function(FunctionName=name)
config: dict[str, Any] = response["Configuration"]
return config
except ClientError as e:
if e.response["Error"]["Code"] == "ResourceNotFoundException":
return None
raise
def _wait_for_function_active(name: str, client: Any, max_attempts: int = 30) -> None:
"""Wait for function to be in Active state."""
for _ in range(max_attempts):
response = client.get_function(FunctionName=name)
state = response["Configuration"].get("State", "Active")
last_update_status = response["Configuration"].get("LastUpdateStatus", "Successful")
if state == "Active" and last_update_status in ("Successful", None):
return
time.sleep(2)
raise TimeoutError(f"Lambda function {name} did not become active")