netoik-cicd/src/deployer.sh

415 lines
11 KiB
Bash
Executable File

#!/bin/bash
################################################################################
# #
# netoik-cicd-deployer #
# #
# This binary is made to be run by root, it expects a request from an #
# unprivileged user server, deploys (installs or updates) the related rpm #
# package and sends a response to the user. #
# #
# This script is coded with respect of google shell styleguide: #
# https://google.github.io/styleguide/shellguide.html #
# #
################################################################################
# Exit immediately if any command fails.
set -e
# Exit with the last non-zero fail code.
set -o pipefail
# Program constants.
readonly PACKAGE_NAME="netoik-cicd"
readonly PROGRAM_NAME="${PACKAGE_NAME}-deployer"
# Declare default variables.
readonly DEFAULT_CONFIG_FILE="/etc/${PACKAGE_NAME}/${PACKAGE_NAME}.conf"
readonly DEFAULT_ERRORS_FILE="/etc/${PACKAGE_NAME}/errors.conf"
# Declare levels variables.
readonly VERBOSITY_LEVELS=(
[10]="CRITICAL"
[20]="ERROR"
[30]="WARN"
[40]="INFO"
[50]="DEBUG"
)
readonly DEFAULT_VERBOSITY_LEVEL=40
# RPM constants.
readonly RPM_RELEASE="1"
RPM_ARCH=$(rpm --eval "%{_arch}")
readonly RPM_ARCH
RPM_DIST=$(rpm --eval "%{dist}")
readonly RPM_DIST
# Functions.
log() (
level="$1"
msg="$2"
if [ "${level}" -le "${verbosity_level}" ]; then
echo "${VERBOSITY_LEVELS[${level}]} - ${msg}"
fi
)
log_critical() (log 10 "$1" >&2)
log_error() (log 20 "$1" >&2)
log_warn() (log 30 "$1" >&2)
log_info() (log 40 "$1")
log_debug() (log 50 "$1")
fail() (
msg="$1"
if [ $# -eq 2 ]; then
code="$2"
else
code=1
fi
log_critical "${msg}"
if [ "${code}" -gt 0 ]; then
exit "${code}"
else
log_error "Undefined error code for '${msg}'."
exit 1
fi
)
usage() (
echo "Usage: ${PROGRAM_NAME} [OPTION]...
Start deployer server, wait for unixsock requests to deploy rpm packages.
Mandatory argumentes for long options are mandatory for short options too.
-c, --conf=<config_file> name of configuration file, default
to ${DEFAULT_CONFIG_FILE}
-d, --daemon run processing loop in background
-e, --errs=<errors_file> name of errors file, default to
${DEFAULT_ERRORS_FILE}
-q, --quiet set level verbosity to WARN, default to INFO
-s, --stop stop listening after first request process
-t, --test just test config file and do not run start loop
-v, --verbose set level verbosity to DEBUG, default to INFO
-h, --help display this help message and exit
"
)
reply() (
code="$1"
msg="$2"
if [ "${code}" -gt 0 ]; then
log_error "${msg}"
else
log_info "${msg}"
fi
if ! output="$(jq --null-input \
--compact-output \
--arg c "${code}" \
--arg m "${msg}" \
'{"code":$c|tonumber,"msg":$m}' |
ncat --unixsock "${response_sock}" 2>&1)"; then
log_error "Cannot write to sock '${response_sock}': ${output}."
fi
)
process_request() (
# Get response_sock from json.
log_debug "Get response sock file."
if ! response_sock="$(echo "${request}" |
jq --raw-output ".response_sock" 2>&1)"; then
log_error "Cannot parse json request '${request}': ${response_sock}."
return
fi
if [ "${response_sock}" = "null" ]; then
log_error "Missing key 'response_sock' in request '${request}'."
return
fi
if [ -z "${response_sock}" ]; then
log_error "Empty value for key 'response_sock' in request '${request}'."
return
fi
if [ ! -S "${response_sock}" ]; then
log_error \
"Sock file '${response_sock}' does not exist in request '${request}'."
return
fi
log_debug "Sock file '${response_sock}' is reachable."
# Get pkg_name from json.
log_debug "Get pkg_name from json."
if ! pkg_name="$(echo "${request}" | jq --raw-output ".pkg_name" 2>&1)"; then
reply "${err_json_bad_format}" \
"Cannot parse json '${request}': ${pkg_name}."
return
fi
if [ "${pkg_name}" = "null" ]; then
reply "${err_pkg_name_missing}" "Missing key pkg_name in '${request}'."
return
fi
if [ -z "${pkg_name}" ]; then
reply "${err_pkg_name_empty}" \
"Empty value for key pkg_name in '${request}'."
return
fi
log_debug "Got pkg name: '${pkg_name}'."
# Get repository folder.
repo_dir="${repos_dir}/${pkg_name}.git"
if [ ! -d "${repo_dir}" ]; then
reply "${err_repo_dir_not_exist}" "Repository '${repo_dir}' does not exist."
return
fi
log_debug "Repository is present at '${repo_dir}'."
# Get pkg_version from json.
log_debug "Get pkg_version from json."
if ! pkg_version="$(echo "${request}" |
jq --raw-output ".pkg_version" 2>&1)"; then
reply "${err_json_bad_format}" \
"Cannot parse json '${request}': ${pkg_version}."
return
fi
if [ "${pkg_version}" = "null" ]; then
reply "${err_pkg_version_missing}" \
"Missing key pkg_version in '${request}'."
return
fi
if [ -z "${pkg_version}" ]; then
reply "${err_pkg_version_empty}" \
"Empty value for key pkg_version in '${request}'."
return
fi
log_debug "Got pkg version '${pkg_version}'."
# Check if package does exist.
rpm_path="${rpms_dir}/${RPM_ARCH}/${pkg_name}-${pkg_version}-${RPM_RELEASE}${RPM_DIST}.${RPM_ARCH}.rpm"
if [ ! -f "${rpm_path}" ]; then
reply "${err_rpm_path_not_exist}" \
"RPM package '${rpm_path}' does not exist!"
return
fi
log_debug "RPM path found at '${rpm_path}'."
# Upgrade package if already installed.
log_debug "Check if pkg '${pkg_name}' is already installed."
if rpm --query "${pkg_name}" 1>/dev/null 2>/dev/null; then
log_debug "Package '${pkg_name}' already installed, so upgrade to v
${pkg_version}"
if ! output="$(sudo rpm --upgrade \
--verbose --hash "${rpm_path}" 2>&1)"; then
reply "${err_rpm_upgrade}" \
"Cannot upgrade package '${pkg_name}' to v${pkg_version}: ${output}."
return
fi
log_debug "RPM package '${pkg_name}' upgraded to v${pkg_version}."
fi
# Install package if not already installed.
log_debug "Package '${pkg_name}' is not already installed, so install
v${pkg_version}."
if ! output="$(sudo rpm --install --verbose --hash "${rpm_path}")"; then
reply "${err_rpm_install}" \
"Cannot install package '${pkg_name}' v${pkg_version}: ${output}."
return
fi
# Package deployed.
reply 0 "RPM package '${pkg_name}' v${pkg_version} has been deployed."
)
process_loop() (
# Loop on every request.
if "${keep_open}"; then
log_info "Start listening unixsock with keep-open at '${deployer_sock}'."
while read -r request; do
log_info "Process new request '${request}'."
process_request
done < <(ncat --listen --keep-open --unixsock "${deployer_sock}")
else
log_info "Start listening unixsock without keep-open at '${deployer_sock}'."
while read -r request; do
log_info "Process new request '${request}'."
process_request
done < <(ncat --listen --unixsock "${deployer_sock}")
fi
# Kill all remaining subprocesses only if daemon.
log_info "End of loop."
if "${daemon}"; then
log_debug "Kill child jobs."
jobs -p | xargs kill 2>/dev/null || true
fi
# Remove sock file
log_debug "Remove deployer unixsock file."
rm --force "${deployer_sock}"
log_info "End of process."
)
main() (
# Parse arguments.
daemon="false"
testing="false"
keep_open="true"
config_file="${DEFAULT_CONFIG_FILE}"
errors_file="${DEFAULT_ERRORS_FILE}"
verbosity_option=""
verbosity_level="${DEFAULT_VERBOSITY_LEVEL}"
if ! args="$(getopt --name "${PROGRAM_NAME}" \
--options c:de:hqstv \
--longoptions conf:,daemon,errs:,help,quiet,stop,test,verbose \
-- "$@")"; then
usage
fail "Bad arguments."
fi
eval set -- "${args}"
while true; do
case "$1" in
-c | --conf)
config_file="$2"
shift 2
;;
-d | --daemon)
daemon="true"
shift
;;
-e | --errs)
errors_file="$2"
shift 2
;;
-h | --help)
usage
exit 0
;;
-q | --quiet)
if [ "${verbosity_option}" ]; then
usage
fail "Options $1 and ${verbosity_option} are mutually exclusive."
fi
verbosity_option="$1"
((verbosity_level -= 10))
shift
;;
-s | --stop)
keep_open="false"
shift
;;
-t | --test)
testing="true"
shift
;;
-v | --verbose)
if [ "${verbosity_option}" ]; then
usage
fail "Options $1 and ${verbosity_option} are mutually exclusive."
fi
verbosity_option="$1"
((verbosity_level += 10))
shift
;;
--)
shift
break
;;
*)
usage
fail "Unexpected option '$1'."
;;
esac
done
if [ "$@" ]; then
usage
fail "Unexpected extra arguments '$*'."
fi
# Load config file.
if [ ! -r "${config_file}" ]; then
fail "Config file '${config_file}' is not readable."
fi
log_info "Load config file '${config_file}'."
# shellcheck source=./conf/netoik-cicd.conf.sample
source "${config_file}"
# Load errors file.
if [ ! -r "${errors_file}" ]; then
fail "Errors file '${errors_file}' is not reachable."
fi
log_info "Load errors file '${errors_file}'."
# shellcheck source=./conf/errors.conf.sample
source "${errors_file}"
# Check variables in config file.
if [ -z "${deployer_sock}" ]; then
fail "Variable deployer_sock is empty." "${err_deployer_sock_empty}"
fi
if [ ! -d "$(dirname "${deployer_sock}")" ]; then
fail "Dirname of deployer_sock='${deployer_sock}' is not a directory." \
"${err_deployer_sock_dir_not_directory}"
fi
if [ ! -w "$(dirname "${deployer_sock}")" ]; then
fail "Directory of deployer_sock='${deployer_sock}' is not writable." \
"${err_deployer_sock_dir_not_writable}"
fi
if [ -e "${deployer_sock}" ]; then
fail "Sock deployer_sock='${deployer_sock}' is already in use." \
"${err_deployer_sock_already_in_use}"
fi
if [ -z "${repos_dir}" ]; then
fail "Variable repos_dir is empty." "${err_repos_dir_empty}"
fi
if [ ! -x "${repos_dir}" ]; then
fail "Directory repos_dir='${repos_dir}' is not accessible." \
"${err_repos_dir_not_accessible}"
fi
if [ -z "${rpms_dir}" ]; then
fail "Variable rpms_dir is empty." "${err_rpms_dir_empty}"
fi
if [ -z "${runner_username}" ]; then
fail "Variable runner_username is empty." "${err_runner_username_empty}"
fi
if ! id --user --name "${runner_username}" 1>/dev/null; then
fail "Runner user '${runner_username}' does not exist." \
"${err_runner_user_not_exist}"
fi
if [ -z "${runner_deployer_groupname}" ]; then
fail "Variable runner_deployer_username is empty." "${err_runner_deployer_groupname_empty}"
fi
if ! getent group "${runner_deployer_groupname}"; then
fail "Runner-deployer group '${runner_deployer_groupname}' does not exist." \
"${err_runner_deployer_group_not_exist}"
fi
# Stop now if we are only testing config.
if "${testing}"; then
exit 0
fi
# Set right access in background after nc listen.
(
inotifywait --event create "$(dirname "${deployer_sock}")" --quiet --quiet
chmod 775 "${deployer_sock}"
chgrp "${runner_deployer_groupname}" "${deployer_sock}"
) &
# Run process loop in background or foreground.
if "${daemon}"; then
log_info "Run process loop in background."
process_loop &
else
log_info "Run process loop in foreground."
process_loop
jobs -p | xargs kill 2>/dev/null || true
fi
)
main "$@"