Files
iptv-docker/iptv
2025-05-06 11:36:10 +08:00

1419 lines
54 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
##########################################################################################
# Скрипт управления проектом iptv.axenov.dev
#
# Copyright (c) 2025 Антон Аксенов <anthonyaxenov@gmail.com>
# MIT License, see LICENSE file for more info.
##########################################################################################
# shellcheck disable=SC2015,SC2103,SC2164,SC2155
set -o pipefail
########################################################
# Служебные исходные переменные
########################################################
if [[ "${BASH_SOURCE[*]}" ]]; then
[[ "${BASH_SOURCE[0]}" != "$0" ]] && echo "*** Команда source iptv запрещена ***" && exit 40
ROOT_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
else
# script running from stdin
ROOT_PATH="$(pwd)"
fi
IPTV_GITEA_URL_SSH="git@git.axenov.dev:IPTV"
IPTV_GITEA_URL_HTTPS="https://git.axenov.dev/IPTV"
IPTV_DOCKER_URL_SSH="$IPTV_GITEA_URL_SSH/docker.git"
IPTV_DOXYGEN_OUTPUT="$ROOT_PATH/docs/doxygen"
for sig in SIGHUP SIGINT SIGQUIT SIGABRT SIGKILL SIGTERM SIGTSTP; do
# https://faculty.cs.niu.edu/~hutchins/csci480/signals.htm
# shellcheck disable=SC2064
trap "set +x && echo && echo && echo '*** Прервано сигналом $sig, остановка ***' && exit" $sig
done
[[ $IPTV_DEBUG == 1 ]] && DEBUG_MODE=1
[[ $IPTV_DEBUG -gt 1 ]] && set -x
RAW_ARGS=("$@")
COMMAND="$1"
shift
########################################################
# Ввод/вывод
########################################################
which tput > /dev/null 2>&1 && [ "$(tput -T"$TERM" colors)" -gt 8 ] && CAN_USE_COLORS=1 || CAN_USE_COLORS=0
IPTV_COLORS=${IPTV_COLORS:-$CAN_USE_COLORS}
[[ $IPTV_COLORS == 1 ]] && FBOLD="$(tput bold)" || FBOLD=''
[[ $IPTV_COLORS == 1 ]] && FDIM="$(tput dim)" || FDIM=''
[[ $IPTV_COLORS == 1 ]] && FRESET="$(tput sgr0)" || FRESET=''
ask() {
IFS= read -rp "$(print "${FBOLD}$1" ): " "$2"
}
print() {
echo -e "$*${FRESET}"
}
debug() {
[[ "$DEBUG_MODE" != 1 ]] && return
if [ "$2" ]; then
print "${FDIM}> ${FUNCNAME[1]:-?}():${BASH_LINENO:-?}\t$* ${FRESET}" >&2
else
print "${FDIM}> $* ${FRESET}" >&2
fi
}
var_dump() {
debug "$1 = ${!1}"
}
inform() {
print "$*${FRESET}"
}
subtitle() {
echo
inform "${FBOLD}$*${FRESET}"
}
title() {
subtitle "$@"
echo
}
success() {
print "${FBOLD}$*${FRESET}"
}
warn() {
print "${FBOLD}Внимание!${FRESET} $*${FRESET}"
}
error() {
print "${FBOLD}Ошибка:${FRESET} $*${FRESET}" >&2
print_stacktrace
}
die() {
error "${1:-halted}"
exit "${2:-255}"
}
print_stacktrace() {
[[ "$DEBUG_MODE" != 1 ]] && return
local i
local stack_size=${#FUNCNAME[@]}
debug "Callstack:"
# for (( i=$stack_size-1; i>=1; i-- )); do
for (( i=1; i<stack_size; i++ )); do
local func="${FUNCNAME[$i]}"
[ "$func" = "" ] && func=MAIN
local linen="${BASH_LINENO[$(( i - 1 ))]}"
local src="${BASH_SOURCE[$i]}"
[ "$src" = "" ] && src=non_file_source
debug " at $func $src:$linen"
done
}
########################################################
# Базовые хелперы
########################################################
# Возвращает абсолютный путь до $1
abspath() {
realpath -q "$1"
}
# Проверяет, указывает ли путь $1 на директорию
is_dir() {
[ -d "$(abspath "$1")" ]
}
# Проверяет, указывает ли путь $1 на файл
is_file() {
[ -f "$(abspath "$1")" ]
}
# Проверяет существование функции с именем $1
is_function() {
declare -F "$1" > /dev/null
}
# Проверяет соответствие строки $1 регулярному выражению $2
regex_match() {
[[ "$1" =~ ^$2$ ]]
}
# Проверяет соответствие строки $1 регулярному выражению $2 с помощью grep
grep_match() {
printf "%s" "$1" | grep -qE "$2" >/dev/null 2>&1
}
# Очищает строку аргументов от тех, которые не следует передавать в docker
clear_flags() {
args=${*/--full/}
args=${args/-f/}
args=${args/--root/}
args=${args/-r/}
echo "$args"
}
# Возвращает название текущей ОС
get_os() {
case "$(uname -s)" in
Linux*) echo Linux ;;
Darwin*) echo Macos ;;
CYGWIN*) echo Cygwin ;;
MINGW*) echo MinGw ;;
MSYS_NT*) echo Git ;;
*) return 1 ;;
esac
}
# Обрезает пробельные символы с начала и с конца строки
trim() {
echo "$1" | xargs
}
########################################################
# Функции парсинга аргументов
# https://gist.axenov.dev/anthony/sh-args
########################################################
# Парсит короткий аргумент
arg() {
[ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; }
local arg_name="${1:0:1}" # first character of argument name to find
local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value
local var_name="$3" || 0 # variable name to return value into or 0 to echo it in stdout
local value= # initialize empty value to check if we found one later
local arg_found=0 # marker of found argument
for idx in "${!RAW_ARGS[@]}"; do # going through all args
local arg_search=${RAW_ARGS[idx]} # get current argument
# skip $arg_search if it starts with '--' or letter
grep_match "$arg_search" "^(\w|--)" && continue
# clear $arg_search from special and duplicate characters, e.g. 'fas-)dfs' will become 'fasd'
local arg_chars="$(printf "%s" "$arg_search" \
| tr -s "[$arg_search]" 2>/dev/null \
| tr -d "[:punct:][:blank:]" 2>/dev/null)"
# if $arg_name is not one of $arg_chars the skip it
grep_match "-$arg_name" "^-[$arg_chars]$" || continue
arg_found=1
# then return '1'|'0' back into $value if we need flag or next arg value otherwise
[[ "$is_flag" == 1 ]] && value=1 || value="${RAW_ARGS[idx+1]}"
break
done
[[ "$is_flag" == 1 ]] && [[ -z "$value" ]] && value=0;
# if value we found is empty or looks like another argument then exit with error message
if [ "$arg_found" = 1 ] && ! grep_match "$value" "^[[:graph:]]+$" || grep_match "$value" "^--?\w+$"; then
echo "ERROR: Argument '-$arg_name' must have correct value!" >&2 && exit 1
fi
# return '$value' back into $var_name (if exists) or echo in stdout
[ "$var_name" ] && eval "$var_name='$value'" || echo "$value"
}
# Парсит длинный аргумент
argl() {
[ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; }
local arg_name="$1" # argument name to find
local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value
local var_name="$3" || 0 # variable name to return value into or 0 to echo it in stdout
local value= # initialize empty value to check if we found one later
local arg_found=0 # marker of found argument
for idx in "${!RAW_ARGS[@]}"; do # going through all args
local arg_search="${RAW_ARGS[idx]}" # get current argument
if [ "$arg_search" = "--$arg_name" ]; then # if current arg begins with two dashes
# then return '1' back into $value if we need flag or next arg value otherwise
[[ "$is_flag" == 1 ]] && value=1 || value="${RAW_ARGS[idx+1]}"
break # stop the loop
elif grep_match "$arg_search" "^--$arg_name=.+$"; then # check if $arg like '--foo=bar'
# then return '1' back into $value if we need flag or part from '=' to arg's end as value otherwise
[[ "$is_flag" == 1 ]] && value=1 || value="${arg_search#*=}"
break # stop the loop
fi
done
[[ "$is_flag" == 1 ]] && [[ -z "$value" ]] && value=0;
# if value we found is empty or looks like another argument then exit with error message
if [ "$arg_found" = 1 ] && ! grep_match "$value" "^[[:graph:]]+$" || grep_match "$value" "^--?\w+$"; then
echo "ERROR: Argument '--$arg_name' must have correct value!" >&2 && exit 1;
fi
# return '$value' back into $var_name (if exists) or echo in stdout
[ "$var_name" ] && eval "$var_name='$value'" || echo "$value"
}
########################################################
# Функции контроля системных пакетов
########################################################
# Проверяет наличие команды в системе
installed() {
command -v "$1" >/dev/null 2>&1
}
# Проверяет наличие пакета в системе
installed_pkg() {
dpkg --list | grep -qw "ii $1"
}
# Требует наличие пакета в системе
require() {
sw=()
for package in "$@"; do
# if ! installed "$package" && ! installed_pkg "$package"; then
if ! installed "$package"; then
sw+=("$package")
fi
done
if [ ${#sw[@]} -gt 0 ]; then
die "Это ПО должно быть установлено в системе:\n${sw[*]}" 200
fi
}
########################################################
# Функции для работы с git
########################################################
# Проверяет, является ли директория git-репозиторием
git.is_repo() {
require git
is_dir "$1/" && is_dir "$1/.git/"
}
# Требует, чтобы в директории был инициализирован репозиторий
git.require_repo() {
require git
git.is_repo "$1" || die "'$1' is not git repository!" 10
}
# Клонирует репозиторий
git.clone() {
require git
cmd="git clone $*"
debug "Команда: $cmd"
$cmd
}
# Клонирует репозиторий быстро
git.clone_quick() {
require git
git.clone --depth=1 --single-branch "$@"
}
# Изменяет значение конфигурацию репозитория по ключу
git.cfg_set() {
require git
cmd="git config --replace-all $1 $2"
debug "Команда: $cmd"
$cmd
}
# Получает значение конфигурацию репозитория по ключу
git.cfg_get() {
require git
cmd="git config --get $1"
debug "Команда: $cmd"
$cmd
}
# Устанавливает имя $1 и email $2 пользователя git в конфигурации репозитория
git.set_user() {
require git
local name="$1"
local email="$2"
git.cfg_set "user.name" "$name"
git.cfg_set "user.email" "$email"
success "Установлен пользователь git: '$name <$email>'"
}
# Получает изменения из удалённого репозитория и сливает их в текущую ветку
# shellcheck disable=SC2120
git.pull() {
require git
git pull "$@" 2>&1
}
# Получает изменения из удалённого репозитория и сливает их в текущую ветку
git.fetch() {
require git
if [ "$1" ]; then
if git.remote_branch_exists "origin/$1"; then
git fetch origin "refs/heads/$1:refs/remotes/origin/$1" --progress --prune --quiet 2>&1 \
|| die "Could not fetch $1 from origin" 12
else
warn "Tried to fetch branch 'origin/$1' but it does not exist."
fi
else
git fetch origin --progress --prune --quiet 2>&1 || exit 12
fi
}
# Переключает HEAD
git.checkout() {
require git
git checkout "$@" 2>&1
}
# Возвращает имя текущей ветки
git.current_branch() {
require git
git branch --show-current
}
########################################################
# Функции для работы с docker
########################################################
# Вызывает корректную команду docker compose
docker.compose() {
require docker
args=${*/--profiles=[a-zA-Z_,0-9]*/}
if docker compose &>/dev/null; then
local cmd="docker compose $args"
elif installed_pkg "docker-compose"; then
local cmd="docker-compose $args"
warn
warn "docker-compose v1 устарел и не поддерживается, его поведение непредсказуемо."
warn "Обнови docker согласно документации: https://docs.docker.com/engine/install/"
warn
else
error "Должен быть установлен docker-compose-plugin!"
die "Установи docker согласно документации: https://docs.docker.com/engine/install/" 2
fi
debug "Команда: $cmd"
$cmd
}
# Выводит информацию о контейнере
docker.inspect() {
cmd="docker inspect $*"
debug "Команда: $cmd"
$cmd 2>/dev/null
}
# Строит образы согласно файлов iptv-docker/docker-compose*.yml
docker.build_base_images() {
# for file in compose*.yml; do
# subtitle "Построение базовых образов: $file"
# docker.compose -f "$file" build
# done
subtitle "Построение образов"
docker.compose build
success "Базовые образы построены"
}
# Выполняет команду в контейнере от имени root
docker.exec() {
cmd="docker exec -u root -it $*"
debug "Команда: $cmd"
$cmd
}
# Выполняет команду в контейнере от имени www-data
docker.exec_www() {
cmd="docker exec -u www-data -it $*"
debug "Команда: $cmd"
$cmd
}
########################################################
# Хелперы для обработки команд
########################################################
# Возвращает ssh-адрес к репозиторию проекта
project_url_ssh() {
echo "$IPTV_GITEA_URL_SSH/${IPTV_PROJECTS[$1]}.git"
}
# Возвращает https-адрес к репозиторию проекта
project_url_https() {
echo "$IPTV_GITEA_URL_HTTPS/${IPTV_PROJECTS[$1]}"
}
# Копирует .env.example в .env, если возможно
prepare_dot_env() {
if is_file .env.example && ! is_file .env; then
debug "Копирование .env.example => .env"
cp .env.example .env
fi
}
# Устанавливает в файле .env для переменной $1 значение $2
set_env_value() {
local key="$1"
local value="$2"
debug "Установка '$key=$value' в файле .env"
if [[ "$(get_os)" = "Macos" ]]; then
sed -Ei '' "s|$key=.+|$key=$value|g" .env
else
sed -Ei "s|$key=.+|$key=$value|g" .env
fi
}
# Клонирует проект
project_clone() {
local repo_url="$1"; shift
local repo_path="$1"; shift
if git.is_repo "$repo_path"; then
debug "Репозиторий уже существует: $repo_path"
else
debug "Репозиторий будет склонирован: $repo_path"
git.clone "$repo_url" "$repo_path" "$@"
fi
}
# Проверяет передачу флагов -h|--help в команду и выводит справку
process_help_arg() {
command="${FUNCNAME[1]}"
need_help=$(arg help 1)
[[ "$need_help" -eq 0 ]] && need_help=$(argl help 1)
[[ "$need_help" -eq 1 ]] && help "$command"
}
# Выводит список сервисов
list_services_compose() {
services=$(docker.compose --profile full config --services | sort)
IFS=$'\n' sorted=("${services[@]}")
unset IFS
for name in "${sorted[@]}"; do
print " $name"
done
}
# Возвращает корректное название сервиса из композа
find_service_compose() {
svc="$1"
[ -z "$svc" ] && die "неизвестный сервис" 2
for known in $(docker.compose --profile full config --services); do
if [ "$known" = "$svc" ]; then
debug "Сервис '$svc' найден в композе"
echo "$known"
exit
fi
done
debug "Сервис '$svc' не найден в композе"
return 1
}
# Возвращает корректные названия сервисов из композа
find_services_compose() {
local services=''
[ "$*" ] && for svc in "$@"; do
grep_match "$svc" "^--?.*" && continue
var_dump svc
svc="$(find_service_compose "$svc")"
services="$services $svc"
done
trim "$services"
}
# Возвращает состояние контейнера
container_state() {
state=$(docker.inspect -f '{{.State.Status}}' "$1")
echo "${state//\'/}"
}
########################################################
# Главные функции обработки команд
########################################################
# Инициализирует репозиторий iptv-docker и все вложенные в него
init() {
process_help_arg
local docker_repo_path=$ROOT_PATH
local basename=$(basename "$docker_repo_path")
if [ ! "$basename" = "iptv-docker" ]; then
# это наиболее надёжный способ понять откуда запускается скрипт
docker_repo_path="$docker_repo_path/iptv-docker"
fi
#TODO установка ПО
subtitle "Подготовка локальной среды..."
project_clone "$IPTV_DOCKER_URL_SSH" "$docker_repo_path"
cd "$docker_repo_path"
prepare_dot_env
local counter=1
local projects_count=${#IPTV_PROJECTS[@]}
for project_name in "${!IPTV_PROJECTS[@]}"; do
local project_repo_path="$docker_repo_path/$project_name"
subtitle "[$counter/$projects_count] Подготовка проекта ${FBOLD}$project_name${FRESET}..."
local project_repo_url=$(project_url_ssh "$project_name")
debug "Известная ссылка на репозиторий: $project_repo_url"
project_clone "$project_repo_url" "$project_repo_path"
cd "$project_repo_path"
# git.checkout dev
prepare_dot_env
cd - >/dev/null
success "Проект $project_name готов"
counter=$((counter+1))
done
success "Локальная среда развёрнута: $docker_repo_path"
cd "$docker_repo_path"
docker.build_base_images && up
success "Проверь корректность значений, указанных в файлах .env, и выполни ${FBOLD}./iptv rebuild${FRESET} при необходимости."
}
# Создаёт и запускает контейнеры
up() {
process_help_arg
subtitle "Создание и запуск контейнеров"
argl profiles 0 profiles
local services=''
[ "$*" ] && services="$(find_services_compose "$@")"
COMPOSE_PROFILES="$profiles" docker.compose up "$services" --build --detach --remove-orphans && \
success 'Среда запущена успешно'
}
# Запускает созданные контейнеры
start() {
process_help_arg
subtitle "Запуск созданных ранее контейнеров"
profiles="$(argl profiles 0)"
local services=''
[ "$*" ] && services="$(find_services_compose "$@")"
COMPOSE_PROFILES="$profiles" docker.compose start "$services" && \
success 'Среда запущена успешно'
}
# Останавливает и удаляет контейнеры
down() {
process_help_arg
subtitle "Остановка и удаление контейнеров"
argl profiles 0 profiles
[[ -z "$profiles" ]] && profiles="full"
local services=''
[ "$*" ] && services="$(find_services_compose "$@")"
COMPOSE_PROFILES="$profiles" docker.compose down "$services" --remove-orphans && \
success 'Среда остановлена успешно'
}
# Останавливает контейнеры
stop() {
process_help_arg
subtitle "Остановка контейнеров"
profiles=$(argl profiles 0)
[[ -z "$profiles" ]] && profiles="full"
local services=''
[ "$*" ] && services="$(find_services_compose "$@")"
COMPOSE_PROFILES="$profiles" docker.compose stop "$services" && \
success 'Среда остановлена успешно'
}
# Останавливает, удаляет, создаёт и запускает контейнеры
rebuild() {
process_help_arg
is_full=$(arg full 1)
[ "$is_full" = 0 ] && is_full=$(argl full 1)
[ -n "$*" ] && down "$@"
[ "$is_full" = 1 ] && docker.build_base_images
up "$@"
}
# Останавливает и запускает созданные контейнеры
restart() {
process_help_arg
stop "$@" && start "$@"
}
# Останавливает среду, удаляет все контейнеры и образы
purge() {
process_help_arg
subtitle "Удаление docker-образов и контейнеров"
down
docker rmi "$(docker image list | grep iptv- | awk '{print $3}')"
docker rmi iptv/doxygen
docker system prune --all --force
success 'Образы удалены успешно'
}
# Обновляет все репозитории
update() {
process_help_arg
local docker_repo_path=$ROOT_PATH
local basename=$(basename "$docker_repo_path")
if [ ! "$basename" = "iptv-docker" ]; then
docker_repo_path="$docker_repo_path/iptv-docker"
fi
subtitle "Обновление локальной среды..."
cd "$docker_repo_path"
need_pull=$(arg pull 1)
[[ "$need_pull" = 0 ]] && need_pull=$(argl pull 1)
[[ "$need_pull" = 1 ]] && git.pull || git.fetch
local counter=1
local projects_count=${#IPTV_PROJECTS[@]}
for project_name in "${!IPTV_PROJECTS[@]}"; do
local project_repo_path="$docker_repo_path/$project_name"
subtitle "[$counter/$projects_count] Обновление проекта ${FBOLD}$project_name${FRESET}..."
cd "$project_repo_path"
#TODO при грязном дереве + $need_pull спрашивать (стеш, сброс), учитывать перед 'cd -' и поддержать аргументы
git.checkout dev
[ "$need_pull" = 1 ] && git.pull && git.fetch
cd - >/dev/null
success "Проект $project_name обновлён"
counter=$((counter+1))
done
success "Локальная среда обновлена"
}
# Генерирует флеймграф
flame() {
process_help_arg
require zcat git
arg trace 0 trace_file_path
[ -z "$trace_file_path" ] && argl trace 0 trace_file_path
[ -z "$trace_file_path" ] && die 'Файл трейса не указан'
trace_file_path=$(abspath "$trace_file_path")
debug "Файл трейса: $trace_file_path"
is_file "$trace_file_path" || die 'Файл трейса не найден'
svg_file_path=${trace_file_path/xt.gz/svg}
debug "Файл флеймграфа: $svg_file_path"
flame_repo_path="$ROOT_PATH/.tools/FlameGraph"
if is_dir "$flame_repo_path"; then
need_update=$(arg update 1)
[ "$need_update" = 0 ] && need_update=$(argl update 1)
if [ "$need_update" = 1 ]; then
inform 'Подготовка генератора...'
cd "$flame_repo_path"
git.pull
cd - >/dev/null
fi
else
inform 'Подготовка генератора...'
# https://derickrethans.nl/flamboyant-flamegraphs.html
# https://github.com/interstellar-space/FlameGraph
git.clone_quick https://github.com/brendangregg/FlameGraph.git "$flame_repo_path"
fi
inform "Генерация флеймграфа (может занять некоторое время)..."
zcat "$trace_file_path" | "$flame_repo_path/flamegraph.pl" > "$svg_file_path"
exit_code=$?
if [[ $exit_code -gt 0 ]]; then
exit $exit_code
fi
success "Флеймграф сохранён. Открой файл в браузере:"
print "$svg_file_path"
}
# Исправляет некоторые возможные проблемы
fix() {
process_help_arg
require docker
subtitle "Попытка исправления построения образов"
print "Список проблем, которые можно попробовать решить:"
print " 1. ошибки при построении образов"
print " 2. ошибки прав на директории vendor/bars/*"
print " 3. ошибки записи в директорию"
ask "Выбери свою проблему (Enter - выход)" choose
# shellcheck disable=SC2154
case $choose in
1)
purge
docker.build_base_images
up
;;
2)
ask "Введи имя сервиса" svc
svc_correct=$(find_service_hardcoded "$svc")
[ -z "$svc_correct" ] && exit
dirs=("$(docker.exec "$svc_correct" "find /var/www/vendor/bars -mindepth 1 -maxdepth 1 -type d")")
for dir in "${dirs[@]}"; do
inform "Исправление $dir"
docker.exec_www "$svc_correct" git config --global --add safe.directory "$dir"
done
;;
3)
ask "Введи имя сервиса" svc
svc_correct=$(find_service_hardcoded "$svc")
[ -z "$svc_correct" ] && exit
# current=$(docker.exec "$svc_correct" sh -c 'pwd')
ask "Введи директорию (Enter - выбор текущей /var/www)" dir
[ -z "$dir" ] && dir='/var/www'
if [[ $IPTV_DEBUG -gt 0 ]]; then
docker.exec "$svc_correct" find "$dir" -type d -print -exec chmod a+w {} +
docker.exec "$svc_correct" find "$dir" -type f -print -exec chmod a+w {} +
else
docker.exec "$svc_correct" find "$dir" -type d -exec chmod a+w {} +
docker.exec "$svc_correct" find "$dir" -type f -exec chmod a+w {} +
fi
;;
esac
success "Готово"
}
# Выполняет команду в контейнере
exec() {
process_help_arg
as_root=0
svc="$1"
regex_match "$svc" "--?r(oot)?" && { as_root=1; shift; svc="$1"; }
svc_correct="$(find_service_compose "$svc")"
command=("${@:2}")
regex_match "${command[0]}" "--?r(oot)?" && { as_root=1; unset "command[0]"; }
[[ -z "${command[*]}" ]] && die "не указана команда для выполнения в контейнере"
#TODO многострочные команды прокидываются корректно, но выполняется только первая строка?
if [[ "$as_root" == 1 ]]; then
docker.exec "$svc_correct" "${command[*]}"
else
docker.exec_www "$svc_correct" "${command[*]}"
fi
}
# Выводит логи сервиса
logs() {
process_help_arg
[ -z "$1" ] && die "не указан сервис" 8
svc=$(find_service_hardcoded "$1")
[ -z "$svc" ] && svc=$(find_service_compose "$1")
[ -z "$svc" ] && die "неизвестный сервис" 3
subtitle "Логи сервиса $svc"
shift
cmd="docker logs $svc $*"
debug "Команда: $cmd"
$cmd
}
# Выводит список профилей
profiles() {
process_help_arg
subtitle "Список доступных профилей"
profiles=("$(docker.compose config --profiles | sort)")
IFS=$'\n' sorted=("${profiles[@]}")
for profile in "${sorted[@]}"; do
print " $profile"
done
echo
inform "Просмотра списка сервисов в профиле используй './iptv services ПРОФИЛЬ'"
}
# Выводит список сервисов профиля
services() {
process_help_arg
[ -z "$1" ] && die "не указан профиль" 8
subtitle "Список сервисов в профиле: $1"
services=$(docker.compose --profile "$1" config --services | sort)
IFS=$'\n' sorted=("${services[@]}")
for service in "${sorted[@]}"; do
print " $service"
done
echo
inform "Для просмотра ПО сервиса используй './iptv info СЕРВИС'"
inform "Для просмотра состояния сервиса используй './iptv status СЕРВИС'"
inform "Для выполнения команды в сервисе используй './iptv exec СЕРВИС'"
}
# Выводит информацию о ПО в контейнере
info() {
process_help_arg
svc=$(find_service_hardcoded "$1")
subtitle "Состояние сервиса $svc"
#TODO как-то определять состав ПО, от этого уже выводить что-то дальше
docker.exec "$svc" date
subtitle "Переменные среды:"
docker.exec_www "$svc" env && echo
subtitle "Версии ПО:"
docker.exec "$svc" cat /etc/os-release && echo
docker.exec "$svc" git --version && echo
docker.exec "$svc" php -v && echo
docker.exec "$svc" composer --version && echo
docker.exec "$svc" php artisan --version && echo
docker.exec "$svc" go version && echo
docker.exec "$svc" go env
subtitle "Содержимое \$_SERVER (php):"
docker.exec_www "$svc" php -i | grep _SERVER
subtitle "Загруженные расширения (php):"
docker.exec_www "$svc" php -m
}
# Выводит состояние сервиса
status() {
process_help_arg
[ -z "$1" ] && die "не указан сервис" 8
svc=$(find_service_hardcoded "$1")
[ -z "$svc" ] && svc=$(find_service_compose "$1")
[ -z "$svc" ] && die "неизвестный сервис" 3
subtitle "Состояние сервиса $svc"
inform "Код:"
# shellcheck disable=SC2015
git.is_repo "$ROOT_PATH/$svc/app/" \
&& print " Репозиторий: +" \
|| print " Репозиторий: -"
if is_dir "$ROOT_PATH/$svc/app/"; then
cd "$ROOT_PATH/$svc/app/" 2>/dev/null
print " Ветка: $(git.current_branch)"
cd - >/dev/null
fi
# shellcheck disable=SC2015
is_file "$ROOT_PATH/$svc/app/.env" && \
print " Файл app/.env: +" || \
print " Файл app/.env: -"
inform "Контейнер:"
ips=$(docker.inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$svc")
print " IP: ${ips:--}"
print " State: $(container_state "$svc")"
health=$(docker.inspect -f '{{.State.Health.Status}}' "$svc")
health=${health//\'/}
print " Health: ${health:--}"
print " Running: $(docker.inspect -f '{{.State.Running}}' "$svc")"
print " Paused: $(docker.inspect -f '{{.State.Paused}}' "$svc")"
print " Restarting: $(docker.inspect -f '{{.State.Restarting}}' "$svc")"
print " OOMKilled: $(docker.inspect -f '{{.State.OOMKilled}}' "$svc")"
print " Dead: $(docker.inspect -f '{{.State.Dead}}' "$svc")"
error=$(docker.inspect -f '{{.State.Error}}' "$svc")
print " Error: ${error:--}"
}
# Выводит статистику потребления ресурсов контейнерами
stats() {
subtitle "Состояние контейнеров"
docker.compose stats "$@"
}
# Генерирует документацию по коду сервиса
doxygen() {
process_help_arg
require docker
if [[ $(docker image ls | grep -c iptv/doxygen) -eq 0 ]]; then
inform "Подготовка doxygen..."
docker build -t iptv/doxygen "$ROOT_PATH/.tools/doxygen"
fi
svc=$(find_service_hardcoded "$1")
[ -z "$svc" ] && die "неизвестный сервис" 3
subtitle "Генерация документации для $svc"
[[ $IPTV_DEBUG -gt 0 ]] && args="" || args="-q"
doxyfile="$ROOT_PATH/.tools/doxygen/$svc.Doxyfile"
is_file "$doxyfile" || die "Не найден файл $doxyfile" 8
input_dir="$ROOT_PATH/$svc"
is_dir "$input_dir" || die "Не найдена директория $input_dir" 9
output_dir="$IPTV_DOXYGEN_OUTPUT/$svc"
is_dir "$output_dir" || mkdir -p "$output_dir"
cmd="docker run --rm -it -v $input_dir:/data -v $output_dir:/output -v $doxyfile:/data/Doxyfile iptv/doxygen $args"
debug "Команда: $cmd"
# $cmd
rm -f "$input_dir/Doxyfile"
doc_path=$(basename "$IPTV_DOXYGEN_OUTPUT")
html_path="$IPTV_DOXYGEN_OUTPUT/../doxygen.html"
html="<!DOCTYPE html><html><head><meta charset='UTF-8'><style>*{font-family:'monospace'}h3{margin-bottom:0}</style><title>LisUP Doxygen</title></head>"
html="$html<body><h1>Документация к коду сервисов</h1><p><b>Для справки по генерации документации выполни в терминале './iptv doxygen --help'</b></p><ul>"
for html_dir in "$IPTV_DOXYGEN_OUTPUT"/*; do
doc_svc=$(basename "$html_dir")
mtime=$(date -d"$(stat -c '%y' "$html_dir")" +'%d.%m.%Y %H:%M')
html="$html<li><h3><a href='./$doc_path/$doc_svc/html/index.html'>$doc_svc</a></h3><b>Обновлено:</b> $mtime<br><b>Команда:</b> ./iptv doxygen $doc_svc</li>"
done
echo "$html</ul></body></html>" > "$html_path"
echo
success "Готово. Открой в браузере файл $(realpath "$html_path")"
}
########################################################
# Команды справки
########################################################
help() {
print "${FBOLD}Среда разработки iptv.axenov.dev"
is_function "help.$1" && help."$1" && exit
print "Использование:"
print " ./iptv КОМАНДА [АРГУМЕНТЫ]"
print
print "Доступные КОМАНДЫ:"
print " h|help - вывести это сообщение и выйти"
print " init - инициализировать окружение"
print " u|up - построить образы, контейнеры и запустить их"
print " start - запустить построенные ранее контейнеры"
print " d|down - остановить и удалить контейнеры"
print " stop - остановить контейнеры"
print " r|rebuild - выполнить 'down' и 'up'"
print " restart - выполнить 'stop' и 'start'"
print " purge - удалить контейнеры и образы"
print " update - обновить репозитории"
print " info - вывести версии ПО контейнера сервиса"
print " f|flame - сгенерировать флеймграф из трейса xdebug"
print " fix - исправить некоторые проблемы окружения"
print " exec - выполнить произвольную команду в контейнере"
print " logs - вывести логи контейнера"
print " profiles - вывести список профилей"
print " services - вывести список сервисов профиля"
print " info - вывести информацию о ПО в контейнере"
print " status - вывести статус сервиса и его репозитория"
print " stats - вывести информацию о потреблении ресурсов окружением"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести справку по любой КОМАНДЕ"
print
print "Любая КОМАНДА может также иметь свои АРГУМЕНТЫ."
print "Некоторые КОМАНДЫ могут также поддерживать АРГУМЕНТЫ 'docker' и его субсомманд."
print
print "См. './iptv help КОМАНДА' или './iptv КОМАНДА -h|--help' для справки."
}
help.help() {
print "КОМАНДА: help"
print "Отображает справку по скрипту и его командам."
print
print "Использование:"
print " ./iptv help [КОМАНДА]"
print " ./iptv КОМАНДА [-h|--help]"
print
print "Если КОМАНДА не указана, будет отображена справка по скрипту со списком допустимых КОМАНД."
}
help.init() {
print "КОМАНДА: init"
print "Инициализирует окружение разработки. Клонирует репозиторий iptv-docker, клонирует"
print "внутрь исходники всех сервисов и создаёт заготовки файлов .env для них. Можно"
print "использовать для подгрузки свежих сервисов, ещё не развёрнутых локально."
print
print "Использование:"
print " ./iptv init [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -n|--name ИМЯ - установить ИМЯ в качестве имени пользователя git"
print " -e|--email ПОЧТА - установить ПОЧТУ в качестве email пользователя git"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если --name и/или --email не переданы, то данные будут взяты из глобального конфига git."
print
print "ИМЯ и ПОЧТА проверяются на валидность:"
print " - ИМЯ должно содержать имя и фамилию на кириллице, разделённые пробелом;"
print " - ПОЧТА должна быть корпоративной (@bars.group или @bars-open.ru)."
print
print "Если ИМЯ или ПОЧТА некорректны, КОМАНДА завершится с ошибкой и предложением передать"
print "явно ИМЯ и ПОЧТУ с помощью аргументов."
print
print "После выполнения КОМАНДЫ необходимо указать корректные данные в файлах .env сервисов."
}
help.up() {
print "КОМАНДА: up"
print "Выполняет перестроение образов (включая базовые, при необходимости)"
print "создание и запуск контейнеров."
print
print "Использование:"
print " ./iptv up [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " -f|--full - перестроить также и базовые образы (если опущено, будут"
print " построены только образы сервисов)"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.start() {
print "КОМАНДА: start"
print "Выполняет запуск построенных ранее контейнеров."
print
print "Использование:"
print " ./iptv start [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.down() {
print "КОМАНДА: down"
print "Выполняет остановку и удаление запущенных контейнеров."
print
print "Использование:"
print " ./iptv down [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.stop() {
print "КОМАНДА: stop"
print "Выполняет остановку запущенных контейнеров."
print
print "Использование:"
print " ./iptv stop [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.rebuild() {
print "КОМАНДА: rebuild"
print "Выполняет 'down' и 'up'."
print
print "Использование:"
print " ./iptv rebuild [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " -f|--full - перестроить базовые образы (если опущено, будут"
print " построены только образы сервисов)"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.restart() {
print "КОМАНДА: restart"
print "Выполняет 'stop' и 'start'."
print
print "Использование:"
print " ./iptv restart [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.purge() {
print "КОМАНДА: purge"
print "Удаляет все образы и контейнеры 'iptv-*'."
print
print "Использование:"
print " ./iptv purge [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
}
help.update() {
print "КОМАНДА: update"
print "Обновляет окружение разработки, используя 'git fetch'."
print
print "Использование:"
print " ./iptv update [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -p|--pull - использовать 'git pull' вместо 'git fetch'"
print " -h|--help - вывести это сообщение и выйти"
print
print "Перед выполнением, необходимо обеспечить чистую рабочую директорию репозитория."
}
help.setenv() {
print "КОМАНДА: setenv"
print "Переключает среду в каждом сервисе окружения. Устанавливает 'APP_ENV'"
print "в файле .env сервиса и переключает git HEAD на тег, указанный в iptv-docker/.env."
print
print "Использование:"
print " ./iptv setenv СРЕДА [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
print "Перед выполнением, необходимо обеспечить чистую рабочие директории репозиториев."
}
help.flame() {
print "КОМАНДА: flame"
print "Выполняет построение флеймграфа по снимку трейса xdebug."
print
print "Использование:"
print " ./iptv flame АРГУМЕНТЫ"
print
print "Доступные АРГУМЕНТЫ:"
print " -t|--trace ПУТЬ - путь до локального файла трейса"
print " -u|--update - сначала обновить генератор"
print " -h|--help - вывести это сообщение и выйти"
}
help.fix() {
print "КОМАНДА: fix"
print "В интерактивном режиме пытается исправить локальную среду в случае ошибок."
print
print "Использование:"
print " ./iptv fix [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
}
help.exec() {
print "КОМАНДА: exec"
print "Выполняет произвольную команду внутри контейнера."
print
print "Использование:"
print " ./iptv exec СЕРВИС [АРГУМЕНТЫ] ВНУТ-КОМАНДА..."
print
print "Доступные АРГУМЕНТЫ:"
print " -r|--root - выполнить ВНУТ-КОМАНДУ от имени root (если опущено, будет выполнена"
print " от имени www-data)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Примеры:"
print " ./iptv exec arm composer i"
print " ./iptv exec core -r chmod 0777 storage/logs"
print
print "ВНУТ-КОМАНДА, которая должна выполниться внутри контейнера, может иметь свои аргументы."
print
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.logs() {
print "КОМАНДА: logs"
print "Выводит логи указанного СЕРВИСА."
print
print "Использование:"
print " ./iptv logs СЕРВИС [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
print "Поддерживаются АРГУМЕНТЫ 'docker logs'."
print
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.profiles() {
print "КОМАНДА: profiles"
print "Выводит список возможных профилей для запуска окружения."
print
print "Использование:"
print " ./iptv profiles [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
}
help.services() {
print "КОМАНДА: services"
print "Выводит список сервисов в указанном ПРОФИЛЕ."
print
print "Использование:"
print " ./iptv services ПРОФИЛЬ [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
print "Полный список ПРОФИЛЕЙ можно узнать командой './iptv profiles'"
}
help.info() {
print "КОМАНДА: info"
print 'Выводит дату, версии ПО, переменные среды и пр. из контейнера указанного СЕРВИСА.'
print
print "Использование:"
print " ./iptv info СЕРВИС [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.status() {
print "КОМАНДА: status"
print "Выводит состояние СЕРВИСА и его репозитория (при наличии)."
print
print "Использование:"
print " ./iptv status СЕРВИС [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.stats() {
print "КОМАНДА: stats"
print "Выводит потребление ресурсов окружением разработки."
print
print "Использование:"
print " ./iptv stats [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
print "Поддерживаются АРГУМЕНТЫ 'docker compose stats'."
print "Рекомендуется использовать вместе с '--no-stream'."
}
help.doxygen() {
print "КОМАНДА: doxygen"
print "Вызывает doxygen для генерации дкоументации к коду указанного СЕРВИСА и индексной"
print "страницы $IPTV_DOXYGEN_OUTPUT/doxygen.html"
print
print "Использование:"
print " ./iptv doxygen СЕРВИС"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
########################################################
# Точка входа
########################################################
debug "Директория выполнения: $ROOT_PATH/$IPTV_DIR"
case "$COMMAND" in
h|help ) help "$1" ;;
init ) init "$@" ;;
u|up ) up "$@" ;;
start ) start "$@" ;;
d|down ) down "$@" ;;
stop ) stop "$@" ;;
r|rebuild ) rebuild "$@" ;;
restart ) restart "$@" ;;
purge ) purge ;;
update ) update ;;
s|setenv ) setenv "$@" ;;
f|flame ) flame "$@" ;;
fix ) fix ;;
exec ) exec "$@" ;;
logs ) logs "$@" ;;
profiles ) profiles ;;
services ) services "$1" ;;
info ) info "$1" ;;
status ) status "$1" ;;
stats ) stats "$@" ;;
doxygen ) doxygen "$@" ;;
* ) die "неизвестная команда. Смотри './iptv help' для справки." ;;
esac