# ========================== # Universal Makefile (namespaced only) # Author: Sergey Antropov # https://devops.org.ru # ========================== # Замена пробелов на табы в консоли (фикс ошибок) # sed -i 's/^ /\t/' makefile # ---- Предустановленные значения под вашу сборку ---- REGISTRY ?= hub.cism-ms.ru/library IMAGE ?= TAG ?= latest CONTEXT ?= . DOCKERFILE ?= Dockerfile # ---- Режимы сборки ---- USE_BUILDX ?= 0 # 1 -> использовать docker buildx PLATFORMS ?= linux/amd64 # например: linux/amd64,linux/arm64 PUSH ?= 0 # 1 -> пушить образ в buildx (в рамках build) LOAD ?= 1 # 1 -> загрузить образ в локальный докер (buildx) NO_CACHE ?= 0 # 1 -> без кэша PULL ?= 0 # 1 -> принудительно тянуть base-образы QUIET ?= 0 # 1 -> тихий билд (минимум вывода) # ---- Build args ---- BUILD_ARGS ?= # строка с доп. флагами: --build-arg FOO=bar --build-arg BAZ=qux ARG_FILE ?= # путь к файлу KEY=VAL; будет превращён в --build-arg KEY=VAL # ---- Проверка контейнера ---- RUN_CMD ?= lsb_release -a # Что проверить после сборки (docker check) # ---- Buildx ---- BUILDER ?= universal-builder # ---- Логи ---- LOG_DIR ?= logs LOG_FILE ?= $(LOG_DIR)/build_$(shell date +%Y%m%d_%H%M%S).log # --- Настройки Git --- GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") GIT_REMOTE ?= origin GIT_MSG ?= # ====================== # OFFLINE LOAD/RETAG/PUSH # ====================== ARCHIVE ?= image.tar # путь к docker save архиву LOGIN ?= 1 # 1 -> docker login в REGISTRY перед push PUSH_OFFLINE ?= 1 # 1 -> docker push после load KEEP_SRC_TAG ?= 1 # 1 -> если IMAGE пуст, взять тег из архива OFFLINE_DIR ?= .offline LOAD_LOG := $(OFFLINE_DIR)/load.log SRC_FILE := $(OFFLINE_DIR)/src_image.txt DST_FILE := $(OFFLINE_DIR)/dst_image.txt LAST_SAVE := $(OFFLINE_DIR)/last_save.txt # ====================== # Universal save (local) # ====================== IMAGES_DIR ?= images # куда сохраняем архивы SRC_IMAGE ?= # образ-источник; если пусто — $(FULL_IMAGE) COMPRESS ?= 0 # 1 -> gzip архив SPLIT_SIZE ?= # размер чанка, напр.: 2G (пусто = не резать) # ---- Полное имя образа ---- ifneq ($(strip $(REGISTRY)),) FULL_IMAGE := $(REGISTRY)/$(IMAGE):$(TAG) else FULL_IMAGE := $(IMAGE):$(TAG) endif # ---- Цвета ---- COLOR_RESET := \033[0m COLOR_INFO := \033[1;34m COLOR_OK := \033[1;32m COLOR_ERR := \033[1;31m COLOR_WARN := \033[1;33m # ---- Флаги сборки ---- ifeq ($(NO_CACHE),1) CACHE_FLAG := --no-cache else CACHE_FLAG := endif ifeq ($(PULL),1) PULL_FLAG := --pull else PULL_FLAG := endif ifeq ($(QUIET),1) QUIET_FLAG := --quiet else QUIET_FLAG := endif # Аргументы из файла KEY=VALUE -> --build-arg KEY=VALUE ifneq ($(strip $(ARG_FILE)),) ARG_FILE_FLAGS := $(shell sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=\(.*\)/--build-arg \1=\2/p' $(ARG_FILE)) else ARG_FILE_FLAGS := endif # ====================== # Help # ====================== .PHONY: help help: @echo "Namespaces:" @echo " make docker [build|check|push|save|load|load-raw|inspect|retag|create-builder|use-builder|login|clean|print-config]" @echo " make git [status|pull|commit|push|sync]" @echo "" @echo "Key vars (override via make VAR=value):" @echo " REGISTRY=$(REGISTRY) IMAGE=$(IMAGE) TAG=$(TAG) CONTEXT=$(CONTEXT) DOCKERFILE=$(DOCKERFILE)" @echo " USE_BUILDX=$(USE_BUILDX) PLATFORMS=$(PLATFORMS) PUSH=$(PUSH) LOAD=$(LOAD) NO_CACHE=$(NO_CACHE) PULL=$(PULL) QUIET=$(QUIET)" @echo " BUILD_ARGS=$(BUILD_ARGS) ARG_FILE=$(ARG_FILE) RUN_CMD=$(RUN_CMD)" @echo " ARCHIVE=$(ARCHIVE) LOGIN=$(LOGIN) PUSH_OFFLINE=$(PUSH_OFFLINE) KEEP_SRC_TAG=$(KEEP_SRC_TAG)" @echo " IMAGES_DIR=$(IMAGES_DIR)" @echo "" @echo "Examples:" @echo " make docker build IMAGE=myapp TAG=1.0.0" @echo " make docker push REGISTRY=registry.local/library IMAGE=myapp TAG=1.0.0" @echo " make docker save IMAGE=myapp TAG=1.0.0 COMPRESS=1" @echo " make docker load ARCHIVE=images/myapp_1.0.0.tar.gz LOGIN=1 PUSH_OFFLINE=1" @echo " make git commit # спросит сообщение" @echo " make git push # commit+push с запросом сообщения" # ====================== # Служебные директории # ====================== $(LOG_DIR): @mkdir -p $(LOG_DIR) $(IMAGES_DIR): @mkdir -p $(IMAGES_DIR) $(OFFLINE_DIR): @mkdir -p $(OFFLINE_DIR) # ====================== # Docker: реализация # ====================== # Внутренние цели (не вызывать напрямую) .PHONY: __docker_build __docker_check __docker_push __docker_login __docker_retag __docker_save __docker_load __docker_load_raw __docker_inspect __docker_create_builder __docker_use_builder __docker_clean __docker_print_config # --- Команды сборки --- define DOCKER_BUILD_CLASSIC docker build $(CACHE_FLAG) $(PULL_FLAG) $(QUIET_FLAG) \ -f $(DOCKERFILE) -t "$(FULL_IMAGE)" \ $(BUILD_ARGS) $(ARG_FILE_FLAGS) \ "$(CONTEXT)" endef define DOCKER_BUILD_BUILDX docker buildx build $(CACHE_FLAG) $(PULL_FLAG) $(QUIET_FLAG) \ $(if $(filter 1,$(PUSH)),--push,) \ $(if $(filter 1,$(LOAD)),--load,) \ --platform $(PLATFORMS) \ -f $(DOCKERFILE) -t "$(FULL_IMAGE)" \ $(BUILD_ARGS) $(ARG_FILE_FLAGS) \ "$(CONTEXT)" endef __docker_build: $(LOG_DIR) @echo "$(COLOR_INFO)[INFO] Build: $(FULL_IMAGE)$(COLOR_RESET)" @echo "$(COLOR_INFO)[INFO] Log: $(LOG_FILE)$(COLOR_RESET)" @{ \ echo "=== Build started: $$(date) ==="; \ if [ "$(USE_BUILDX)" = "1" ]; then \ $(DOCKER_BUILD_BUILDX); \ else \ $(DOCKER_BUILD_CLASSIC); \ fi; \ echo "=== Build finished: $$(date) ==="; \ } | tee $(LOG_FILE) @echo "$(COLOR_OK)[OK] Build success$(COLOR_RESET)" __docker_check: @echo "$(COLOR_INFO)[INFO] Check (RUN_CMD) for $(FULL_IMAGE): $(RUN_CMD)$(COLOR_RESET)" @docker run --rm --entrypoint /bin/sh "$(FULL_IMAGE)" -lc "$(RUN_CMD)" __docker_push: @if [ "$(USE_BUILDX)" != "1" ]; then \ echo "$(COLOR_INFO)[INFO] Pushing: $(FULL_IMAGE)$(COLOR_RESET)"; \ docker push "$(FULL_IMAGE)"; \ else \ echo "$(COLOR_WARN)[WARN] USE_BUILDX=1: push выполняется при buildx (PUSH=1).$(COLOR_RESET)"; \ echo "$(COLOR_WARN)[WARN] Пересоберите с PUSH=1 или выполните docker push вручную.$(COLOR_RESET)"; \ fi @echo "$(COLOR_OK)[OK] Push done$(COLOR_RESET)" __docker_login: @if [ -z "$(REGISTRY)" ]; then \ echo "$(COLOR_ERR)[ERR] REGISTRY не задан. Пример: make docker login REGISTRY=registry.local$(COLOR_RESET)"; \ exit 1; \ fi @echo "$(COLOR_INFO)[INFO] docker login $(REGISTRY)$(COLOR_RESET)" @docker login $(REGISTRY) __docker_retag: @if [ -z "$(NEW_TAG)" ]; then \ echo "$(COLOR_ERR)[ERR] Укажи NEW_TAG: make docker retag NEW_TAG=vX.Y.Z$(COLOR_RESET)"; \ exit 1; \ fi @echo "$(COLOR_INFO)[INFO] Retag: $(FULL_IMAGE) -> $(IMAGE):$(NEW_TAG)$(COLOR_RESET)" @docker tag "$(FULL_IMAGE)" "$(IMAGE):$(NEW_TAG)" @echo "$(COLOR_OK)[OK] Retag done$(COLOR_RESET)" __docker_save: | $(IMAGES_DIR) $(OFFLINE_DIR) @src="$(SRC_IMAGE)"; \ if [ -z "$$src" ]; then \ if [ -n "$(IMAGE)" ]; then src="$(FULL_IMAGE)"; else echo "$(COLOR_ERR)[ERR] Укажи SRC_IMAGE или IMAGE/TAG$(COLOR_RESET)"; exit 1; fi; \ fi; \ docker image inspect "$$src" >/dev/null 2>&1 || { echo "$(COLOR_ERR)[ERR] Образ не найден: $$src$(COLOR_RESET)"; exit 1; }; \ safe="$${src//\//_}"; safe="$${safe//:/_}"; out="$(IMAGES_DIR)/$$safe.tar"; \ echo "$(COLOR_INFO)[INFO] Сохранение $$src -> $$out$(COLOR_RESET)"; \ docker save -o "$$out" "$$src"; \ if [ "$(COMPRESS)" = "1" ]; then \ echo "$(COLOR_INFO)[INFO] Gzip $$out$(COLOR_RESET)"; gzip -f "$$out"; out="$$out.gz"; \ fi; \ if [ -n "$(SPLIT_SIZE)" ]; then \ echo "$(COLOR_INFO)[INFO] Резка архива на чанки по $(SPLIT_SIZE)$(COLOR_RESET)"; \ split -b $(SPLIT_SIZE) -d -a 3 "$$out" "$$out.part."; \ fi; \ { sha256sum "$$out" 2>/dev/null || shasum -a 256 "$$out"; } > "$$out.sha256" 2>/dev/null || true; \ echo "$$out" > "$(LAST_SAVE)"; \ echo "$(COLOR_OK)[OK] Образ сохранён: $$out$(COLOR_RESET)"; \ ls -lh "$$out"* __docker_load_raw: @if [ -z "$(FILE)" ]; then \ echo "$(COLOR_ERR)[ERR] Укажи FILE: make docker load-raw FILE=img.tar$(COLOR_RESET)"; \ exit 1; \ fi @echo "$(COLOR_INFO)[INFO] Loading image from $(FILE)$(COLOR_RESET)" @docker load -i "$(FILE)" @echo "$(COLOR_OK)[OK] Load done$(COLOR_RESET)" __docker_load: | $(OFFLINE_DIR) @if [ ! -f "$(ARCHIVE)" ]; then \ echo "$(COLOR_ERR)[ERR] Не найден архив: $(ARCHIVE) (ARCHIVE=... )$(COLOR_RESET)"; exit 1; \ fi @echo "$(COLOR_INFO)[INFO] docker load -i $(ARCHIVE)$(COLOR_RESET)" @docker load -i "$(ARCHIVE)" | tee "$(LOAD_LOG)" @awk '/Loaded image:/ { last=$$0 } END { gsub(/^.*Loaded image: /,"",last); print last }' "$(LOAD_LOG)" > "$(SRC_FILE)" @src="$$(cat "$(SRC_FILE)" 2>/dev/null || true)"; \ if [ -z "$$src" ]; then \ echo "$(COLOR_ERR)[ERR] Не удалось определить repo:tag из docker load. Укажи IMAGE/TAG вручную.$(COLOR_RESET)"; \ exit 1; \ fi; \ echo "$(COLOR_OK)[OK] Загружен исходный образ: $$src$(COLOR_RESET)"; \ repo="$${src%:*}"; srctag="$${src#*:}"; [ "$$repo" = "$$srctag" ] && srctag="latest"; \ src_name="$${repo##*/}"; \ dst_image="$(IMAGE)"; [ -z "$$dst_image" ] && dst_image="$$src_name"; \ if [ -z "$(IMAGE)" ] && [ "$(KEEP_SRC_TAG)" = "1" ]; then dst_tag="$$srctag"; else dst_tag="$(TAG)"; fi; \ if [ -z "$(REGISTRY)" ]; then echo "$(COLOR_ERR)[ERR] REGISTRY пуст$(COLOR_RESET)"; exit 1; fi; \ full_dst="$(REGISTRY)/$$dst_image:$$dst_tag"; echo "$$full_dst" > "$(DST_FILE)"; \ echo "$(COLOR_INFO)[INFO] Тегируем: $$src -> $$full_dst$(COLOR_RESET)"; \ docker tag "$$src" "$$full_dst"; \ if [ "$(LOGIN)" = "1" ]; then echo "$(COLOR_INFO)[INFO] docker login $(REGISTRY)$(COLOR_RESET)"; docker login "$(REGISTRY)"; fi; \ if [ "$(PUSH_OFFLINE)" = "1" ]; then echo "$(COLOR_INFO)[INFO] docker push $$full_dst$(COLOR_RESET)"; docker push "$$full_dst"; echo "$(COLOR_OK)[OK] Пуш завершён$(COLOR_RESET)"; else echo "$(COLOR_WARN)[WARN] PUSH_OFFLINE=0: пуш пропущен$(COLOR_RESET)"; fi __docker_inspect: @docker image inspect "$(FULL_IMAGE)" || true __docker_create_builder: @docker buildx create --name $(BUILDER) --use --bootstrap || docker buildx use $(BUILDER) @echo "$(COLOR_OK)[OK] Builder ready: $(BUILDER)$(COLOR_RESET)" __docker_use_builder: @docker buildx use $(BUILDER) @docker buildx inspect --bootstrap @echo "$(COLOR_OK)[OK] Using builder: $(BUILDER)$(COLOR_RESET)" __docker_clean: @echo "$(COLOR_WARN)[WARN] Removing logs and .offline$(COLOR_RESET)" @rm -rf "$(LOG_DIR)" "$(OFFLINE_DIR)" @echo "$(COLOR_OK)[OK] Clean done$(COLOR_RESET)" __docker_print_config: @echo "FULL_IMAGE : $(FULL_IMAGE)" @echo "REGISTRY : $(REGISTRY)" @echo "IMAGE : $(IMAGE)" @echo "TAG : $(TAG)" @echo "CONTEXT : $(CONTEXT)" @echo "DOCKERFILE : $(DOCKERFILE)" @echo "USE_BUILDX : $(USE_BUILDX)" @echo "PLATFORMS : $(PLATFORMS)" @echo "PUSH : $(PUSH)" @echo "LOAD : $(LOAD)" @echo "NO_CACHE : $(NO_CACHE)" @echo "PULL : $(PULL)" @echo "QUIET : $(QUIET)" @echo "BUILD_ARGS : $(BUILD_ARGS)" @echo "ARG_FILE : $(ARG_FILE)" @echo "RUN_CMD : $(RUN_CMD)" @echo "BUILDER : $(BUILDER)" @echo "LOG_DIR : $(LOG_DIR)" @echo "LOG_FILE : $(LOG_FILE)" # ====================== # Docker dispatcher # ====================== .PHONY: docker docker: @sub="$(word 2,$(MAKECMDGOALS))"; \ case "$$sub" in \ build) $(MAKE) __docker_build ;; \ check) $(MAKE) __docker_check ;; \ push) $(MAKE) __docker_build __docker_check __docker_push ;; \ save) $(MAKE) __docker_save ;; \ load) $(MAKE) __docker_load ;; \ load-raw) $(MAKE) __docker_load_raw ;; \ inspect) $(MAKE) __docker_inspect ;; \ retag) $(MAKE) __docker_retag ;; \ create-builder) $(MAKE) __docker_create_builder ;; \ use-builder) $(MAKE) __docker_use_builder ;; \ login) $(MAKE) __docker_login ;; \ clean) $(MAKE) __docker_clean ;; \ print-config) $(MAKE) __docker_print_config ;; \ "") \ echo "$(COLOR_INFO)[INFO] Docker commands:$(COLOR_RESET)"; \ echo " make docker build — собрать образ"; \ echo " make docker check — проверить контейнер (RUN_CMD)"; \ echo " make docker push — build + check + push"; \ echo " make docker save — сохранить образ в ./images/"; \ echo " make docker load — загрузить из архива и (опц.) запушить"; \ echo " make docker load-raw — чистый docker load -i FILE"; \ echo " make docker inspect — docker image inspect"; \ echo " make docker retag — перетегировать локальный образ"; \ echo " make docker create-builder — создать buildx builder"; \ echo " make docker use-builder — выбрать buildx builder"; \ echo " make docker login — docker login в REGISTRY"; \ echo " make docker clean — удалить логи и .offline"; \ echo " make docker print-config — показать конфигурацию"; \ ;; \ *) echo "$(COLOR_ERR)[ERR] Unknown docker subcommand: $$sub$(COLOR_RESET)"; exit 1 ;; \ esac # ====================== # Git: реализация # ====================== .PHONY: __git_status __git_pull __git_commit __git_push __git_sync __git_status: @echo "$(COLOR_INFO)[INFO] Git status ($(GIT_BRANCH))$(COLOR_RESET)" @git status -sb || echo "$(COLOR_WARN)[WARN] Not a git repository$(COLOR_RESET)" __git_pull: @echo "$(COLOR_INFO)[INFO] Pulling from $(GIT_REMOTE)/$(GIT_BRANCH)$(COLOR_RESET)" @git pull $(GIT_REMOTE) $(GIT_BRANCH) || echo "$(COLOR_ERR)[ERR] git pull failed$(COLOR_RESET)" __git_commit: @echo "$(COLOR_INFO)[INFO] Preparing commit$(COLOR_RESET)" @if [ -z "$(GIT_MSG)" ]; then \ read -p "Введите сообщение коммита: " msg; \ GIT_MSG="$$msg"; \ else \ GIT_MSG="$(GIT_MSG)"; \ fi; \ git add -A; \ if git diff --cached --quiet; then \ echo "$(COLOR_WARN)[WARN] Нет изменений для коммита$(COLOR_RESET)"; \ else \ git commit -m "$$GIT_MSG" && echo "$(COLOR_OK)[OK] Commit created: $$GIT_MSG$(COLOR_RESET)"; \ fi __git_push: @echo "$(COLOR_INFO)[INFO] Preparing push for $(GIT_BRANCH)$(COLOR_RESET)" @$(MAKE) __git_commit @echo "$(COLOR_INFO)[INFO] Pushing to $(GIT_REMOTE)/$(GIT_BRANCH)$(COLOR_RESET)" @git push $(GIT_REMOTE) $(GIT_BRANCH) || echo "$(COLOR_ERR)[ERR] git push failed$(COLOR_RESET)" __git_sync: @echo "$(COLOR_INFO)[INFO] Git sync (pull + commit + push)$(COLOR_RESET)" @$(MAKE) __git_pull @$(MAKE) __git_push @echo "$(COLOR_OK)[OK] Git sync complete for branch $(GIT_BRANCH)$(COLOR_RESET)" # ====================== # Git dispatcher # ====================== .PHONY: git git: @sub="$(word 2,$(MAKECMDGOALS))"; \ case "$$sub" in \ status) $(MAKE) __git_status ;; \ pull) $(MAKE) __git_pull ;; \ commit) $(MAKE) __git_commit ;; \ push) $(MAKE) __git_push ;; \ sync) $(MAKE) __git_sync ;; \ "") \ echo "$(COLOR_INFO)[INFO] Git commands:$(COLOR_RESET)"; \ echo " make git status — показать статус"; \ echo " make git pull — получить изменения"; \ echo " make git commit — коммит (со вводом сообщения)"; \ echo " make git push — commit + push (со вводом сообщения)"; \ echo " make git sync — pull + commit + push"; \ ;; \ *) echo "$(COLOR_ERR)[ERR] Unknown git subcommand: $$sub$(COLOR_RESET)"; exit 1 ;; \ esac .PHONY: status pull commit push sync \ build check save load load-raw inspect retag create-builder use-builder login clean print-config status pull commit push sync \ build check save load load-raw inspect retag create-builder use-builder login clean print-config: @: