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
268 lines
6.9 KiB
Python
268 lines
6.9 KiB
Python
"""ECR repository and image push."""
|
|
|
|
import subprocess
|
|
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 create_repository(name: str, stack_name: str, region: str = DEFAULT_REGION) -> dict[str, Any]:
|
|
"""Create ECR repository.
|
|
|
|
Args:
|
|
name: Repository name.
|
|
stack_name: Stack name for tagging.
|
|
region: AWS region.
|
|
|
|
Returns:
|
|
Dictionary with repository info: uri, arn, name.
|
|
"""
|
|
ecr_client = get_boto3_client("ecr", region)
|
|
|
|
try:
|
|
response = ecr_client.create_repository(
|
|
repositoryName=name,
|
|
imageScanningConfiguration={"scanOnPush": True},
|
|
imageTagMutability="MUTABLE",
|
|
tags=get_standard_tags(stack_name),
|
|
)
|
|
repo = response["repository"]
|
|
except ClientError as e:
|
|
if e.response["Error"]["Code"] == "RepositoryAlreadyExistsException":
|
|
response = ecr_client.describe_repositories(repositoryNames=[name])
|
|
repo = response["repositories"][0]
|
|
else:
|
|
raise
|
|
|
|
return {
|
|
"uri": repo["repositoryUri"],
|
|
"arn": repo["repositoryArn"],
|
|
"name": repo["repositoryName"],
|
|
}
|
|
|
|
|
|
def get_login_password(region: str = DEFAULT_REGION) -> str:
|
|
"""Get ECR login password for docker.
|
|
|
|
Args:
|
|
region: AWS region.
|
|
|
|
Returns:
|
|
Authorization token for docker login.
|
|
"""
|
|
ecr_client = get_boto3_client("ecr", region)
|
|
|
|
response = ecr_client.get_authorization_token()
|
|
auth_data = response["authorizationData"][0]
|
|
|
|
# Token is base64 encoded "AWS:password"
|
|
import base64
|
|
|
|
token = base64.b64decode(auth_data["authorizationToken"]).decode()
|
|
password = token.split(":")[1]
|
|
|
|
return password
|
|
|
|
|
|
def get_registry_url(region: str = DEFAULT_REGION) -> str:
|
|
"""Get ECR registry URL.
|
|
|
|
Args:
|
|
region: AWS region.
|
|
|
|
Returns:
|
|
Registry URL (e.g., 123456789.dkr.ecr.us-east-1.amazonaws.com).
|
|
"""
|
|
ecr_client = get_boto3_client("ecr", region)
|
|
|
|
response = ecr_client.get_authorization_token()
|
|
proxy_endpoint = response["authorizationData"][0]["proxyEndpoint"]
|
|
|
|
# Remove https:// prefix
|
|
return str(proxy_endpoint).replace("https://", "")
|
|
|
|
|
|
def docker_login(region: str = DEFAULT_REGION) -> None:
|
|
"""Perform docker login to ECR.
|
|
|
|
Args:
|
|
region: AWS region.
|
|
"""
|
|
password = get_login_password(region)
|
|
registry = get_registry_url(region)
|
|
|
|
subprocess.run(
|
|
["docker", "login", "-u", "AWS", "--password-stdin", registry],
|
|
input=password.encode(),
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
|
|
def build_and_push(
|
|
dockerfile_path: Path,
|
|
repository_uri: str,
|
|
tag: str = "latest",
|
|
platform: str = "linux/arm64",
|
|
build_args: dict[str, str] | None = None,
|
|
region: str = DEFAULT_REGION,
|
|
context_dir: Path | None = None,
|
|
) -> str:
|
|
"""Build and push Docker image.
|
|
|
|
Args:
|
|
dockerfile_path: Path to Dockerfile or directory containing it.
|
|
repository_uri: ECR repository URI.
|
|
tag: Image tag.
|
|
platform: Target platform (linux/arm64, linux/amd64).
|
|
build_args: Build arguments.
|
|
region: AWS region.
|
|
context_dir: Optional build context directory (defaults to Dockerfile parent).
|
|
|
|
Returns:
|
|
Full image URI with tag.
|
|
"""
|
|
# Ensure logged in
|
|
docker_login(region)
|
|
|
|
# Determine context directory and dockerfile path
|
|
if dockerfile_path.is_file():
|
|
dockerfile = str(dockerfile_path)
|
|
if context_dir is None:
|
|
context_dir = dockerfile_path.parent
|
|
else:
|
|
dockerfile = str(dockerfile_path / "Dockerfile")
|
|
if context_dir is None:
|
|
context_dir = dockerfile_path
|
|
|
|
full_uri = f"{repository_uri}:{tag}"
|
|
|
|
# Build command
|
|
cmd = [
|
|
"docker",
|
|
"build",
|
|
"--platform",
|
|
platform,
|
|
"-t",
|
|
full_uri,
|
|
"-f",
|
|
dockerfile,
|
|
]
|
|
|
|
if build_args:
|
|
for key, value in build_args.items():
|
|
cmd.extend(["--build-arg", f"{key}={value}"])
|
|
|
|
cmd.append(str(context_dir))
|
|
|
|
# Build
|
|
subprocess.run(cmd, check=True, capture_output=True)
|
|
|
|
# Push
|
|
subprocess.run(["docker", "push", full_uri], check=True, capture_output=True)
|
|
|
|
return full_uri
|
|
|
|
|
|
def delete_repository(name: str, region: str = DEFAULT_REGION) -> None:
|
|
"""Delete repository and all images.
|
|
|
|
Args:
|
|
name: Repository name.
|
|
region: AWS region.
|
|
"""
|
|
ecr_client = get_boto3_client("ecr", region)
|
|
|
|
try:
|
|
ecr_client.delete_repository(repositoryName=name, force=True)
|
|
except ClientError as e:
|
|
if e.response["Error"]["Code"] != "RepositoryNotFoundException":
|
|
raise
|
|
|
|
|
|
def get_repository(name: str, region: str = DEFAULT_REGION) -> dict[str, Any] | None:
|
|
"""Get repository details.
|
|
|
|
Args:
|
|
name: Repository name.
|
|
region: AWS region.
|
|
|
|
Returns:
|
|
Repository details or None if not found.
|
|
"""
|
|
ecr_client = get_boto3_client("ecr", region)
|
|
|
|
try:
|
|
response = ecr_client.describe_repositories(repositoryNames=[name])
|
|
if response["repositories"]:
|
|
repo = response["repositories"][0]
|
|
return {
|
|
"uri": repo["repositoryUri"],
|
|
"arn": repo["repositoryArn"],
|
|
"name": repo["repositoryName"],
|
|
}
|
|
return None
|
|
except ClientError as e:
|
|
if e.response["Error"]["Code"] == "RepositoryNotFoundException":
|
|
return None
|
|
raise
|
|
|
|
|
|
def list_images(repository_name: str, region: str = DEFAULT_REGION) -> list[dict[str, Any]]:
|
|
"""List images in a repository.
|
|
|
|
Args:
|
|
repository_name: Repository name.
|
|
region: AWS region.
|
|
|
|
Returns:
|
|
List of image details.
|
|
"""
|
|
ecr_client = get_boto3_client("ecr", region)
|
|
|
|
images = []
|
|
paginator = ecr_client.get_paginator("describe_images")
|
|
|
|
for page in paginator.paginate(repositoryName=repository_name):
|
|
for image in page.get("imageDetails", []):
|
|
images.append(
|
|
{
|
|
"digest": image.get("imageDigest"),
|
|
"tags": image.get("imageTags", []),
|
|
"pushed_at": image.get("imagePushedAt"),
|
|
"size_bytes": image.get("imageSizeInBytes"),
|
|
}
|
|
)
|
|
|
|
return images
|
|
|
|
|
|
def delete_images(
|
|
repository_name: str,
|
|
image_ids: list[dict[str, str]],
|
|
region: str = DEFAULT_REGION,
|
|
) -> None:
|
|
"""Delete specific images from repository.
|
|
|
|
Args:
|
|
repository_name: Repository name.
|
|
image_ids: List of image identifiers (imageDigest or imageTag).
|
|
region: AWS region.
|
|
"""
|
|
ecr_client = get_boto3_client("ecr", region)
|
|
|
|
if not image_ids:
|
|
return
|
|
|
|
ecr_client.batch_delete_image(
|
|
repositoryName=repository_name,
|
|
imageIds=image_ids,
|
|
)
|