143 lines
5.9 KiB
Python
143 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Скрипт публикации Docker-образа (multi-arch) в реестр.
|
||
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
|
||
Поведение:
|
||
- Собирает multi-arch образ с использованием docker buildx
|
||
- Логинится в реестр (пароль можно передать через аргумент/ENV, либо будет запрошен интерактивно)
|
||
- Публикует образ
|
||
|
||
Параметры через аргументы/ENV:
|
||
- --registry / $REGISTRY_HOST (например, docker.io, ghcr.io)
|
||
- --image / $IMAGE_NAME_FULL (например, inecs/logboard)
|
||
- --tag / $IMAGE_TAG (например, v1 или короткий sha)
|
||
- --user / $REG_USER
|
||
- --password / $REG_PASS
|
||
- --platforms / $PLATFORMS (по умолчанию: linux/amd64,linux/arm64)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import getpass
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from typing import List
|
||
|
||
|
||
def run(cmd: List[str], check: bool = True, quiet: bool = False) -> subprocess.CompletedProcess:
|
||
stdout = subprocess.DEVNULL if quiet else None
|
||
stderr = subprocess.STDOUT if quiet else None
|
||
return subprocess.run(cmd, check=check, stdout=stdout, stderr=stderr)
|
||
|
||
|
||
def git_short_sha_default() -> str:
|
||
try:
|
||
out = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL)
|
||
return out.decode("utf-8").strip() or "latest"
|
||
except Exception:
|
||
return "latest"
|
||
|
||
|
||
def ensure_buildx_builder(builder_name: str) -> None:
|
||
try:
|
||
run(["docker", "buildx", "inspect", builder_name], check=True, quiet=True)
|
||
except subprocess.CalledProcessError:
|
||
run(["docker", "buildx", "create", "--name", builder_name, "--use"], check=True)
|
||
run(["docker", "buildx", "use", builder_name], check=True)
|
||
|
||
|
||
def ensure_binfmt() -> None:
|
||
try:
|
||
run(["docker", "run", "--privileged", "--rm", "tonistiigi/binfmt", "--install", "all"], check=False, quiet=True)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="Публикация Docker-образа в реестр (multi-arch)")
|
||
parser.add_argument("--registry", default=os.getenv("REGISTRY_HOST", ""))
|
||
parser.add_argument("--image", dest="image", default=os.getenv("IMAGE_NAME_FULL", ""))
|
||
parser.add_argument("--tag", dest="tag", default=os.getenv("IMAGE_TAG", ""))
|
||
parser.add_argument("--user", dest="user", default=os.getenv("REG_USER", ""))
|
||
parser.add_argument("--password", dest="password", default=os.getenv("REG_PASS", ""))
|
||
parser.add_argument("--platforms", default=os.getenv("PLATFORMS", "linux/amd64,linux/arm64"))
|
||
parser.add_argument("--builder", default=os.getenv("BUILDX_BUILDER", "logboard_builder"))
|
||
args = parser.parse_args()
|
||
|
||
# 1) Определяем реестр, поддерживая ввод вида "host" или "host/namespace"
|
||
registry_input = (args.registry or os.getenv("REGISTRY_HOST", "")).strip()
|
||
if not registry_input:
|
||
registry_input = input(
|
||
"Введите Docker Registry (например, ghcr.io, docker.io, registry.hub.docker.com/namespace): "
|
||
).strip()
|
||
registry_host = registry_input.split("/", 1)[0] if registry_input else "docker.io"
|
||
registry_path = ""
|
||
if "/" in registry_input:
|
||
registry_path = registry_input.split("/", 1)[1]
|
||
if not registry_host or registry_host == "registry.hub.docker.com":
|
||
registry_host = "docker.io"
|
||
|
||
# 2) Имя образа. Если не задано, спрашиваем пользователя (по умолчанию logboard)
|
||
image_input = (args.image or os.getenv("IMAGE_NAME_FULL", "")).strip()
|
||
if not image_input:
|
||
image_input = input(
|
||
"Введите имя образа (по умолчанию: logboard): "
|
||
).strip() or "logboard"
|
||
# Если пользователь ввёл только имя без namespace, но registry_path задан, добавим префикс
|
||
if "/" not in image_input and registry_path:
|
||
image_full = f"{registry_path}/{image_input}"
|
||
else:
|
||
image_full = image_input
|
||
|
||
# 3) Тег: если не задан, спросим. По умолчанию короткий SHA
|
||
tag_input = (args.tag or os.getenv("IMAGE_TAG", "")).strip()
|
||
if not tag_input:
|
||
default_tag = git_short_sha_default()
|
||
tag_input = input(f"Введите тег образа (по умолчанию: {default_tag}): ").strip() or default_tag
|
||
|
||
user = (args.user or os.getenv("REG_USER", "")).strip()
|
||
password = args.password
|
||
if not user:
|
||
user = input("Введите имя пользователя реестра: ").strip()
|
||
if not password:
|
||
password = getpass.getpass("Введите пароль реестра: ")
|
||
|
||
image_remote = f"{registry_host}/{image_full}:{tag_input}"
|
||
|
||
print(f"Логин в реестр: {registry_host}")
|
||
p = subprocess.Popen(["docker", "login", registry_host, "-u", user, "--password-stdin"], stdin=subprocess.PIPE)
|
||
p.communicate(input=password.encode("utf-8"))
|
||
if p.returncode != 0:
|
||
print("Ошибка логина в реестр", file=sys.stderr)
|
||
return 1
|
||
|
||
print("Установка binfmt (для multi-arch)...")
|
||
ensure_binfmt()
|
||
print(f"Подготовка buildx builder: {args.builder}")
|
||
ensure_buildx_builder(args.builder)
|
||
|
||
print(f"Сборка и публикация образа: {image_remote}")
|
||
run([
|
||
"docker", "buildx", "build",
|
||
"--platform", args.platforms,
|
||
"-t", image_remote,
|
||
"--push",
|
||
".",
|
||
], check=True)
|
||
|
||
print("Образ опубликован успешно!")
|
||
print("Готово. Для генерации compose выполните: make release-compose")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|
||
|
||
|