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())