diff --git a/.github/workflows/daily_image_backup.yml b/.github/workflows/daily_image_backup.yml new file mode 100644 index 0000000000..82f33b303b --- /dev/null +++ b/.github/workflows/daily_image_backup.yml @@ -0,0 +1,29 @@ +name: Daily Docker Image Backup + +on: + schedule: + - cron: '0 2 * * *' # Daily at 02:00 UTC + workflow_dispatch: # Allows manual triggering + +jobs: + backup_images: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install dependencies + run: pip install aiohttp + + - name: Run image backup script + run: python main.py + env: + TARGET_REGISTRY_URL: ${{ secrets.TARGET_REGISTRY_URL_SECRET }} + TARGET_NAMESPACE: ${{ secrets.TARGET_NAMESPACE_SECRET }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_SECRET }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_SECRET }} diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index e68af8e51f..f317d00acf 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -3,7 +3,7 @@ name: Docker on: workflow_dispatch: push: - branches: [ main ] + branches: [ master ] env: diff --git a/.github/workflows/tencent.yaml b/.github/workflows/tencent.yaml new file mode 100644 index 0000000000..73f0828a39 --- /dev/null +++ b/.github/workflows/tencent.yaml @@ -0,0 +1,42 @@ +name: tencent_docker +on: + workflow_dispatch: + push: + branches: [main] + +permissions: + contents: write + +env: + TENCENT_NAME_SPACE: "${{ secrets.TENCENT_NAME_SPACE }}" + TENCENT_REGISTRY_USER: "${{ secrets.TENCENT_REGISTRY_USER }}" + TENCENT_REGISTRY_PASSWORD: "${{ secrets.TENCENT_REGISTRY_PASSWORD }}" + +jobs: + build: + name: Pull + runs-on: ubuntu-latest + steps: + - name: Setup Docker buildx + uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf + + - name: Check out code + uses: actions/checkout@v2 + + - name: Build and push image TENCENT + run: | + docker login https://ccr.ccs.tencentyun.com --username=$TENCENT_REGISTRY_USER --password $TENCENT_REGISTRY_PASSWORD + while IFS= read -r line; do + [[ -z "$line" ]] && continue + echo "docker pull $line" + docker pull $line + # 获取镜像的完整名称,例如kasmweb/nginx:1.25.3(命名空间/镜像名:版本号) + image=$(echo "$line" | awk '{print $NF}') + # 获取 镜像名:版本号 例如nginx:1.25.3 + image_name_tag=$(echo "$image" | awk -F'/' '{print $NF}') + new_image="ccr.ccs.tencentyun.com/$TENCENT_NAME_SPACE/$image_name_tag" + echo "docker tag $image $new_image" + docker tag $image $new_image + echo "docker push $new_image" + docker push $new_image + done < images.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..c13101ff6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.lh +.vscode +*.pyc diff --git a/README.md b/README.md index 400d316a96..b00a300d59 100644 --- a/README.md +++ b/README.md @@ -50,3 +50,134 @@ docker pull registry.cn-hangzhou.aliyuncs.com/shrimp-images/alpine registry.cn-hangzhou.aliyuncs.com 即 ALIYUN_REGISTRY
shrimp-images 即 ALIYUN_NAME_SPACE
alpine 即images.txt里面填的镜像
+ +## 命令行脚本: `main.py` + +该脚本提供了一个命令行界面,用于将 Docker Hub 上的 Docker 镜像备份到指定的私有仓库。它会获取镜像标签,拉取镜像,为目标仓库重新标记镜像,推送它们,并记录已备份的镜像以避免重复操作。 + +### 先决条件 + +* Python 3.7+ (由于使用了 `asyncio` 和 `aiohttp`)。 +* 在脚本执行的机器上已安装并正在运行 Docker。 +* 可以访问 Docker Hub 的网络 (用于获取公共镜像)。 +* 拥有目标私有 Docker 仓库的凭据和访问权限 (例如,腾讯云 CCR、阿里云 ACR、Docker Hub 私有仓库等)。 + +### 依赖项 + +该脚本需要以下 Python 包: +* `aiohttp` + +您可以使用 pip 安装它: +```bash +pip install aiohttp +``` + +### 配置 + +配置通过环境变量管理凭据和目标仓库详细信息,通过命令行参数管理操作参数。 + +#### 环境变量 (必需) + +在运行脚本之前 **必须** 设置这些环境变量。如果缺少任何一个,脚本将退出。 + +* `TARGET_REGISTRY_URL`: 您的目标私有仓库的 URL。 + * 示例: `ccr.ccs.tencentyun.com` 或 `registry.aliyuncs.com` 或 `docker.io` (用于 Docker Hub)。 +* `TARGET_NAMESPACE`: 您的目标私有仓库中用于存储镜像的命名空间。 + * 示例: `my-docker-backups` 或 `myusername` (如果使用 Docker Hub 作为目标)。 +* `DOCKER_USERNAME`: 用于向 `TARGET_REGISTRY_URL` 进行身份验证的用户名。 +* `DOCKER_PASSWORD`: 用于向 `TARGET_REGISTRY_URL` 进行身份验证的密码。 + +#### 命令行参数 + +这些参数是可选的,用于控制脚本的执行: + +* `--num-tags` (`-n`): 为每个镜像获取和处理的最新标签数量。 + * 默认值: `5` +* `--record-file` (`-r`): 用于记录已备份镜像以防止重复处理的文件路径。 + * 默认值: `backed_up_images.txt` +* `--image-urls` (`-u`): 以逗号分隔的 Docker Hub 镜像 URL 字符串,用于指定要处理的镜像。 + * 示例: `"https://hub.docker.com/_/nginx/tags,https://hub.docker.com/r/prom/prometheus/tags"` + * 如果未提供,脚本将使用 `main.py` 中硬编码的默认镜像 URL 列表。 + +### 运行脚本 + +1. **设置环境变量:** + ```bash + export TARGET_REGISTRY_URL="your-registry.example.com" + export TARGET_NAMESPACE="your-namespace" + export DOCKER_USERNAME="your-registry-username" + export DOCKER_PASSWORD="your-registry-password" + ``` + +2. **执行 `main.py`:** + ```bash + python main.py [选项] + ``` + 例如,备份 `alpine` 和 `redis` 的最新3个标签: + ```bash + python main.py -n 3 -u "https://hub.docker.com/_/alpine/tags,https://hub.docker.com/_/redis/tags" + ``` + 使用默认镜像列表并为每个镜像获取5个标签: + ```bash + python main.py + ``` + + 脚本会将其进度记录到标准输出,包括获取标签、拉取、标记、推送镜像以及遇到的任何错误的信息。 + +### 工作原理 + +脚本执行以下步骤: + +1. **加载配置:** 读取环境变量并解析命令行参数。 +2. **Docker 登录:** 使用提供的凭据登录到目标 Docker 仓库。如果登录失败则退出。 +3. **镜像处理循环:** 对于指定的每个源镜像 URL: + a. **获取标签:** 使用其 API 从 Docker Hub 检索指定数量的最新标签。 + b. **标签处理循环:** 对于获取的每个标签: + i. **检查备份记录:** 查询记录文件 (例如 `backed_up_images.txt`),看是否已备份特定的镜像和标签组合。如果是,则跳到下一个标签。 + ii. **拉取 (Pull):** 如果未备份,则从 Docker Hub 拉取镜像 (例如 `nginx:latest`)。 + iii. **标记 (Tag):** 为目标私有仓库重新标记拉取的镜像 (例如 `your-registry.example.com/your-namespace/nginx:latest`)。 + iv. **推送 (Push):** 将新标记的镜像推送到目标私有仓库。 + v. **记录备份:** 如果所有先前的步骤 (拉取、标记、推送) 都成功,则将镜像和标签的条目添加到记录文件。 + +### 记录文件 + +* 记录文件 (默认: `backed_up_images.txt`) 存储已成功备份的镜像和标签组合的列表。 +* 文件中的每一行格式为: `镜像在Hub上的名称:标签` + * 示例: `nginx:1.25` 或 `prom/prometheus:v2.40.0` +* 该文件确保脚本不会重复处理已备份的镜像和标签,从而节省时间和资源。 + +### 错误处理 + +* 脚本使用 Python 的 `logging` 模块将所有操作 (包括错误) 记录到控制台。 +* 如果缺少关键配置 (环境变量) 或初始登录到目标 Docker 仓库失败,脚本将立即退出。 +* 对于在处理特定镜像或标签期间遇到的错误 (例如,拉取失败、推送失败),脚本将记录错误并尝试继续处理下一个标签或镜像 URL。 + +## 自动化每日备份工作流 (`.github/workflows/daily_image_backup.yml`) + +该仓库包含一个 GitHub Actions 工作流,可自动每日执行 `main.py` 脚本以备份 Docker 镜像。 + +### 特性 + +- **定时执行:** 每天 UTC 时间 02:00 自动运行。 +- **手动触发:** 也可以从 GitHub Actions 选项卡手动触发。 +- **安全配置:** 使用 GitHub Actions Secrets 存储敏感信息,如仓库凭据。 + +### 工作流配置 + +要使用此工作流,您需要在您的 GitHub 仓库设置中配置以下 Secrets (`Settings > Secrets and variables > Actions > New repository secret`): + +- **`TARGET_REGISTRY_URL_SECRET`**: 您的目标私有仓库的 URL (例如, `ccr.ccs.tencentyun.com` 或 `registry.aliyuncs.com`)。 +- **`TARGET_NAMESPACE_SECRET`**: 您的目标私有仓库中用于存储镜像的命名空间 (例如, `my-docker-images` 或 `my-project`)。 +- **`DOCKER_USERNAME_SECRET`**: 用于向您的目标私有仓库进行身份验证的用户名。 +- **`DOCKER_PASSWORD_SECRET`**: 用于向您的目标私有仓库进行身份验证的密码。 + +### 工作原理 + +该工作流执行以下步骤: + +1. **检出 (Checks out)** 仓库代码。 +2. **设置 (Sets up)** Python 3.9 环境。 +3. **安装 (Installs)** 所需的 Python 依赖 (`aiohttp`)。 +4. **执行 (Executes)** `main.py` 脚本,并将配置的 Secrets作为环境变量传递给脚本。然后,脚本按照 "命令行脚本: `main.py`" 部分所述处理镜像备份逻辑。 + +脚本执行的日志可以在此工作流的 GitHub Actions 运行历史中查看。 diff --git a/back_images.txt b/back_images.txt new file mode 100644 index 0000000000..1276452d5d --- /dev/null +++ b/back_images.txt @@ -0,0 +1,74 @@ +bitnami/kafka +jnovack/autossh +registry.k8s.io/pause:3.9 +registry.k8s.io/coredns/coredns:v1.11.1 +registry.k8s.io/etcd:3.5.10-0 +registry.k8s.io/kube-proxy:v1.29.2 +registry.k8s.io/kube-scheduler:v1.29.2 +registry.k8s.io/kube-controller-manager:v1.29.2 +registry.k8s.io/kube-apiserver:v1.29.2 +docker/desktop-kubernetes:kubernetes-v1.29.2-cni-v1.4.0-critools-v1.29.0-cri-dockerd-v0.3.11-1-debian +nginx +gcr.io/k8s-minikube/kicbase:v0.0.44 +alpine +busybox +httpd +mongo +ubuntu +node +postgres +memcached +golang +centos +php +mariadb +rabbitmq +elasticsearch +ruby +debian +tomcat:8 +tomcat +jenkins +kibana +neo4j +fedora +sentry +solr +rethinkdb +zookeeper +redmine +tomee +ubuntu-debootstra +ros +rust +scratch +python:alpine3.19 +mysql:5.7 +mysql:8 +redis +kibana:8.2.0 +neo4j +fedora +sentry +solr +rethinkdb +zookeeper +redmine +tomee +certbot/certbot + +eipwork/kuboard +gitlab/gitlab-ce:latest +gitlab/gitlab-runner:latest +certbot/certbot +ros +rust +eipwork/kuboard +gitlab/gitlab-ce:latest +gitlab/gitlab-runner:latest +certbot/certbot +grafana/grafana:11.2.0 +prom/prometheus:v2.54.1 +prom/node-exporter:v1.8.2 +nginx/nginx-ingress:3.6.2 +nginx/nginx-prometheus-exporter:1.3 diff --git a/images.txt b/images.txt index 9d47a4fdb5..ffcb163234 100644 --- a/images.txt +++ b/images.txt @@ -1,5 +1,3 @@ -alpine -python:alpine3.19 -kasmweb/nginx:1.25.3 ---platform linux/arm64 cooderl/wewe-rss-sqlite:latest - +rapidfort/etcd:3.5 +rapidfort/etcd:3.5.12 +rapidfort/etcd:3.5.9 diff --git a/main.py b/main.py new file mode 100644 index 0000000000..e79195499b --- /dev/null +++ b/main.py @@ -0,0 +1,285 @@ +import asyncio +from aiohttp import ClientSession +import json # 用于解析 JSON +from urllib.parse import urlparse # 用于解析 URL +import subprocess +import os +import argparse +import sys +import logging + +# Global list of image URLs, can be overridden by command-line argument +image_urls = [ + "https://hub.docker.com/_/nginx/tags", + "https://hub.docker.com/r/prom/prometheus/tags" +] + +def parse_dockerhub_url(image_url): + path_parts = [part for part in urlparse(image_url).path.split('/') if part] + if len(path_parts) >= 3 and path_parts[-1] == 'tags': + if path_parts[0] == '_': + namespace = "library" + repository = path_parts[1] + return namespace, repository + elif path_parts[0] == 'r' and len(path_parts) >= 4: + namespace = path_parts[1] + repository = path_parts[2] + return namespace, repository + logging.warning(f"Could not parse Docker Hub URL: {image_url}") + return None, None + + +def pull_image(image_name: str, tag: str) -> bool: + pull_command = f"docker pull {image_name}:{tag}" + logging.info(f"Pulling image: {image_name}:{tag} with command: '{pull_command}'") + try: + result = subprocess.run(pull_command, shell=True, check=True, capture_output=True, text=True) + logging.info(f"Successfully pulled {image_name}:{tag}") + # Stderr might contain useful info even on success (e.g. image up to date) + if result.stdout and result.stdout.strip(): + logging.debug(f"Pull stdout: {result.stdout.strip()}") + if result.stderr and result.stderr.strip(): + logging.debug(f"Pull stderr: {result.stderr.strip()}") + return True + except subprocess.CalledProcessError as e: + logging.error(f"Failed to pull {image_name}:{tag}. Command: '{e.cmd}' exited with code {e.returncode}") + if e.stderr: + logging.error(f"Stderr: {e.stderr.strip()}") + if e.stdout: + logging.error(f"Stdout: {e.stdout.strip()}") # Error because it might contain error messages + return False + except FileNotFoundError: + logging.error("Docker command not found. Please ensure Docker is installed and in PATH.") + return False + except Exception as e: + logging.exception(f"An unexpected error occurred while pulling {image_name}:{tag}: {e}") + return False + +def tag_image(original_image_name: str, original_tag: str, target_repo_url: str, new_tag: str) -> bool: + source_image_ref = f"{original_image_name}:{original_tag}" + target_image_ref = f"{target_repo_url}/{original_image_name}:{new_tag}" + tag_command = f"docker tag {source_image_ref} {target_image_ref}" + logging.info(f"Tagging image {source_image_ref} as {target_image_ref} with command: '{tag_command}'") + try: + result = subprocess.run(tag_command, shell=True, check=True, capture_output=True, text=True) + logging.info(f"Successfully tagged {source_image_ref} as {target_image_ref}") + if result.stdout and result.stdout.strip(): # Docker tag usually doesn't produce stdout + logging.debug(f"Tag stdout: {result.stdout.strip()}") + return True + except subprocess.CalledProcessError as e: + logging.error(f"Error tagging image. Command: '{e.cmd}' failed with exit code {e.returncode}") + if e.stderr: + logging.error(f"Stderr: {e.stderr.strip()}") + if e.stdout: + logging.error(f"Stdout: {e.stdout.strip()}") + return False + except FileNotFoundError: + logging.error("Docker command not found. Please ensure Docker is installed and in PATH.") + return False + except Exception as e: + logging.exception(f"An unexpected error occurred while tagging {source_image_ref}: {e}") + return False + +def docker_login(registry_url: str, username: str, password: str) -> bool: + login_command = f"docker login {registry_url} -u {username} --password-stdin" + logging.info(f"Attempting to login to {registry_url} as {username}...") + try: + result = subprocess.run(login_command, input=password, text=True, shell=True, check=True, capture_output=True) + # Docker login success message is often on stderr, or stdout depending on version/registry + # Checking result.stdout and result.stderr for "Login Succeeded" or similar messages is more robust + # For now, just log the attempt as successful if no error is raised. + logging.info(f"Docker login command to {registry_url} as {username} executed successfully.") + if result.stdout and result.stdout.strip(): + logging.info(f"Login stdout: {result.stdout.strip()}") + if result.stderr and result.stderr.strip(): # Often "Login Succeeded" is here + logging.info(f"Login stderr: {result.stderr.strip()}") + return True + except subprocess.CalledProcessError as e: + logging.error(f"Error during Docker login to {registry_url}. Command: '{e.cmd}' failed with exit code {e.returncode}") + if e.stderr: + logging.error(f"Stderr: {e.stderr.strip()}") + if e.stdout: + logging.error(f"Stdout: {e.stdout.strip()}") + return False + except FileNotFoundError: + logging.error("Docker command not found. Please ensure Docker is installed and in PATH.") + return False + except Exception as e: + logging.exception(f"An unexpected error occurred during Docker login to {registry_url}: {e}") + return False + +def push_image(full_image_reference: str) -> bool: + push_command = f"docker push {full_image_reference}" + logging.info(f"Pushing image: {full_image_reference} with command: '{push_command}'") + try: + result = subprocess.run(push_command, shell=True, check=True, capture_output=True, text=True) + logging.info(f"Successfully pushed {full_image_reference}") + if result.stdout and result.stdout.strip(): + logging.debug(f"Push stdout: {result.stdout.strip()}") + if result.stderr and result.stderr.strip(): # Push progress might also be on stderr + logging.debug(f"Push stderr: {result.stderr.strip()}") + return True + except subprocess.CalledProcessError as e: + logging.error(f"Error pushing image {full_image_reference}. Command: '{e.cmd}' failed with exit code {e.returncode}") + if e.stderr: + logging.error(f"Stderr: {e.stderr.strip()}") + if e.stdout: + logging.error(f"Stdout: {e.stdout.strip()}") + return False + except FileNotFoundError: + logging.error("Docker command not found. Please ensure Docker is installed and in PATH.") + return False + except Exception as e: + logging.exception(f"An unexpected error occurred while pushing {full_image_reference}: {e}") + return False + +def is_image_backed_up(image_name: str, tag: str, record_file: str = "backed_up_images.txt") -> bool: + record_to_check = f"{image_name}:{tag}" + try: + if not os.path.exists(record_file): + logging.debug(f"Record file {record_file} does not exist. Image {record_to_check} is not backed up.") + return False + with open(record_file, "r") as f: + for line in f: + if line.strip() == record_to_check: + logging.info(f"Image {record_to_check} already recorded as backed up in {record_file}.") + return True + logging.debug(f"Image {record_to_check} not found in {record_file}.") + return False + except IOError as e: + logging.error(f"Error reading record file {record_file}: {e}. Assuming not backed up.") + return False + +def record_backup(image_name: str, tag: str, record_file: str = "backed_up_images.txt") -> None: + record_to_add = f"{image_name}:{tag}" + try: + with open(record_file, "a") as f: + f.write(record_to_add + "\n") + logging.info(f"Successfully recorded {record_to_add} as backed up in {record_file}.") + except IOError as e: + logging.error(f"Error writing to record file {record_file}: {e}") + +async def get_tags_data(session, image_url, n=5): + namespace, repository = parse_dockerhub_url(image_url) + if not namespace or not repository: + # Error already logged by parse_dockerhub_url if it fails + return [] + + api_url = f"https://hub.docker.com/v2/repositories/{namespace}/{repository}/tags/?page_size={n}" + logging.debug(f"Fetching tags from: {api_url}") + + try: + async with session.get(api_url) as response: + response.raise_for_status() + data = await response.json() + tags = [] + if "results" in data and isinstance(data["results"], list): + for item in data["results"]: + if "name" in item: + tags.append({"repository": repository, "tag": item["name"]}) + return tags + except Exception as e: + logging.error(f"Error fetching or parsing tags for {image_url}: {e}", exc_info=True) + return [] + +async def main(): + logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(module)s - %(funcName)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + + parser = argparse.ArgumentParser(description="Backup Docker images from Docker Hub to a target registry.") + parser.add_argument("-n", "--num-tags", type=int, default=5, help="Number of latest tags to fetch per image. Default: 5") + parser.add_argument("-r", "--record-file", type=str, default="backed_up_images.txt", help="Path to the backup record file. Default: backed_up_images.txt") + parser.add_argument("-u", "--image-urls", type=str, help="Comma-separated string of Docker Hub URLs to process. Overrides the hardcoded list.") + + args = parser.parse_args() + + logging.info("--- Configuration Loading ---") + target_registry_url = os.getenv("TARGET_REGISTRY_URL") + target_namespace = os.getenv("TARGET_NAMESPACE") + docker_username = os.getenv("DOCKER_USERNAME") + docker_password = os.getenv("DOCKER_PASSWORD") + + if not all([target_registry_url, target_namespace, docker_username, docker_password]): + logging.error("Error: Missing one or more required environment variables: TARGET_REGISTRY_URL, TARGET_NAMESPACE, DOCKER_USERNAME, DOCKER_PASSWORD") + sys.exit(1) + + logging.info(f"Target Registry URL: {target_registry_url}") + logging.info(f"Target Namespace: {target_namespace}") + logging.info(f"Record File: {args.record_file}") + logging.info(f"Number of tags to fetch per image: {args.num_tags}") + + current_image_urls_to_process = image_urls + if args.image_urls: + current_image_urls_to_process = [url.strip() for url in args.image_urls.split(',')] + logging.info(f"Using provided image URLs: {current_image_urls_to_process}") + else: + logging.info(f"Using hardcoded image URLs: {current_image_urls_to_process}") + + logging.info("--- Docker Login ---") + if not docker_login(target_registry_url, docker_username, docker_password): + logging.error("Docker login failed. Exiting.") + sys.exit(1) + # Success message logged by docker_login or here if preferred + logging.info("Docker login process completed.") + + + logging.info("--- Starting Image Backup Process ---") + async with ClientSession() as session: + for source_image_url in current_image_urls_to_process: + logging.info(f"Processing URL: {source_image_url}") + source_hub_namespace, source_hub_repo_name = parse_dockerhub_url(source_image_url) + + if not source_hub_repo_name: + logging.warning(f"Failed to parse Docker Hub URL: {source_image_url}. Skipping.") + continue + + if source_hub_namespace == "library": + image_name_on_hub = source_hub_repo_name + else: + image_name_on_hub = f"{source_hub_namespace}/{source_hub_repo_name}" + + logging.info(f"Fetching tags for {image_name_on_hub} (up to {args.num_tags} tags)...") + tags_data = await get_tags_data(session, source_image_url, n=args.num_tags) + + if not tags_data: + logging.warning(f"No tags found or error fetching tags for {image_name_on_hub}. Skipping.") + continue + + logging.info(f"Found {len(tags_data)} tags for {image_name_on_hub}. Processing...") + + for image_info in tags_data: + current_tag = image_info["tag"] + logging.info(f"Processing tag: {image_name_on_hub}:{current_tag}") + + if is_image_backed_up(image_name_on_hub, current_tag, args.record_file): + # Message already logged by is_image_backed_up + continue + + logging.info(f"Attempting to pull {image_name_on_hub}:{current_tag}...") + if not pull_image(image_name_on_hub, current_tag): + logging.error(f"Pull failed for {image_name_on_hub}:{current_tag}. Skipping this tag.") + continue + + target_repo_base_for_tagging = f"{target_registry_url}/{target_namespace}" + logging.info(f"Attempting to tag {image_name_on_hub}:{current_tag} for {target_repo_base_for_tagging}...") + if not tag_image(original_image_name=image_name_on_hub, + original_tag=current_tag, + target_repo_url=target_repo_base_for_tagging, + new_tag=current_tag): + logging.error(f"Tagging failed for {image_name_on_hub}:{current_tag}. Skipping this tag.") + continue + + full_image_ref_for_push = f"{target_repo_base_for_tagging}/{image_name_on_hub}:{current_tag}" + logging.info(f"Attempting to push {full_image_ref_for_push}...") + if not push_image(full_image_ref_for_push): + logging.error(f"Push failed for {full_image_ref_for_push}. Skipping this tag.") + continue + + logging.info(f"Successfully pulled, tagged, and pushed {image_name_on_hub}:{current_tag} to {full_image_ref_for_push}") + record_backup(image_name_on_hub, current_tag, args.record_file) + + logging.info("--- Image Backup Process Completed ---") + +if __name__ == "__main__": + asyncio.run(main())