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
215 lines
6.0 KiB
Python
215 lines
6.0 KiB
Python
"""S3 bucket creation and management."""
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from botocore.exceptions import ClientError
|
|
|
|
from tests.shared.infrastructure_sdk.deployer import (
|
|
DEFAULT_REGION,
|
|
get_boto3_client,
|
|
get_boto3_resource,
|
|
get_standard_tags,
|
|
)
|
|
|
|
|
|
def create_bucket(name: str, stack_name: str, region: str = DEFAULT_REGION) -> dict[str, Any]:
|
|
"""Create S3 bucket with tags.
|
|
|
|
Args:
|
|
name: Bucket name (must be globally unique).
|
|
stack_name: Stack name for tagging.
|
|
region: AWS region.
|
|
|
|
Returns:
|
|
Dictionary with bucket info: name, arn, region.
|
|
"""
|
|
s3_client = get_boto3_client("s3", region)
|
|
|
|
# Create bucket (LocationConstraint needed for non-us-east-1)
|
|
create_config: dict[str, Any] = {}
|
|
if region != "us-east-1":
|
|
create_config["CreateBucketConfiguration"] = {"LocationConstraint": region}
|
|
|
|
try:
|
|
s3_client.create_bucket(Bucket=name, **create_config)
|
|
except ClientError as e:
|
|
if e.response["Error"]["Code"] == "BucketAlreadyOwnedByYou":
|
|
pass # Bucket already exists, continue
|
|
else:
|
|
raise
|
|
|
|
# Add tags
|
|
s3_client.put_bucket_tagging(
|
|
Bucket=name,
|
|
Tagging={"TagSet": get_standard_tags(stack_name)},
|
|
)
|
|
|
|
# Block public access by default
|
|
s3_client.put_public_access_block(
|
|
Bucket=name,
|
|
PublicAccessBlockConfiguration={
|
|
"BlockPublicAcls": True,
|
|
"IgnorePublicAcls": True,
|
|
"BlockPublicPolicy": True,
|
|
"RestrictPublicBuckets": True,
|
|
},
|
|
)
|
|
|
|
return {
|
|
"name": name,
|
|
"arn": f"arn:aws:s3:::{name}",
|
|
"region": region,
|
|
}
|
|
|
|
|
|
def delete_bucket(name: str, region: str = DEFAULT_REGION) -> None:
|
|
"""Delete bucket and all objects.
|
|
|
|
Args:
|
|
name: Bucket name.
|
|
region: AWS region.
|
|
"""
|
|
s3_resource = get_boto3_resource("s3", region)
|
|
bucket = s3_resource.Bucket(name)
|
|
|
|
import contextlib
|
|
|
|
with contextlib.suppress(ClientError):
|
|
# Delete all objects (including versions)
|
|
bucket.object_versions.delete()
|
|
|
|
with contextlib.suppress(ClientError):
|
|
# Delete all objects (non-versioned)
|
|
bucket.objects.all().delete()
|
|
|
|
try:
|
|
bucket.delete()
|
|
except ClientError as e:
|
|
if e.response["Error"]["Code"] != "NoSuchBucket":
|
|
raise
|
|
|
|
|
|
def grant_read(bucket_name: str, role_arn: str, region: str = DEFAULT_REGION) -> None:
|
|
"""Add bucket policy for read access.
|
|
|
|
Args:
|
|
bucket_name: Bucket name.
|
|
role_arn: ARN of the IAM role to grant access.
|
|
region: AWS region.
|
|
"""
|
|
policy = {
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Sid": "AllowRead",
|
|
"Effect": "Allow",
|
|
"Principal": {"AWS": role_arn},
|
|
"Action": ["s3:GetObject", "s3:ListBucket"],
|
|
"Resource": [
|
|
f"arn:aws:s3:::{bucket_name}",
|
|
f"arn:aws:s3:::{bucket_name}/*",
|
|
],
|
|
}
|
|
],
|
|
}
|
|
_merge_bucket_policy(bucket_name, policy, region)
|
|
|
|
|
|
def grant_write(bucket_name: str, role_arn: str, region: str = DEFAULT_REGION) -> None:
|
|
"""Add bucket policy for write access.
|
|
|
|
Args:
|
|
bucket_name: Bucket name.
|
|
role_arn: ARN of the IAM role to grant access.
|
|
region: AWS region.
|
|
"""
|
|
policy = {
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Sid": "AllowWrite",
|
|
"Effect": "Allow",
|
|
"Principal": {"AWS": role_arn},
|
|
"Action": ["s3:PutObject", "s3:DeleteObject"],
|
|
"Resource": [f"arn:aws:s3:::{bucket_name}/*"],
|
|
}
|
|
],
|
|
}
|
|
_merge_bucket_policy(bucket_name, policy, region)
|
|
|
|
|
|
def grant_read_write(bucket_name: str, role_arn: str, region: str = DEFAULT_REGION) -> None:
|
|
"""Add bucket policy for full read/write access.
|
|
|
|
Args:
|
|
bucket_name: Bucket name.
|
|
role_arn: ARN of the IAM role to grant access.
|
|
region: AWS region.
|
|
"""
|
|
policy = {
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Sid": "AllowReadWrite",
|
|
"Effect": "Allow",
|
|
"Principal": {"AWS": role_arn},
|
|
"Action": [
|
|
"s3:GetObject",
|
|
"s3:PutObject",
|
|
"s3:DeleteObject",
|
|
"s3:ListBucket",
|
|
],
|
|
"Resource": [
|
|
f"arn:aws:s3:::{bucket_name}",
|
|
f"arn:aws:s3:::{bucket_name}/*",
|
|
],
|
|
}
|
|
],
|
|
}
|
|
_merge_bucket_policy(bucket_name, policy, region)
|
|
|
|
|
|
def _merge_bucket_policy(bucket_name: str, new_policy: dict[str, Any], region: str) -> None:
|
|
"""Merge a new policy with existing bucket policy."""
|
|
s3_client = get_boto3_client("s3", region)
|
|
|
|
# Get existing policy
|
|
try:
|
|
existing = s3_client.get_bucket_policy(Bucket=bucket_name)
|
|
existing_policy = json.loads(existing["Policy"])
|
|
except ClientError as e:
|
|
if e.response["Error"]["Code"] == "NoSuchBucketPolicy":
|
|
existing_policy = {"Version": "2012-10-17", "Statement": []}
|
|
else:
|
|
raise
|
|
|
|
# Merge statements
|
|
existing_sids = {s.get("Sid") for s in existing_policy.get("Statement", [])}
|
|
for statement in new_policy.get("Statement", []):
|
|
if statement.get("Sid") not in existing_sids:
|
|
existing_policy["Statement"].append(statement)
|
|
|
|
s3_client.put_bucket_policy(
|
|
Bucket=bucket_name,
|
|
Policy=json.dumps(existing_policy),
|
|
)
|
|
|
|
|
|
def bucket_exists(name: str, region: str = DEFAULT_REGION) -> bool:
|
|
"""Check if bucket exists.
|
|
|
|
Args:
|
|
name: Bucket name.
|
|
region: AWS region.
|
|
|
|
Returns:
|
|
True if bucket exists.
|
|
"""
|
|
s3_client = get_boto3_client("s3", region)
|
|
try:
|
|
s3_client.head_bucket(Bucket=name)
|
|
return True
|
|
except ClientError:
|
|
return False
|