707 lines
16 KiB
Bash
Executable file
707 lines
16 KiB
Bash
Executable file
#!/bin/sh
|
|
|
|
LOGPID=''
|
|
|
|
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
export PATH
|
|
|
|
# Directory structure:
|
|
#
|
|
# /var/cache/deployd
|
|
# ... <did>/
|
|
# upstream/
|
|
# sources/
|
|
# build/
|
|
# build.live/ (temporary, while building)
|
|
# .lock (for locking)
|
|
# backup/
|
|
# sources/
|
|
# build/
|
|
# backup.bak/ (temporary, while backuping)
|
|
# deploy.log
|
|
#
|
|
# Flow:
|
|
# prepare() (get sources, set up the environment)
|
|
# build() (build the sources, minify them, etc)
|
|
# test() (test the built sources)
|
|
# cleanup() (clean up the current directory of any garbage)
|
|
# teardown() (tear down any previous resources (such as daemons))
|
|
# deploy() (set up any resources (such as daemons))
|
|
# sysadmin() (sysadmin tasks)
|
|
|
|
set -e
|
|
|
|
error() {
|
|
echo "$(date -u +'%Y-%m-%d %H:%M:%S') | Error: $*" >&2
|
|
}
|
|
|
|
info() {
|
|
echo "$(date -u +'%Y-%m-%d %H:%M:%S') | Info: $*" >&2
|
|
}
|
|
|
|
warn() {
|
|
echo "$(date -u +'%Y-%m-%d %H:%M:%S') | Warning: $*" >&2
|
|
}
|
|
|
|
abs_path() {
|
|
path="$1"
|
|
|
|
if [ -d "$path" ]; then
|
|
(cd "$path" 2>/dev/null && pwd) || return 1
|
|
else
|
|
dir="${path%/*}"
|
|
file="${path##*/}"
|
|
|
|
if [ "$dir" = "$path" ]; then
|
|
printf '%s/%s\n' "$(pwd)" "$path"
|
|
else
|
|
(cd "$dir" 2>/dev/null && printf '%s/%s\n' "$(pwd)" "$file") || return 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
parse_deploy_header_block() {
|
|
inside_block=0
|
|
|
|
DEPLOY_NAME=""
|
|
DEPLOY_EXTENSIONS=""
|
|
DEPLOY_USER=""
|
|
DEPLOY_GROUP=""
|
|
DEPLOY_TARGET=""
|
|
DEPLOY_COPY=""
|
|
|
|
if ! grep -q '\bprepare()' -- "$1"; then
|
|
error 'Deployer script does not include a prepare() function'
|
|
return 1;
|
|
fi
|
|
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
'# -----'*)
|
|
if [ "$inside_block" -eq 0 ]; then
|
|
inside_block=1
|
|
else
|
|
break
|
|
fi
|
|
;;
|
|
'# '*)
|
|
if [ "$inside_block" -eq 1 ]; then
|
|
case "$line" in
|
|
\#\ Name:\ *)
|
|
DEPLOY_NAME="${line#\# Name: }"
|
|
;;
|
|
\#\ Extensions:\ *)
|
|
DEPLOY_EXTENSIONS="${line#\# Extensions: }"
|
|
;;
|
|
\#\ User:\ *)
|
|
DEPLOY_USER="${line#\# User: }"
|
|
;;
|
|
\#\ Group:\ *)
|
|
DEPLOY_GROUP="${line#\# Group: }"
|
|
;;
|
|
\#\ Target:\ *)
|
|
DEPLOY_TARGET="${line#\# Target: }"
|
|
;;
|
|
\#\ Copy:\ *)
|
|
DEPLOY_COPY="${line#\# Copy: }"
|
|
|
|
case "$DEPLOY_COPY" in
|
|
yes | Yes | YES | 1 | true | True | TRUE)
|
|
DEPLOY_COPY=1
|
|
;;
|
|
*)
|
|
DEPLOY_COPY=''
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
fi
|
|
;;
|
|
esac
|
|
done <"$1"
|
|
|
|
if [ "$inside_block" -eq 0 ]; then
|
|
error "No (valid) metadata block found in the deployer, please use the following template:"
|
|
|
|
cat <<EOF
|
|
# --------------------------------------
|
|
# Name: my epic deploy
|
|
# Extensions: ??? (optional)
|
|
# User: ???
|
|
# Group: ???
|
|
# Target: ??? (optional)
|
|
# Copy: yes
|
|
# --------------------------------------
|
|
EOF
|
|
|
|
return 1
|
|
fi
|
|
|
|
for var in DEPLOY_NAME DEPLOY_USER DEPLOY_GROUP; do
|
|
if [ -z "$var" ]; then
|
|
error "$var is not set"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
export DEPLOY_NAME DEPLOY_EXTENSIONS DEPLOY_USER DEPLOY_GROUP DEPLOY_TARGET DEPLOY_COPY
|
|
}
|
|
|
|
validate_directory_path() {
|
|
dir_path="$1"
|
|
var_name="$2"
|
|
|
|
# Check non-empty
|
|
if [ -z "$dir_path" ]; then
|
|
error "$var_name is empty"
|
|
return 1
|
|
fi
|
|
|
|
# Disallow root dir
|
|
if [ "$dir_path" = "/" ]; then
|
|
error "$var_name cannot be root directory (/)"
|
|
return 1
|
|
fi
|
|
|
|
# Disallow relative paths (must be absolute)
|
|
case "$dir_path" in
|
|
/*) ;; # absolute path, OK
|
|
*)
|
|
error "$var_name must be an absolute path"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
if [ "$3" != '-L' ]; then
|
|
# Check if directory is symlink
|
|
if [ -L "$dir_path" ]; then
|
|
error "$var_name path is a symlink; refusing for safety"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Remove leading slash, split by '/'
|
|
path_components="$(printf '%s' "$dir_path" | sed 's|^/||' | tr '/' '\n')"
|
|
|
|
# Allowed characters regex: a-zA-Z0-9_- only
|
|
for comp in $path_components; do
|
|
if ! printf '%s\n' "$comp" | grep -qE '^[a-zA-Z0-9_.-]+$'; then
|
|
error "$var_name contains invalid characters in path component '$comp'. Allowed: a-z, A-Z, 0-9, _, ., -"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
validate_user_and_group() {
|
|
user="$1"
|
|
group="$2"
|
|
|
|
if ! printf '%s\n' "$user" | grep -qE '^[a-zA-Z0-9_.-]+$'; then
|
|
error "User '$user' is invalid"
|
|
return 1
|
|
fi
|
|
|
|
if ! id -u "$user" >/dev/null 2>&1; then
|
|
error "User '$user' does not exist"
|
|
return 1
|
|
fi
|
|
|
|
if ! printf '%s\n' "$group" | grep -qE '^[a-zA-Z0-9_.-]+$'; then
|
|
error "Group '$group' is invalid"
|
|
return 1
|
|
fi
|
|
|
|
if ! getent group "$group" >/dev/null 2>&1; then
|
|
error "Group '$group' does not exist"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
print_usage() {
|
|
"$1" "Usage: $(basename -- "$2") <teardown|deploy|rollback|cleanup|restart|sysadmin|logs|help>"
|
|
"$1" " $(basename -- "$2") --global-cleanup [scripts directory] - clean up residuals"
|
|
"$1" " $(basename -- "$2") --unlock <teardown|deploy|rollback|cleanup|restart|sysadmin|logs|help> - force-unlock the deploy (such as in case of a deadlock)"
|
|
}
|
|
|
|
run_deploy() {
|
|
deployer="$1"
|
|
didpath="$2"
|
|
|
|
backup="$didpath/backup"
|
|
upstream="$didpath/upstream"
|
|
|
|
logfile="$didpath/deploy.log"
|
|
|
|
validate_directory_path "$deployer" deployer || return 1
|
|
validate_directory_path "$didpath" didpath || return 1
|
|
|
|
subcommand="$3"
|
|
case "$subcommand" in
|
|
'') ;;
|
|
teardown | deploy) ;;
|
|
rollback) ;;
|
|
cleanup) ;;
|
|
restart) ;;
|
|
sysadmin) ;;
|
|
*)
|
|
error "Unknown subcommand: $subcommand"
|
|
print_usage error "$deployer"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
parse_deploy_header_block "$deployer"
|
|
validate_directory_path "$DEPLOY_TARGET" DEPLOY_TARGET -L || return 1
|
|
|
|
validate_user_and_group "$DEPLOY_USER" "$DEPLOY_GROUP" || return 1
|
|
|
|
setup="#!/bin/sh
|
|
|
|
set -e
|
|
|
|
PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"
|
|
export PATH
|
|
|
|
# Just a random Safe and Sophie Germain prime number, because I love primes
|
|
DEPLOYER_ID=\"$(echo "${deployer}${DEPLOY_NAME}10397618657851825223" | sha256sum | cut -d ' ' -f 1)\"
|
|
export DEPLOYER_ID
|
|
|
|
build() { return 0; }
|
|
test() { return 0; }
|
|
teardown() { return 0; }
|
|
deploy() { return 0; }
|
|
cleanup() { return 0; }
|
|
restart() { return 0; }
|
|
sysadmin() { return 0; }
|
|
|
|
error() { echo \"[deploy] \$(date -u +'%Y-%m-%d %H:%M:%S') | Error: \$*\" >&2; }
|
|
info() { echo \"[deploy] \$(date -u +'%Y-%m-%d %H:%M:%S') | Info: \$*\" >&2; }
|
|
warn() { echo \"[deploy] \$(date -u +'%Y-%m-%d %H:%M:%S') | Warning: \$*\" >&2; }
|
|
|
|
prevsrc() {
|
|
mkdir -p -- '$upstream/sources'
|
|
|
|
if [ -d '$backup' ]; then
|
|
cp -a -- '$backup/sources/.' '$upstream/sources/.'
|
|
fi
|
|
}
|
|
|
|
{
|
|
|
|
$(cat "$deployer")
|
|
|
|
}
|
|
"
|
|
|
|
for ext in $DEPLOY_EXTENSIONS; do
|
|
ext_path="/usr/share/deployer/extensions/$ext"
|
|
|
|
if [ -f "$ext_path" ] && [ -r "$ext_path" ]; then
|
|
setup="$setup
|
|
|
|
# Extension: $ext
|
|
{
|
|
|
|
$(cat "$ext_path")
|
|
|
|
}
|
|
"
|
|
else
|
|
error "Extension $ext was not found"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
deploy="$setup
|
|
|
|
__deployd_run_step() {
|
|
info \"Running \$1()\"
|
|
if ! \"\$1\"; then
|
|
error \"\$1() failed!\"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
__deployer_on_goodbye() {
|
|
info 'Goodbye!'
|
|
rm -rf -- '$upstream/sources' '$upstream/build.live'
|
|
exit 1
|
|
}
|
|
|
|
info 'Cleaning up upstream...'
|
|
rm -rf -- '$upstream/sources' '$upstream/build.live'
|
|
|
|
trap __deployer_on_goodbye INT TERM
|
|
|
|
if [ -d '$backup' ]; then
|
|
if [ ! -d '$upstream' ]; then
|
|
info 'Restoring from backup...'
|
|
cp -a -- '$backup' '$upstream'
|
|
fi
|
|
else
|
|
if [ -e '$DEPLOY_TARGET' ]; then
|
|
error 'Deploy target exists first-run'
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p -- '$upstream'
|
|
fi
|
|
cd -- '$upstream'
|
|
|
|
info \"Build name: \$DEPLOY_NAME\"
|
|
"
|
|
|
|
if [ "$subcommand" ]; then
|
|
deploy="$deploy
|
|
|
|
case '$subcommand' in
|
|
teardown | deploy)
|
|
cd -- '$upstream/build'
|
|
__deployd_run_step '$subcommand'
|
|
;;
|
|
|
|
rollback)
|
|
if [ ! -d '$backup' ]; then
|
|
error 'No backup to rollback to'
|
|
exit 1
|
|
fi
|
|
|
|
rm -rf -- '$upstream'
|
|
if ! cp -a -- '$backup' '$upstream'; then
|
|
error 'Restoring from backup failed.'
|
|
rm -rf -- '$upstream'
|
|
fi
|
|
;;
|
|
|
|
cleanup)
|
|
info 'Removing backups...'
|
|
rm -rf -- '$backup'
|
|
|
|
info 'Removing upstream live builds...'
|
|
rm -rf -- '$upstream/build.live'
|
|
|
|
if [ '$4' = '--unlink' ]; then
|
|
if [ '$DEPLOY_TARGET' ]; then
|
|
if [ '$DEPLOY_COPY' ]; then
|
|
info 'Removing upstream...'
|
|
rm -rf -- '$DEPLOY_TARGET'
|
|
else
|
|
if [ -L '$DEPLOY_TARGET' ]; then
|
|
info 'Unlinking upstream...'
|
|
unlink '$DEPLOY_TARGET'
|
|
elif [ -e '$DEPLOY_TARGET' ]; then
|
|
error \"$DEPLOY_TARGET exists and is not a symlink\"
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
restart)
|
|
cd -- '$upstream/build'
|
|
info 'Restarting the deploy...'
|
|
__deployd_run_step teardown
|
|
__deployd_run_step deploy
|
|
;;
|
|
|
|
sysadmin)
|
|
info 'Your sysadmin tasks will soon be ran...'
|
|
;;
|
|
|
|
*)
|
|
error 'Unknown subcommand passed to the deployer'
|
|
exit 1
|
|
;;
|
|
esac
|
|
"
|
|
else
|
|
deploy="$deploy
|
|
|
|
info 'Creating the sources directory'
|
|
mkdir -p sources build.live
|
|
|
|
# Step 1: prepare
|
|
cd -- '$upstream/sources'
|
|
__deployd_run_step prepare
|
|
|
|
info 'Copying sources to build'
|
|
cd -- '$upstream'
|
|
cp -a -- '$upstream/sources/.' '$upstream/build.live/.'
|
|
cd -- '$upstream/build.live'
|
|
|
|
# Step 2: build
|
|
cd -- '$upstream/build.live'
|
|
__deployd_run_step build
|
|
|
|
# Step 3: test
|
|
cd -- '$upstream/build.live'
|
|
__deployd_run_step test
|
|
|
|
# Step 4: cleanup
|
|
cd -- '$upstream/build.live'
|
|
__deployd_run_step cleanup
|
|
|
|
# Step 5: teardown
|
|
cd -- '$upstream/build.live'
|
|
__deployd_run_step teardown
|
|
|
|
# Step 5.5: Link
|
|
rm -rf -- '$upstream/build'
|
|
mv -- '$upstream/build.live' '$upstream/build'
|
|
|
|
if [ '$DEPLOY_TARGET' ]; then
|
|
if [ '$DEPLOY_COPY' ]; then
|
|
rm -rf -- '$DEPLOY_TARGET'
|
|
cp -a -- '$upstream/build' '$DEPLOY_TARGET'
|
|
else
|
|
if [ -L '$DEPLOY_TARGET' ]; then
|
|
ln -sfn '$upstream/build' '$DEPLOY_TARGET'
|
|
elif [ -e '$DEPLOY_TARGET' ]; then
|
|
error \"$DEPLOY_TARGET exists and is not a symlink\"
|
|
exit 1
|
|
else
|
|
ln -s '$upstream/build' '$DEPLOY_TARGET'
|
|
fi
|
|
fi
|
|
|
|
# Step 6: deploy
|
|
cd -- '$DEPLOY_TARGET'
|
|
__deployd_run_step deploy
|
|
else
|
|
# Step 6: deploy
|
|
cd -- '$upstream/build'
|
|
__deployd_run_step deploy
|
|
fi
|
|
|
|
info 'Build deployed'
|
|
"
|
|
fi
|
|
|
|
if ! chown "${DEPLOY_USER}:${DEPLOY_GROUP}" -R "$didpath"; then
|
|
error 'Failed to chown working directory'
|
|
return 1
|
|
fi
|
|
|
|
if ! deployd-su "$DEPLOY_USER" "$DEPLOY_GROUP" "$deploy"; then
|
|
error 'Failed to deployd-su.'
|
|
return 1
|
|
fi
|
|
|
|
if ! /bin/sh -c "$setup
|
|
|
|
info 'Entering root for sysadmin tasks...'
|
|
|
|
__deployd_run_step sysadmin
|
|
"; then
|
|
error "Sysadmin command failed"
|
|
return 1
|
|
fi
|
|
|
|
if [ ! "$subcommand" ]; then
|
|
info 'Backing up the upstream'
|
|
|
|
if [ -d "$backup" ]; then
|
|
cp -a "$backup" "${backup}.tmp"
|
|
fi
|
|
|
|
rm -rf -- "$backup"
|
|
if ! cp -a -- "$upstream" "$backup"; then
|
|
rm -rf -- "$backup"
|
|
if [ -d "${backup}.tmp" ]; then
|
|
mv -- "${backup}.tmp" "$backup"
|
|
fi
|
|
return 1
|
|
fi
|
|
rm -rf -- "${backup}.tmp"
|
|
fi
|
|
|
|
if ! chown "${DEPLOY_USER}:${DEPLOY_GROUP}" -R "$didpath"; then
|
|
error 'Failed to chown working directory'
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
logs_cleanup() {
|
|
if [ "$LOGPID" ]; then
|
|
if ! kill "$LOGPID" 2>/dev/null; then
|
|
info "$1"
|
|
fi
|
|
|
|
for _ in 1 2 3 4 5; do
|
|
if ! kill -0 "$LOGPID" 2>/dev/null; then
|
|
break
|
|
fi
|
|
|
|
sleep 0.2
|
|
done
|
|
|
|
if kill -0 "$LOGPID" 2>/dev/null; then
|
|
kill -9 "$LOGPID" || true
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$1" ]; then
|
|
info 'Closed logs.'
|
|
exit 1
|
|
else
|
|
info "Closed logs. $1"
|
|
fi
|
|
}
|
|
|
|
acquire_lock() {
|
|
lockdir="$1"
|
|
count=0
|
|
|
|
while ! mkdir "$lockdir" 2>/dev/null; do
|
|
count="$((count + 1))"
|
|
if [ "$count" -ge 60 ]; then
|
|
error "Failed to acquire lock $lockdir after 60 seconds"
|
|
return 1
|
|
fi
|
|
info "Waiting for $lockdir..."
|
|
sleep 1
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
release_lock() {
|
|
lockdir="$1"
|
|
rmdir "$lockdir" 2>/dev/null
|
|
}
|
|
|
|
main() {
|
|
if [ "$(id -u)" != 0 ]; then
|
|
error 'Deployer must be ran as root'
|
|
exit 1
|
|
fi
|
|
|
|
basedir='/var/cache/deployd'
|
|
|
|
if [ "$1" = '--global-cleanup' ]; then
|
|
if [ ! -d "$2" ]; then
|
|
error 'Please specify a directory to --global-cleanup which has all the deployer scripts'
|
|
exit 1
|
|
fi
|
|
|
|
dids=''
|
|
|
|
for script in "$2"/*.deployer; do
|
|
deployer="$(abs_path "$script")"
|
|
did="$(echo "$deployer" | sha256sum | cut -d ' ' -f 1)"
|
|
didpath="$basedir/$did"
|
|
|
|
if [ ! -d "$didpath" ]; then
|
|
warn "Deploy ID path for script $script does not exist"
|
|
continue
|
|
fi
|
|
|
|
dids="${dids}:${did};"
|
|
done
|
|
|
|
if [ -z "$dids" ]; then
|
|
error 'No deployers found'
|
|
exit 1
|
|
fi
|
|
|
|
for did in "$basedir"/*/; do
|
|
lockdir="$did/.lock"
|
|
|
|
acquire_lock "$lockdir"
|
|
if echo "$dids" | grep -q ":$(basename "$did");"; then
|
|
info "$did OK"
|
|
else
|
|
info "Removing pointless $did (no script)"
|
|
rm -rf -- "$did"
|
|
fi
|
|
release_lock "$lockdir"
|
|
done
|
|
|
|
info 'Cleanup done'
|
|
|
|
exit $?
|
|
fi
|
|
|
|
if [ ! -f "$1" ]; then
|
|
error "deployer: '$1': No such file or directory"
|
|
exit 1
|
|
fi
|
|
|
|
deployer="$(abs_path "$1")"
|
|
shift
|
|
|
|
did="$(echo "$deployer" | sha256sum | cut -d ' ' -f 1)"
|
|
|
|
didpath="$basedir/$did"
|
|
backup="$didpath/backup"
|
|
upstream="$didpath/upstream"
|
|
lockdir="$didpath/.lock"
|
|
|
|
logfile="$didpath/deploy.log"
|
|
|
|
mkdir -p -- "$didpath"
|
|
|
|
if [ "$1" = '--unlock' ]; then
|
|
warn 'Unlocked the lockdir forcefully'
|
|
release_lock "$lockdir" || true
|
|
shift
|
|
fi
|
|
|
|
case "$1" in
|
|
logs)
|
|
echo "==== logs ($logfile) ===="
|
|
if [ -f "$logfile" ] && [ -r "$logfile" ]; then
|
|
cat -- "$logfile"
|
|
fi
|
|
return 0
|
|
;;
|
|
help)
|
|
print_usage info "$deployer"
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
acquire_lock "$lockdir"
|
|
|
|
trap 'logs_cleanup;release_lock "$lockdir"' INT TERM
|
|
|
|
mkdir -p -- "$didpath"
|
|
if ! cd "$didpath"; then
|
|
error "Failed to change directory to $didpath"
|
|
exit 1
|
|
fi
|
|
|
|
true >"$logfile"
|
|
stdbuf -o0 -e0 tail -f "$logfile" &
|
|
LOGPID="$!"
|
|
|
|
(
|
|
set -e
|
|
|
|
info "Beginning deploy at $(date -u)"
|
|
|
|
if ! run_deploy "$deployer" "$didpath" "$@"; then
|
|
logs_cleanup 'Failed to run deploy!'
|
|
|
|
if [ -d "$backup" ]; then
|
|
rm -rf -- "$upstream"
|
|
cp -a "$backup" "$upstream"
|
|
fi
|
|
|
|
release_lock "$lockdir"
|
|
exit 1
|
|
fi
|
|
|
|
info "Deploy ended at $(date -u)"
|
|
) >"$logfile" 2>&1
|
|
|
|
release_lock "$lockdir"
|
|
logs_cleanup 'Your deploy is ready!'
|
|
}
|
|
|
|
main "$@"
|