From 8d9c15241af8831f454cac318a11b85a0c02c107 Mon Sep 17 00:00:00 2001 From: John J Date: Tue, 16 Jul 2024 20:22:54 +0100 Subject: [PATCH] Added updated script after two years of development --- .deploy-cli/_deployments.sh | 934 ++++++++++++++++++++ .deploy-cli/_extras.sh | 44 + .deploy-cli/_functions.sh | 366 ++++++++ .deploy-cli/_logs.sh | 212 +++++ .deploy-cli/_update.sh | 537 +++++++++++ .deploy-cli/_utils.sh | 240 +++++ .github/workflows/ssh-deployment-update.md | 37 + .github/workflows/ssh-deployment-update.yml | 99 +++ .github/workflows/ssh-deployment.md | 43 + .github/workflows/ssh-deployment.yml | 107 +++ deploy-cli.sh | 240 +++++ deployment.sh | 563 ------------ 12 files changed, 2859 insertions(+), 563 deletions(-) create mode 100644 .deploy-cli/_deployments.sh create mode 100755 .deploy-cli/_extras.sh create mode 100755 .deploy-cli/_functions.sh create mode 100755 .deploy-cli/_logs.sh create mode 100755 .deploy-cli/_update.sh create mode 100644 .deploy-cli/_utils.sh create mode 100644 .github/workflows/ssh-deployment-update.md create mode 100644 .github/workflows/ssh-deployment-update.yml create mode 100644 .github/workflows/ssh-deployment.md create mode 100644 .github/workflows/ssh-deployment.yml create mode 100755 deploy-cli.sh delete mode 100644 deployment.sh diff --git a/.deploy-cli/_deployments.sh b/.deploy-cli/_deployments.sh new file mode 100644 index 0000000..c358907 --- /dev/null +++ b/.deploy-cli/_deployments.sh @@ -0,0 +1,934 @@ +#!/bin/bash + +# This script variables +SCRIPT_NAME="${SCRIPT_NAME:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")" | sed 's/\.[^.]*$//')}" +SCRIPT="${SCRIPT:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")}" +SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}" +SCRIPT_DIR_NAME="${SCRIPT_DIR_NAME:-$(basename "$PWD")}" +SCRIPT_DEBUG=${SCRIPT_DEBUG:-false} + +# Terminal starting directory +STARTING_LOCATION=${STARTING_LOCATION:-"$(pwd)"} + +# Deployment environment +DEPLOYMENT_ENV=${DEPLOYMENT_ENV:-"production"} + +# Enable location targeted deployment +DEPLOYMENT_ENV_LOCATION=${DEPLOYMENT_ENV_LOCATION:-false} + +# Deployment location +ISOLOCATION=${ISOLOCATION:-"GB"} +ISOSTATELOCATION=${ISOSTATELOCATION:-""} + +# Git repo name +GIT_REPO_NAME="${GIT_REPO_NAME:-$(basename "$(git rev-parse --show-toplevel)")}" + +# if using GitHub, Github Details if not ignore +GITHUB_REPO_OWNER="${GITHUB_REPO_OWNER:-$(git remote get-url origin | sed -n 's/.*github.com:\([^/]*\)\/.*/\1/p')}" +GITHUB_REPO_URL="${GITHUB_REPO_URL:-"https://api.github.com/repos/$GITHUB_REPO_OWNER/$GIT_REPO_NAME/commits"}" + +SCRIPT_LOG_FILE=${SCRIPT_LOG_FILE:-"${SCRIPT_DIR}/${SCRIPT_NAME}.log"} +SCRIPT_LOG_EMAIL_FILE=${SCRIPT_LOG_EMAIL_FILE:-"$HOME/${SCRIPT_NAME}.mail.log"} +JSON_FILE_NAME=${JSON_FILE_NAME:-"${SCRIPT_DIR}/${SCRIPT_NAME}_${NOWDATESTAMP}.json"} +SCRIPT_RUNNING_FILE=${SCRIPT_RUNNING_FILE:-"${HOME}/${GIT_REPO_NAME}_running.txt"} + +LATEST_PROJECT_SHA=${LATEST_PROJECT_SHA:-0} + +# START - IMPORT FUNCTIONS +if [[ ! -n "$(type -t _registered)" ]]; then + if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" ]]; then + # shellcheck source=_functions.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" + fi +fi +# END - IMPORT FUNCTIONS + +_registered_deployments() { + # This is used for checking is _function.sh has been imported or not + return 0 +} + +# Function: _calculate_folder_size +# Description: Function to calculate the size of a folder excluding specific directories. +# Parameters: None +# Returns: None + +_calculate_folder_size() { + local folder=$1 + local exclude_dirs=(".git" "laravel/node_modules" "laravel/vendor") + local exclude_opts="" + + for dir in "${exclude_dirs[@]}"; do + exclude_opts+="--exclude='${folder}'/'${dir}' " + done + + du -s --exclude='.*/' "$exclude_opts" "$folder" | awk '{print $1}' +} + +# Function: _delete_old_project_files +# Description: Deletes old project files. +# Parameters: None +# Returns: None + +_delete_old_project_files() { + + [[ ! -d "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}" ]] && return + local old_size new_size size_difference + + # Compare the size of the old and new project folders + old_size=$(_calculate_folder_size "$HOME/${GIT_REPO_NAME}") + new_size=$(_calculate_folder_size "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}") + size_difference=$(echo "scale=2; ($old_size - $new_size) / $old_size * 100" | bc) + + # Check if the old project folder is within 80% of the size of the new project + if (($(echo "$size_difference <= 80" | bc -l))); then + _log_info "Deleted: $HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}" + yes | rm -rf "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}" + else + _log_info "NOT Deleted: $HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}" + fi +} + +# START - PROJECT FUNCTIONS + +# Function: _check_project_secrets +# Description: Checks if the secrets file exists and prompts user to create it if it doesn't exist. +# Parameters: None +# Returns: None + +_check_project_secrets() { + # If no secrets file + if [[ ! -f "$HOME/.${GIT_REPO_NAME}" ]]; then + + # Log the missing file + _log_error "" + _log_error "Failed deployment ${NOWDATESTAMP}" + _log_error "" + _log_error "Missing twisted var file $HOME/.${GIT_REPO_NAME}" + + # If script ran from tty + if [[ "$(_interactive_shell)" = "1" ]]; then + # Ask user if they want to write secret file + read -rp "Write secrets file? [Y/n] (empty: no): " write_file + if [[ $write_file =~ ^(YES|Yes|yes|Y|y)$ ]]; then + _write_project_secrets + fi + fi + # Exit script + _exit_script + fi +} + +# Function: _load_project_secrets +# Description: Checks if the secrets file exists and load it. +# Parameters: None +# Returns: None + +_load_project_secrets() { + # shellcheck disable=SC1090 + [[ -f "$HOME/.${GIT_REPO_NAME}" ]] && source "$HOME/.${GIT_REPO_NAME}" ##|| echo 0 +} + +# Function: _write_project_secrets +# Description: Writes environment variables to a file in the user's home directory. +# Parameters: None +# Returns: None + +_write_project_secrets() { + + cat >"$HOME/.${GIT_REPO_NAME}" </dev/null; then + + # Get the variable name + sec_name="$(echo "$CONFIGLINE" | cut -d '=' -f 1)" + + # Get the variable value + sec_value="$(echo "$CONFIGLINE" | cut -d '=' -f 2-)" + + # While loop grep .env file to replace all found configs + while [[ "$(grep -oF "\"<$sec_name>\"" "$HOME/${GIT_REPO_NAME}/.env")" = "\"<$sec_name>\"" ]]; do + if sed -i 's|"<'"$sec_name"'>"|'"$sec_value"'|' "$HOME/${GIT_REPO_NAME}/.env"; then + # This because it seems, if we act to soon it doesn't write. + sync "$HOME/${GIT_REPO_NAME}/.env" + # Sleep for 1 second + sleep 0.2 + fi + done + fi + fi + done <"$HOME/.${GIT_REPO_NAME}" + + # Replace deployment variables + while grep -F "\"\"" "$HOME/${GIT_REPO_NAME}/.env" &>/dev/null; do + sed -i "s|\"\"|$LATEST_PROJECT_SHA|" "$HOME/${GIT_REPO_NAME}/.env" + sync "$HOME/${GIT_REPO_NAME}/.env" + sleep 0.2s + done + sed -i "s|\"\"|$NOWDATESTAMP|" "$HOME/${GIT_REPO_NAME}/.env" + + # Call sync on .env file for inode changes + sync "$HOME/${GIT_REPO_NAME}/.env" + + # _log_info "END: Replacing APP environment variables" +} + +# Function: _get_project_docker_compose_file +# Description: Locates projects docker compose file. +# Parameters: None +# Returns: +# 0 if failed to locate docker-compose yml file +# File path to project docker compose file + +_get_project_docker_compose_file() { + local docker_compose_file="0" + + # Check if docker-compose is installed + if [[ "$(_is_present docker-compose)" = "1" ]]; then + + # Check for the default docker-compose yml file + if [[ -f "$HOME/${GIT_REPO_NAME}/docker-compose.yml" ]]; then + docker_compose_file="$HOME/${GIT_REPO_NAME}/docker-compose.yml" + fi + # Check for docker compose file with deployment environment tag + if [[ -f "$HOME/${GIT_REPO_NAME}/docker-compose.${DEPLOYMENT_ENV}.yml" ]]; then + docker_compose_file="$HOME/${GIT_REPO_NAME}/docker-compose.${DEPLOYMENT_ENV}.yml" + fi + fi + + # Return results + echo "${docker_compose_file}" +} + +# END - PROJECT FUNCTIONS + +# START - GIT SERVICES + +# Function: _git_service_provider +# Description: Returns git service providers domain. +# Parameters: None +# Returns: None + +_git_service_provider() { + # shellcheck disable=SC2164 + cd "$HOME"/"${GIT_REPO_NAME}" + local git_domain + + git_domain=$(git remote get-url origin | awk -F'@|:' '{gsub("//", "", $2); print $2}') + echo "$git_domain" +} + +# END - GIT SERVICES + +# START - GITHUB TOKEN + +# Function: _check_github_token +# Description: Check $GITHUB_TOKEN variable has been set and matches the github personal token pattern. +# Parameters: None +# Returns: +# 1 if successfully loaded github token and matches pattern + +_check_github_token() { + local pattern="^ghp_[a-zA-Z0-9]{36}$" + [[ ${GITHUB_TOKEN:-"ghp_##"} =~ $pattern ]] && echo 1 +} + +# Function: _check_github_token_file +# Description: Check the location for the github token file. +# Parameters: None +# Returns: +# 1 if github token file exists, otherwise 0. + +_check_github_token_file() { + [[ -f "$HOME/.github_token" ]] && echo 1 +} + +# Function: _load_github_token +# Description: If github token already has been loaded or check and loads from file then validate. +# Parameters: None +# Returns: +# 1 if github token already loaded or loads token from file and matches pattern, otherwise 0. + +_load_github_token() { + # Call _check_github_token to vildate current token variable. + if [[ $(_check_github_token) = "1" ]]; then + return + fi + + # Call function to check for token file + if [[ "$(_check_github_token_file)" = "1" ]]; then + # shellcheck source=/dev/null + source "$HOME/.github_token" || echo "Failed import of github_token" + fi +} + +# Function: _write_github_token +# Description: Given a gh token or from user prompt, validate and creates .github_token file. +# Parameters: +# $1: optional github token +# Returns: +# 1 if successfully installed github token. + +#shellcheck disable=SC2120 +_write_github_token() { + local pattern="^ghp_[a-zA-Z0-9]{36}$" + local token + + # If function has param + if [[ $# -ge 1 ]]; then + # Use the param $1 as token + token=$1 + elif [[ "$(_interactive_shell)" = "1" ]]; then # If run from tty + + # Create user interaction to get token from user. + read -rp "Please provide Github personal access token (empty: cancel): " input_token + + token="$input_token" + + # Check user input token against pattern above. + if [[ ! $token =~ $pattern ]]; then + # Log error and exit script + # _log_error "Missing github token file .github_token" + _log_error "GITHUB_TOKEN=ghp_azAZ09azAZ09azAZ09azAZ09azAZ09azAZ09" + _log_error "public_repo, read:packages, repo:status, repo_deployment" + _log_error "Invalid github personal access token." + _exit_script + fi + fi + + # If give token matches pattern + if [[ $token =~ $pattern ]]; then + # Create github token file + echo "#" >"$HOME"/.github_token + echo "GITHUB_TOKEN=$token" >>"$HOME"/.github_token + echo "" >>"$HOME"/.github_token + chmod 700 "$HOME"/.github_token + # Load github token + _load_github_token + # Return success + echo 1 + else + # Log error and exit script + _log_error "Invalid github personal access token." + _exit_script + fi +} + +# END - GITHUB TOKEN + +# START - GITHUB API + +# Function: _get_project_github_latest_sha +# Description: Gets project files latest git commit sha from github. +# Parameters: None +# Returns: +# 0 - if failed to get latest git commit sha +# github commit sha + +_get_project_github_latest_sha() { + + # Load the github token if not loaded + _load_github_token + + # Validate loaded token + if [[ "$(_check_github_token)" = "0" ]]; then + # On fail ask user to create token + _write_github_token + fi + + # Create local function variable + local curl_data gh_sha + + # Send request to github with creds + curl_data=$(curl -s -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version:2022-11-28" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + "$GITHUB_REPO_URL") + + # Check returned data from request + if [[ $(echo "$curl_data" | jq -r .message 2>/dev/null && echo 1) ]]; then + # Log error and return fail from function + _log_to_file "$(echo "$curl_data" | jq .message)" + echo 0 + return + fi + + # Validate commit sha and return. + if [[ $(echo "$curl_data" | jq -r .[0].commit.tree.sha 2>/dev/null && echo 1) ]]; then + gh_sha="$(echo "$curl_data" | jq .[0].commit.tree.sha)" + echo "${gh_sha//\"/}" + return + fi + + # Return fail code. + echo 0 +} + +# END - GITHUB API + +# START - ONEDEV TOKEN + +# Function: _check_onedev_token +# Description: Check $GITHUB_TOKEN variable has been set and matches the onedev personal token pattern. +# Parameters: None +# Returns: +# 1 if successfully loaded github token and matches pattern + +_check_onedev_token() { + local pattern="^[A-Za-z0-9]+$" + [[ ${ONEDEV_TOKEN:-"######"} =~ $pattern ]] && echo 1 +} + +# Function: _check_onedev_file +# Description: Check the location for the onedev token file. +# Parameters: None +# Returns: +# 1 if github token file exists, otherwise 0. + +_check_onedev_file() { + [[ -f "$HOME/.onedev_auth" ]] && echo 1 +} + +# Function: _load_onedev_token +# Description: If onedev token already has been loaded or check and loads from file then validate. +# Parameters: None +# Returns: +# 1 if github token already loaded or loads token from file and matches pattern, otherwise 0. + +_load_onedev_token() { + if [[ $(_check_onedev_token) = "1" ]]; then + return + fi + + if [[ "$(_check_onedev_file)" = "1" ]]; then + # shellcheck source=/dev/null + source "$HOME/.onedev_auth" || echo "Failed import of onedev_auth" + fi +} + +# Function: _write_onedev_token +# Description: Given a onedev token or from user prompt, validate and creates .onedev_token file. +# Parameters: +# $1: optional github token +# Returns: +# 1 if successfully installed github token. + +# shellcheck disable=SC2120 +_write_onedev_token() { + # Set local function variables + local pattern="^[A-Za-z0-9]+$" + local token username + + # If function has been given 1 argument + if [[ $# -ge 1 ]]; then + # Use the param $1 as token + token=$1 + elif [[ "$(_interactive_shell)" = "1" ]]; then # If run from tty + + # Create user interaction to get token from user. + read -rp "Please provide OneDev Access Token (empty: cancel): " input_token + + token="$input_token" + + # Check user input token against pattern above. + if [[ ! $token =~ $pattern ]]; then + # Log error and exit script + # _log_error "Missing github token file .onedev_auth" + _log_error "ONEDEV_TOKEN=########" + _log_error "ONEDEV_USERNAME=######" + _exit_script + fi + fi + + # If give token matches pattern + if [[ $token =~ $pattern ]]; then + + # Write token file + echo "#" >"$HOME"/.onedev_auth + echo "ONEDEV_TOKEN=$token" >>"$HOME"/.onedev_auth + + # If function has been given 2 arguments + if [[ $# -ge 2 ]]; then + username="$2" + else + # Create user interaction to get username from user. + read -rp "Please provide OneDev Username (empty: cancel): " input_username + username="$input_username" + fi + + # Add username variable to token file + echo "ONEDEV_USERNAME=$username" >>"$HOME"/.onedev_auth + + echo "" >>"$HOME"/.onedev_auth + chmod 700 "$HOME"/.onedev_auth + + # Load token from newly create token file + _load_onedev_token + echo 1 + else + # Log error and exit script + _log_error "Invalid github personal access token." + _exit_script + fi +} + +# END - GITHUB TOKEN + +# START - ONEDEV API + +# Function: _get_project_onedev_latest_sha +# Description: Gets project files latest git commit sha from onedev. +# Parameters: None +# Returns: +# 0 - if failed to get latest git commit sha +# github commit sha + +_get_project_onedev_latest_sha() { + # Call function to load token if not loaded + _load_onedev_token + + # Run check on token variable + if [[ "$(_check_onedev_token)" != "1" ]]; then + # Ask user to full missing token + _write_onedev_token + fi + + # Set local function variables + local curl_data project_id onedev_sha + + cd "$HOME"/"${GIT_REPO_NAME}" || _exit_script + local git_domain git_url + + # Calling _git_service_provider function to check git provider from .git data + git_url=$(git remote get-url origin) + git_domain="$(_git_service_provider)" + + # URL to process + local query='query="Name" is "'${GIT_REPO_NAME}'"' + + cleaned_url="${git_url#*://}" # Remove "http://" or "https://" + cleaned_url="${cleaned_url#*/*}" # Remove "git.xoren.io:6611/" + cleaned_url="${cleaned_url/\/$GIT_REPO_NAME/}" # Remove "git.xoren.io" + + if [[ ${#cleaned_url} -ge 1 ]]; then + query+=' and children of "'${cleaned_url}'"' + fi + ## Enable for debugging. + # _log_to_file "query: $query" + + # Send request to git api to get id of repo + curl_data=$(curl -s -u "${ONEDEV_USERNAME}:${ONEDEV_TOKEN}" \ + -G https://git.xoren.io/~api/projects \ + --data-urlencode "${query}" \ + --data-urlencode offset=0 --data-urlencode count=100) + + # Check request returning data + if [[ ! $(echo "$curl_data" | jq .[0].id 2>/dev/null && echo 1) ]]; then + # Error in api response, log and return fail from this function. + _log_to_file "Cant find project id from git api" + echo 0 + return + fi + + # Set if from request data + project_id="$(echo "$curl_data" | jq .[0].id)" + + # Send request to git repo api for commit data + curl_data=$(curl -s -u "${ONEDEV_USERNAME}:${ONEDEV_TOKEN}" \ + -G "https://git.xoren.io/~api/repositories/${project_id}/commits" \ + --data-urlencode count=1) + + # Check request returning data + if [[ $(echo "$curl_data" | jq -r .[0] 2>/dev/null && echo 1) ]]; then + + # On success echo back sha + onedev_sha="$(echo "$curl_data" | jq .[0])" + echo "${onedev_sha//\"/}" + return + fi + + # Return error code if failed above check. + echo 0 +} + +# END - ONEDEV API + +# START - UPDATE FUNCTIONS + +# Function: _check_latest_sha +# Description: Sets LATEST_PROJECT_SHA via _set_latest_sha function, if LATEST_PROJECT_SHA not already set. +# Parameters: None +# Returns: None + +_check_latest_sha() { + local sha_length + + # Check if LATEST_PROJECT_SHA isn't set + if [[ -z "${LATEST_PROJECT_SHA}" ]]; then + # Call function to set LATEST_PROJECT_SHA + LATEST_PROJECT_SHA="$(_set_latest_sha true)" + else + # If LATEST_PROJECT_SHA is set check length + sha_length=${#LATEST_PROJECT_SHA} + if ((sha_length <= 31)); then + # If LATEST_PROJECT_SHA length is smaller then 32 + LATEST_PROJECT_SHA="$(_set_latest_sha true)" + fi + fi +} + +# Function: _set_latest_sha +# Description: Checks git repo provider and gets sha from provider api. +# Parameters: None +# $1: (optional) echo SHA +# Returns: None +# SHA: if + +_set_latest_sha() { + cd "$HOME"/"${GIT_REPO_NAME}" || _exit_script + local git_domain + + # Calling _git_service_provider function to check git provider from .git data + git_domain="$(_git_service_provider)" + + # Check git provider host again known list + if echo "$git_domain" | grep -q github.com; then + # Set LATEST_PROJECT_SHA from github api function + LATEST_PROJECT_SHA="$(_get_project_github_latest_sha)" + if [[ $# -ge 1 ]]; then + echo "$LATEST_PROJECT_SHA" + fi + return + elif [[ "$git_domain" = "git.xoren.io" ]]; then + # Set LATEST_PROJECT_SHA from onedev api function + LATEST_PROJECT_SHA="$(_get_project_onedev_latest_sha)" + if [[ $# -ge 1 ]]; then + echo "$LATEST_PROJECT_SHA" + fi + return + else + if [[ $# -ge 1 ]]; then + echo 0 + return + fi + # Unknown or no provider + _log_error "Cant find git host." + _exit_script + fi +} + +# Function: _check_update +# Description: Checks if the local version matches the remote version of the repository. +# If the versions match, the script will exit. +# If the versions do not match, the script will perform an update and update the local version. +# Parameters: None +# Returns: None + +_check_update() { + + if [[ "$(_check_working_schedule)" = "1" ]]; then + _exit_script + fi + + # Call function to set if not set latest project sha. + _check_latest_sha + # LATEST_PROJECT_SHA="$(_check_latest_sha true)" + + # If LATEST_PROJECT_SHA equals 0. + if [[ "${LATEST_PROJECT_SHA}" = "0" ]]; then + # Log error and exit scripts + _log_error "Failed to fetching SHA from git api service" + _exit_script + fi + # If LATEST_PROJECT_SHA is blank. + if [[ "${LATEST_PROJECT_SHA}" = "" ]]; then + # Log error and exit scripts + _log_error "Failed to fetching SHA from git api service" + _exit_script + fi + + # Check for default value. + if [[ "${DEPLOYMENT_VERSION}" = "" ]]; then + + # Replace with requested data version. + _log_error "Current version AKA deployment failure somewhere" + _update + elif [[ "${DEPLOYMENT_VERSION}" = "DEV" ]]; then + + _log_error "Updating is disabled in development" + else + + # If local version and remote version match. + if [[ "${DEPLOYMENT_VERSION}" = "${LATEST_PROJECT_SHA}" ]]; then + + if [[ "$(_interactive_shell)" = "1" ]]; then + _log_info "VERSION MATCH, ending script" + fi + _exit_script + fi + + # Finally run the update function + _update + fi +} + +# Function: _update +# Description: Performs re-deployment of the project by cloning a fresh copy from GitHub and updating project files. +# It also moves the old project folder to a backup location. +# The function replaces environment variables and propagates the environment file. +# Parameters: None +# Returns: None + +# shellcheck disable=SC2120 +_update() { + local sha + sha="$1" + + # Set local variable. + local docker_compose_file="0" + + cd "$HOME/$GIT_REPO_NAME" || _exit_script + + # Set local variable using _get_project_docker_compose_file function + docker_compose_file="$(_get_project_docker_compose_file)" + + # Check for _update.sh script to overwrite or provide update functions + # if [[ -f "$HOME/${GIT_REPO_NAME}/_update.sh" ]]; then + # shellcheck disable=SC1090 + # source "$HOME/${GIT_REPO_NAME}/_update.sh" + # fi + + if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_update.sh" ]]; then + # shellcheck source=_update.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_update.sh" + fi + # Log the re-deployment + _log_to_file "Re-deployment Started" + _log_to_file "=====================" + _log_to_file "env: ${DEPLOYMENT_ENV}" + + # Enter project repo + cd "$HOME/$GIT_REPO_NAME" || _exit_script + + # Check if the function is set + if [[ -n "$(type -t _pre_update)" ]]; then + _pre_update + fi + + # Leave project directory + cd "$HOME" || _exit_script + + # Call function to download fresh copy of project + _download_project_files + + if [ -n "$sha" ]; then + cd "$HOME"/"${GIT_REPO_NAME}" || _exit_script + git checkout "$sha" 1>/dev/null 2>&1 + fi + + # Move any log or json files + if ls "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}/"*.log 1>/dev/null 2>&1; then + mv "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}/"*.log "$HOME/${GIT_REPO_NAME}/" + fi + if ls "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}/"*.json 1>/dev/null 2>&1; then + mv "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}/"*.json "$HOME/${GIT_REPO_NAME}/" + fi + + # Log the download finishing + _log_to_file "Finished cloning fresh copy from ${GITHUB_REPO_OWNER}/${GIT_REPO_NAME}." + + # Replace .env file + _replace_env_project_secrets + + # Check if _post_update function has been set + if [[ -n "$(type -t _post_update)" ]]; then + _post_update + else + # If no _post_update function + if [[ "$DOCKER_IS_PRESENT" = "1" && "${docker_compose_file}" != "0" ]]; then + docker-compose -f "${docker_compose_file}" up -d --build + fi + + # Call function to delete old project files if condition match + _delete_old_project_files + fi + + # Log the finishing of the update + _log_to_file "Finished updated project files." + _log_to_file "" +} + +# Function: _download_project_files +# Description: Performs re-download of the project files by cloning a fresh copy via git and updating project files. +# It also moves the old project folder to a backup location. +# The function replaces environment variables and propagates the environment file. +# Parameters: None +# Returns: None + +_download_project_files() { + + cd "$HOME"/"${GIT_REPO_NAME}" || _exit_script + + GIT_URL=$(git remote get-url origin) + + # Leave project folder. + cd "$HOME" || _exit_script + + # Log the folder move. + _log_to_file "Moving old project folder." + + # Delete old environment secret. + [[ -f "$HOME/${GIT_REPO_NAME}/.env" ]] && rm "$HOME/${GIT_REPO_NAME}/.env" + + # Remove old project directory. + mv -u -f "$HOME/$GIT_REPO_NAME" "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}" + + # Call inode sync. + sync "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}" + + # Run git clone in if to error check. + if ! git clone --quiet "${GIT_URL}"; then # If failed to git clone + + # Move old project files back to latest directory + mv -u -f "$HOME/${GIT_REPO_NAME}_${NOWDATESTAMP}" "$HOME/$GIT_REPO_NAME" + + # Log the error + _log_error "Cant contact to $(_git_service_provider)" + + fi + + # Call inode sync + sync "$HOME/${GIT_REPO_NAME}" +} + +# END - UPDATE FUNCTIONS + +# START - COMPLETION +# _script_completion() { +# local cur prev opts +# COMPREPLY=() +# cur="${COMP_WORDS[COMP_CWORD]}" +# prev="${COMP_WORDS[COMP_CWORD-1]}" +# opts="user:add linux:install setup:hpages setup:ssh:keys setup:certbot setup:git:profile setup:well-known certbot:add system:json repo:check repo:update queue:worker config:backup config:restore version:local version:remote" + +# case "${prev}" in +# certbot:add) +# # Custom completion for certbot:add option +# COMPREPLY=($(compgen -f -- "${cur}")) +# return 0 +# ;; +# user:add) +# # Custom completion for user:add option +# COMPREPLY=($(compgen -f -- "${cur}")) +# return 0 +# ;; +# *) +# ;; +# esac + +# COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) +# return 0 +# } + +# complete -F _script_completion "${SCRIPT}" + +# END - COMPLETION + +# START - SETUP + +# Function: _setup +# Description: Sets up the Linux environment for hosting. +# Parameters: None +# Returns: None + +_setup() { + _install_linux_dep + _ensure_project_secrets_file + # _install_update_cron + _check_update + + _send_email "Setup $GIT_REPO_NAME on $(hostname)" +} diff --git a/.deploy-cli/_extras.sh b/.deploy-cli/_extras.sh new file mode 100755 index 0000000..0ce9733 --- /dev/null +++ b/.deploy-cli/_extras.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# This script variables +SCRIPT_NAME="${SCRIPT_NAME:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")" | sed 's/\.[^.]*$//')}" +SCRIPT="${SCRIPT:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")}" +SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}" +SCRIPT_DIR_NAME="${SCRIPT_DIR_NAME:-$(basename "$PWD")}" +SCRIPT_DEBUG=${SCRIPT_DEBUG:-false} + +# Terminal starting directory +STARTING_LOCATION=${STARTING_LOCATION:-"$(pwd)"} + +# Deployment environment +DEPLOYMENT_ENV=${DEPLOYMENT_ENV:-"production"} + +# Enable location targeted deployment +DEPLOYMENT_ENV_LOCATION=${DEPLOYMENT_ENV_LOCATION:-false} + +# Deployment location +ISOLOCATION=${ISOLOCATION:-"GB"} +ISOSTATELOCATION=${ISOSTATELOCATION:-""} + +# Git repo name +GIT_REPO_NAME="${GIT_REPO_NAME:-$(basename "$(git rev-parse --show-toplevel)")}" + +# if using GitHub, Github Details if not ignore +GITHUB_REPO_OWNER="${GITHUB_REPO_OWNER:-$(git remote get-url origin | sed -n 's/.*github.com:\([^/]*\)\/.*/\1/p')}" +GITHUB_REPO_URL="${GITHUB_REPO_URL:-"https://api.github.com/repos/$GITHUB_REPO_OWNER/$GIT_REPO_NAME/commits"}" + +SCRIPT_LOG_FILE=${SCRIPT_LOG_FILE:-"${SCRIPT_DIR}/${SCRIPT_NAME}.log"} +SCRIPT_LOG_EMAIL_FILE=${SCRIPT_LOG_EMAIL_FILE:-"$HOME/${SCRIPT_NAME}.mail.log"} +JSON_FILE_NAME=${JSON_FILE_NAME:-"${SCRIPT_DIR}/${SCRIPT_NAME}_${NOWDATESTAMP}.json"} +SCRIPT_RUNNING_FILE=${SCRIPT_RUNNING_FILE:-"${HOME}/${GIT_REPO_NAME}_running.txt"} + +LATEST_PROJECT_SHA=${LATEST_PROJECT_SHA:-0} + +# START - IMPORT FUNCTIONS +if [[ ! -n "$(type -t _registered)" ]]; then + if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" ]]; then + # shellcheck source=_functions.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" + fi +fi +# END - IMPORT FUNCTIONS diff --git a/.deploy-cli/_functions.sh b/.deploy-cli/_functions.sh new file mode 100755 index 0000000..0e85659 --- /dev/null +++ b/.deploy-cli/_functions.sh @@ -0,0 +1,366 @@ +#!/bin/bash + +# Author: admin@xoren.io +# Script: _functions.sh +# Link https://github.com/xorenio +# Description: Functions script. + +### +#INDEX +## Function +# _log_error() +# _log_info() +# _log_debug() +# _log_success() +# _log_data() +# _log_to_file() +# _log_console() +# _create_running_file() +# _check_running_file() +# _delete_running_file() +# _exit_script() +# _in_working_schedule() +# _check_working_schedule() +# _is_present() +# _is_file_open() +# _is_in_screen() +# _interactive_shell() +# _wait_pid_expirer() +# _install_cronjob() +# _remove_cronjob() +# _calculate_folder_size() +# _delete_old_project_files() +# _valid_ip() +# _set_location_var() +# _check_project_secrets() +# _load_project_secrets() +# _write_project_secrets() +# _ensure_project_secrets_file() +# _replace_env_project_secrets() +# _get_project_docker_compose_file() +# _install_update_cron() +# _remove_update_cron() +# _git_service_provider() +# _check_github_token() +# _check_github_token_file() +# _load_github_token() +# _write_github_token() +# _get_project_github_latest_sha() +# _check_onedev_token() +# _check_onedev_file() +# _load_onedev_token() +# _write_onedev_token() +# _get_project_onedev_latest_sha() +# _download_project_files() +# _update() +# _set_latest_sha() +# _check_latest_sha() +# _check_update() +# _script_completion() +# _setup() + +# Defaulting variables +NOWDATESTAMP="${NOWDATESTAMP:-$(date "+%Y-%m-%d_%H-%M-%S")}" + +# This script variables +SCRIPT_NAME="${SCRIPT_NAME:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")" | sed 's/\.[^.]*$//')}" +SCRIPT="${SCRIPT:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")}" +SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}" +SCRIPT_DIR_NAME="${SCRIPT_DIR_NAME:-$(basename "$PWD")}" +SCRIPT_DEBUG=${SCRIPT_DEBUG:-false} + +# Terminal starting directory +STARTING_LOCATION=${STARTING_LOCATION:-"$(pwd)"} + +# Deployment environment +DEPLOYMENT_ENV=${DEPLOYMENT_ENV:-"production"} + +# Enable location targeted deployment +DEPLOYMENT_ENV_LOCATION=${DEPLOYMENT_ENV_LOCATION:-false} + +# Deployment location +ISOLOCATION=${ISOLOCATION:-"GB"} +ISOSTATELOCATION=${ISOSTATELOCATION:-""} + +# Git repo name +GIT_REPO_NAME="${GIT_REPO_NAME:-$(basename "$(git rev-parse --show-toplevel)")}" + +# if using GitHub, Github Details if not ignore +GITHUB_REPO_OWNER="${GITHUB_REPO_OWNER:-$(git remote get-url origin | sed -n 's/.*github.com:\([^/]*\)\/.*/\1/p')}" +GITHUB_REPO_URL="${GITHUB_REPO_URL:-"https://api.github.com/repos/$GITHUB_REPO_OWNER/$GIT_REPO_NAME/commits"}" + +SCRIPT_LOG_FILE=${SCRIPT_LOG_FILE:-"${SCRIPT_DIR}/${SCRIPT_NAME}.log"} +SCRIPT_LOG_EMAIL_FILE=${SCRIPT_LOG_EMAIL_FILE:-"$HOME/${SCRIPT_NAME}.mail.log"} +JSON_FILE_NAME=${JSON_FILE_NAME:-"${SCRIPT_DIR}/${SCRIPT_NAME}_${NOWDATESTAMP}.json"} +SCRIPT_RUNNING_FILE=${SCRIPT_RUNNING_FILE:-"${HOME}/${GIT_REPO_NAME}_running.txt"} + +LATEST_PROJECT_SHA=${LATEST_PROJECT_SHA:-0} +# Working Schedule +# This is referenced in the update check function and will exclude updating in given time frames, or false to disable +# Define a single string with time ranges, where ranges can be specified like 1-5:07:00-16:00 +# Format: day(s):start_time-end_time|... +# Example: +# Monday to Friday: 1-5:07:00-16:00 +# Saturday and Sunday: 6-7:09:00-15:00 +# WORKING_SCHEDULE=${WORKING_SCHEDULE:-"1-5:07:00-16:00|6:20:00-23:59"} +WORKING_SCHEDULE=false + +_registered() { + # This is used for checking is _function.sh has been imported or not + echo 1 +} + +# START - LOGS +if [[ ! -n "$(type -t _registered_logs)" ]]; then + if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_logs.sh" ]]; then + # shellcheck source=_functions.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_logs.sh" + fi +fi +# END - LOGS + +# START - UTILS +if [[ ! -n "$(type -t _registered_utils)" ]]; then + if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_utils.sh" ]]; then + # shellcheck source=_functions.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_utils.sh" + fi +fi +# END - UTILS + +# START - DEPLOYMENTS +if [[ ! -n "$(type -t _registered_deployments)" ]]; then + if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_deployments.sh" ]]; then + # shellcheck source=_functions.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_deployments.sh" + fi +fi +# END - DEPLOYMENTS + +# START - RUNNING FILE + +# Function: _create_running_file +# Description: Creates a running file with the current date and time. +# Parameters: None +# Returns: None + +_create_running_file() { + echo "${NOWDATESTAMP}" >"${SCRIPT_RUNNING_FILE}" +} + +# Function: _check_running_file +# Description: Checks if the running file exists and exits the script if it does. +# Parameters: None +# Returns: None + +_check_running_file() { + # If running file exists + if [[ -f "${SCRIPT_RUNNING_FILE}" ]]; then + # Log and hard exit + _log_info "Script already running." + exit + fi +} + +# Function: _delete_running_file +# Description: Deletes the running file. +# Parameters: None +# Returns: None + +_delete_running_file() { + # If running file exists delete it + if [[ -f "${SCRIPT_RUNNING_FILE}" ]]; then + rm "${SCRIPT_RUNNING_FILE}" + fi + # Return users tty to starting directory or home or do nothing. + cd "${STARTING_LOCATION}" || cd "$HOME" || return +} + +# END - RUNNING FILE + +# START - WORKING SCHEDULE + +# Function: _in_working_schedule +# Description: Validate working schedule variable and checks if in time. +# Parameters: None +# Returns: +# 0: Not in working hours +# 1: In configured working hours. +# exit: Invalid working schedule variable. + +_in_working_schedule() { + local pattern="^[0-7]-[0-7]:[0-2][0-9]:[0-5][0-9]-[0-2][0-9]:[0-5][0-9]$" + if [[ ! $WORKING_SCHEDULE =~ $pattern ]]; then + _log_error "Invalid WORKING_SCHEDULE format. Please use the format: day(s):start_time-end_time." + _exit_script + fi + + # Get the current day of the week (1=Monday, 2=Tuesday, ..., 7=Sunday) + day_of_week=$(date +%u) + + # Get the current hour (in 24-hour format) + current_hour=$(date +%H) + + # Define a single string with time ranges, where ranges can be specified like 1-5:07:00-16:00 + # Format: day(s):start_time-end_time|... + # e.g., "1-5:07:00-16:00|6:09:00-15:00|7:09:00-15:00" + # SCRIPT_SCHEDULE="1-5:07:00-16:00|6:09:00-15:00|7:09:00-15:00" + + # Split the time_ranges string into an array using the pipe '|' delimiter + IFS="|" read -ra ranges <<<"$WORKING_SCHEDULE" + + # Initialize a variable to store the current day's time range + current_day_schedule="" + + # Iterate through the time ranges to find the one that matches the current day + for range in "${ranges[@]}"; do + days="${range%%:*}" + times="${range#*:}" + start_day="${days%%-*}" + end_day="${days##*-}" + + if [ "$day_of_week" -ge "$start_day" ] && [ "$day_of_week" -le "$end_day" ]; then + current_day_schedule="$times" + break + fi + done + + if [ -n "$current_day_schedule" ]; then + start_time="${current_day_schedule%%-*}" + end_time="${current_day_schedule##*-}" + + if [ "$current_hour" -ge "$start_time" ] && [ "$current_hour" -le "$end_time" ]; then + _log_error "Script is running within the allowed time range. Stopping..." + echo 1 + return + fi + fi + echo 0 +} + +# Function: _check_working_schedule +# Description: Check working variable doesn't equals false and runs in working schedule function +# Parameters: None +# Returns: +# 0: Not in working hours +# 1: In configured working hours + +_check_working_schedule() { + + # Check for update exclude + if [[ "$WORKING_SCHEDULE" != "false" ]]; then + + _in_working_schedule + return + fi + echo 0 +} +# END - WORKING SCHEDULE + +# START - HELPER FUNCTIONS + +# START - UPDATE CRONJOB + +# Function: _install_update_cron +# Description: Sets up the update project cronjob. +# Parameters: None +# Returns: None + +_install_update_cron() { + # shellcheck disable=SC2005 + echo "$(_install_cronjob "*/15 * * * *" "/bin/bash $HOME/${GIT_REPO_NAME}/${SCRIPT} version:check")" +} + +# Function: _remove_update_cron +# Description: Removes the update project cronjob. +# Parameters: None +# Returns: None + +_remove_update_cron() { + # shellcheck disable=SC2005 + echo "$(_remove_cronjob "*/15 * * * *" "/bin/bash $HOME/${GIT_REPO_NAME}/${SCRIPT} version:check")" +} + +# END - UPDATE CRONJOB + +# Function: _setup_ssh_key +# Description: Sets up an ED25519 ssh key for the root user. +# Parameters: None +# Returns: None + +_setup_ssh_key() { + + _log_info "Checking ssh key" + + if [[ ! -f "$HOME/.ssh/id_ed25519" ]]; then + _log_info "Creating ed25519 ssh key" + ssh-keygen -t ed25519 -N "" -C "${GIT_EMAIL}" -f "$HOME/.ssh/id_ed25519" >/dev/null 2>&1 + _log_info "Public: $(cat "$HOME/.ssh/id_ed25519.pub")" + eval "$(ssh-agent -s)" + ssh-add "$HOME/.ssh/id_ed25519" + fi +} + +# END - HELPER FUNCTIONS + +# START - HELPER VARIABLES + +# shellcheck disable=SC2034 +APT_IS_PRESENT="$(_is_present apt-get)" +# shellcheck disable=SC2034 +YUM_IS_PRESENT="$(_is_present yum)" +# shellcheck disable=SC2034 +PACMAN_IS_PRESENT="$(_is_present pacman)" +# shellcheck disable=SC2034 +ZYPPER_IS_PRESENT="$(_is_present zypper)" +# shellcheck disable=SC2034 +DNF_IS_PRESENT="$(_is_present dnf)" +# shellcheck disable=SC2034 +DOCKER_IS_PRESENT="$(_is_present docker)" +# END - HELPER VARIABLES + +# START - SET DISTRO VARIABLES + +if [[ "$APT_IS_PRESENT" = "1" ]]; then + PM_COMMAND=apt-get + PM_INSTALL=(install -y) + PREREQ_PACKAGES="docker docker-compose whois jq yq curl git bc parallel screen sendmail" +elif [[ "$YUM_IS_PRESENT" = "1" ]]; then + PM_COMMAND=yum + PM_INSTALL=(-y install) + PREREQ_PACKAGES="docker docker-compose whois jq yq curl git bc parallel screen sendmail" +elif [[ "$PACMAN_IS_PRESENT" = "1" ]]; then + PM_COMMAND=pacman + PM_INSTALL=(-S --noconfirm) + PREREQ_PACKAGES="docker docker-compose whois jq yq curl git bc parallel screen sendmail" +elif [[ "$ZYPPER_IS_PRESENT" = "1" ]]; then + PM_COMMAND=zypper + PM_INSTALL=(install -y) + PREREQ_PACKAGES="docker docker-compose whois jq yq curl git bc parallel screen sendmail" +elif [[ "$DNF_IS_PRESENT" = "1" ]]; then + PM_COMMAND=dnf + PM_INSTALL=(install -y) + PREREQ_PACKAGES="docker docker-compose whois jq yq curl git bc parallel screen sendmail" +else + _log_error "This system doesn't appear to be supported. No supported package manager (apt/yum/pacman/zypper/dnf) was found." + exit +fi + +# Function: _install_linux_dep +# Description: Installed Linux dependencies. +# Parameters: None +# Returns: None + +_install_linux_dep() { + # Install prerequisites + $PM_COMMAND "${PM_INSTALL[@]}" $PREREQ_PACKAGES +} + +# END - SET DISTRO VARIABLES + +# START - EXTRAS +if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_extras.sh" ]]; then + # shellcheck source=_update.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_extras.sh" +fi +# END -EXTRAS diff --git a/.deploy-cli/_logs.sh b/.deploy-cli/_logs.sh new file mode 100755 index 0000000..aba5b48 --- /dev/null +++ b/.deploy-cli/_logs.sh @@ -0,0 +1,212 @@ +#!/bin/bash + +# Defaulting variables +NOWDATESTAMP="${NOWDATESTAMP:-$(date "+%Y-%m-%d_%H-%M-%S")}" + +# This script variables +SCRIPT_NAME="${SCRIPT_NAME:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")" | sed 's/\.[^.]*$//')}" +SCRIPT="${SCRIPT:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")}" +SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}" +SCRIPT_DIR_NAME="${SCRIPT_DIR_NAME:-$(basename "$PWD")}" +SCRIPT_DEBUG=${SCRIPT_DEBUG:-false} + +# Terminal starting directory +STARTING_LOCATION=${STARTING_LOCATION:-"$(pwd)"} + +# Deployment environment +DEPLOYMENT_ENV=${DEPLOYMENT_ENV:-"production"} + +# Enable location targeted deployment +DEPLOYMENT_ENV_LOCATION=${DEPLOYMENT_ENV_LOCATION:-false} + +# Deployment location +ISOLOCATION=${ISOLOCATION:-"GB"} +ISOSTATELOCATION=${ISOSTATELOCATION:-""} + +# Git repo name +GIT_REPO_NAME="${GIT_REPO_NAME:-$(basename "$(git rev-parse --show-toplevel)")}" + +# if using GitHub, Github Details if not ignore +GITHUB_REPO_OWNER="${GITHUB_REPO_OWNER:-$(git remote get-url origin | sed -n 's/.*github.com:\([^/]*\)\/.*/\1/p')}" +GITHUB_REPO_URL="${GITHUB_REPO_URL:-"https://api.github.com/repos/$GITHUB_REPO_OWNER/$GIT_REPO_NAME/commits"}" + +SCRIPT_LOG_FILE=${SCRIPT_LOG_FILE:-"${SCRIPT_DIR}/${SCRIPT_NAME}.log"} +SCRIPT_LOG_EMAIL_FILE=${SCRIPT_LOG_EMAIL_FILE:-"$HOME/${SCRIPT_NAME}.mail.log"} +JSON_FILE_NAME=${JSON_FILE_NAME:-"${SCRIPT_DIR}/${SCRIPT_NAME}_${NOWDATESTAMP}.json"} +SCRIPT_RUNNING_FILE=${SCRIPT_RUNNING_FILE:-"${HOME}/${GIT_REPO_NAME}_running.txt"} + +LATEST_PROJECT_SHA=${LATEST_PROJECT_SHA:-0} + +# # START - IMPORT FUNCTIONS +if [[ ! -n "$(type -t _registered)" ]]; then + if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" ]]; then + # shellcheck source=_functions.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" + fi +fi +# END - IMPORT FUNCTIONS + +_registered_logs() { + # This is used for checking is _function.sh has been imported or not + return 0 +} + +# START - LOG FUNCTIONS + +# Function: _log_error +# Description: Logs an error message and sends it to the _log_data function. +# Parameters: +# $1: The error message to log. +# Returns: None + +_log_error() { + _log_data "ERROR" "$1" +} + +# Function: _log_info +# Description: Logs an informational message and sends it to the _log_data function. +# Parameters: +# $1: The informational message to log. +# Returns: None + +_log_info() { + _log_data "INFO" "$1" +} + +# Function: _log_debug +# Description: Logs a debug message and sends it to the _log_data function. +# Parameters: +# $1: The debug message to log. +# Returns: None + +_log_debug() { + _log_data "DEBUG" "$1" +} + +# Function: _log_success +# Description: Logs a success message and sends it to the _log_data function. +# Parameters: +# $1: The success message to log. +# Returns: None + +_log_success() { + _log_data "SUCCESS" "$1" +} + +# Function: _log_data +# Description: Adds a datestamp to the log message and sends it to the logs file and console. +# Parameters: +# $1: The log level (e.g., ERROR, INFO, DEBUG, SUCCESS). +# $2: The log message. +# Returns: None + +_log_data() { + local message + + # Check for two params + if [[ $# -eq 2 ]]; then + # Add prefix to message + message="[$1] $2" + else + # No prefix + message="$1" + fi + + if [[ "$(_interactive_shell)" = "1" ]]; then + # Log to the console if debug mode is enabled + _log_console "[$(date "+%d/%m/%Y %H:%M:%S")]$message" + fi + + # Log to file + _log_to_file "[$NOWDATESTAMP]$message" +} + +# Function: _log_to_file +# Description: Sends the log message to the log file. +# Parameters: +# $1: The log message. +# Returns: None + +_log_to_file() { + # If not existing log file directory return + if [[ ! -d $(pwd "${SCRIPT_LOG_FILE}") ]]; then + return + fi + # If not existing log file create + if [[ ! -f "${SCRIPT_LOG_FILE}" ]]; then + echo "$1" >"${SCRIPT_LOG_FILE}" + # If existing log file add to it + else + echo "$1" >>"${SCRIPT_LOG_FILE}" + fi + + # To the email file for sending later + # If not existing log file directory return + if [[ ! -d $(pwd "${SCRIPT_LOG_EMAIL_FILE}") ]]; then + return + fi + # If not existing log file create + if [[ ! -f "${SCRIPT_LOG_EMAIL_FILE}" ]]; then + echo "$1" >"${SCRIPT_LOG_EMAIL_FILE}" + # If existing log file add to it + else + echo "$1" >>"${SCRIPT_LOG_EMAIL_FILE}" + fi +} + +# Function: _log_console +# Description: Prints the log message to the console. +# Parameters: +# $1: The log message. +# Returns: +# $1: The log message. + +_log_console() { + local _message="$1" + echo "$_message" +} + +# Function: _log_console +# Description: Prints the log message to the console. +# Parameters: +# $1: The log message. +# Returns: +# $1: The log message. + +_send_email() { + + cat <~/.msmtprc +defaults +tls on +tls_starttls off +tls_certcheck off + +account default +host $MAIL_HOST +port $MAIL_PORT +auth on +user $MAIL_USERNAME +password $MAIL_PASSWORD +from $MAIL_FROM_ADDRESS +logfile ~/.msmtp.log +EOF + chmod 600 ~/.msmtprc + + local _email_subject="${1:-"Untitled"}" + + if [[ "$MAIL_MAILER" = "smtp" ]]; then + + # echo -e "From: $MAIL_FROM_NAME <$MAIL_FROM_ADDRESS>\nTo: <$MAIL_TO_ADDRESS>\nSubject: ${GIT_REPO_NAME}: ${_email_subject}\n\n$(cat "$SCRIPT_LOG_EMAIL_FILE")" | msmtp -a default + # echo -e "From: $MAIL_FROM_NAME <$MAIL_FROM_ADDRESS>\nTo: <$MAIL_TO_ADDRESS>\nSubject: ${GIT_REPO_NAME}: ${_email_subject}\n\n$(cat "$SCRIPT_LOG_EMAIL_FILE")" | msmtp --host="$MAIL_HOST" --port="$MAIL_PORT" --auth=on --user="$MAIL_USERNAME" --passwordeval="$MAIL_PASSWORD" --from="$MAIL_FROM_ADDRESS" --tls=on --tls-starttls=on --tls-certcheck=off "$MAIL_TO_ADDRESS" + msmtp -a default -t <\n +To: <$MAIL_TO_ADDRESS>\n +Subject: ${GIT_REPO_NAME}: ${_email_subject} + +$(cat "$SCRIPT_LOG_EMAIL_FILE") +EOF + rm "${SCRIPT_LOG_EMAIL_FILE}" + fi +} + +# END - LOG FUNCTIONS diff --git a/.deploy-cli/_update.sh b/.deploy-cli/_update.sh new file mode 100755 index 0000000..ab8f2c0 --- /dev/null +++ b/.deploy-cli/_update.sh @@ -0,0 +1,537 @@ +#!/bin/bash + +# This script variables +SCRIPT="${SCRIPT:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")}" +SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}" +SCRIPT_DIR_NAME="${SCRIPT_DIR_NAME:-$(basename "$PWD")}" +SCRIPT_DEBUG=${SCRIPT_DEBUG:-false} + +# START - IMPORT FUNCTIONS +if [[ ! -n "$(type -t _registered)" ]]; then + source _functions.sh +fi +# END - IMPORT FUNCTIONS + +# Function: __overwrite_nginx_configurations +# Description: Private function to overwrite nginx sites, configs & snippets. +# Parameters: None +# Returns: None + +__overwrite_nginx_configurations() { + + # If nginx config already broken, just exit. + if ! nginx -t >/dev/null 2>&1; then + # echo "nginx test failed:" + return + fi + + local timestamp + local backup_dir + local nginx_dir + local nginx_conf_path + local available_sites_dir + local enabled_sites_dir + local snippets_dir + local sites_to_copy + local snippets_to_copy + local nginx_conf_to_copy + local backup_nginx_conf_file + local backup_available_file + local backup_enabled_files + local backup_snippets_files + + # Backup current available-sites folder + timestamp="$(date +"%Y%m%d_%H%M%S")" + backup_dir="/etc/nginx/backup/${timestamp}" + mkdir -p "${backup_dir}" + + nginx_dir="/etc/nginx" + nginx_conf_path="${nginx_dir}/nginx.conf" + + available_sites_dir="${nginx_dir}/sites-available" + enabled_sites_dir="${nginx_dir}/sites-enabled" + snippets_dir="${nginx_dir}/snippets" + + sites_to_copy="$HOME/${GIT_REPO_NAME}/etc/nginx/sites-available" + snippets_to_copy="$HOME/${GIT_REPO_NAME}/etc/nginx/snippets" + + nginx_conf_to_copy="$HOME/${GIT_REPO_NAME}/etc/nginx/nginx.conf" + + # Backup existing site files + backup_nginx_conf_file="${backup_dir}/nginx.conf" + backup_available_file="${backup_dir}/available-sites.tar.gz" + backup_enabled_files="${backup_dir}/enabled-sites.tar.gz" + backup_snippets_files="${backup_dir}/snippets.tar.gz" + + tar -czvf "${backup_available_file}" "${available_sites_dir}" >/dev/null 2>&1 + tar -czvf "${backup_enabled_files}" "${enabled_sites_dir}" >/dev/null 2>&1 + tar -czvf "${backup_snippets_files}" "${snippets_dir}" >/dev/null 2>&1 + + # Remove existing files in available-sites + rm -rf "${available_sites_dir:?}/" + rm -rf "${enabled_sites_dir:?}/" + rm -rf "${snippets_dir:?}/" + + mkdir -p "${available_sites_dir}" + mkdir -p "${enabled_sites_dir}" + mkdir -p "${snippets_dir}" + + # Copy nginx config + cp -r "${nginx_conf_path}" "${backup_nginx_conf_file}" + cp -r "${nginx_conf_to_copy}" "${nginx_conf_path}" + + # Copy new site files from /home/sites/ + cp -r "${sites_to_copy}"/* "${available_sites_dir}" + cp -r "${snippets_to_copy}"/* "${snippets_dir}" + + __symlink_available_sites + + sync + + # Check nginx configuration after updating files + + if nginx -t >/dev/null 2>&1; then + _log_info "Nginx test after configuration update successful" + systemctl restart nginx 2>&1 + + # Delete un-used certs + __delete_unused_certs + + # Delete left over backup archives. + rm "${backup_available_file}" + rm "${backup_enabled_files}" + rm "${backup_snippets_files}" + + # Delete backed up nginx config file. + rm "${backup_nginx_conf_file}" + + # Delete baackup folder if empty. + if [ -d "$backup_dir" ]; then + if [ -z "$(ls -A "$backup_dir")" ]; then + rm -rf "$backup_dir" + fi + fi + _send_email "$GIT_REPO_NAME on $(hostname) - configuration update successful" + + return + else + _log_error "Nginx new configuration failed, Rolling back..." + _send_email "$GIT_REPO_NAME on $(hostname) - Rolling back..." + + # Delete updated configs + rm -rf "${available_sites_dir:?}/" + rm -rf "${enabled_sites_dir:?}/" + rm -rf "${snippets_dir:?}/" + + # Copy back backed up nginx config. + cp -r "${backup_nginx_conf_file}" "${nginx_conf_path}" + + sync + + # Restore backup, extract to / as archive has /etc/nginx/... structure. + tar -xzvf "${backup_available_file}" -C / >/dev/null 2>&1 + tar -xzvf "${backup_enabled_files}" -C / >/dev/null 2>&1 + tar -xzvf "${backup_snippets_files}" -C / >/dev/null 2>&1 + + sync + + # Delete left over backup archives. + rm "${backup_available_file}" + rm "${backup_enabled_files}" + rm "${backup_snippets_files}" + + # Delete backed up nginx config file. + rm "${backup_nginx_conf_file}" + + # Delete baackup folder if empty. + if [ -d "$backup_dir" ]; then + if [ -z "$(ls -A "$backup_dir")" ]; then + rm -rf "$backup_dir" + fi + fi + + sync + + # echo 1 + return + fi +} + +# Function: __symlink_available_sites +# Description: Private function to create symlinks for every available nginx site config. +# Parameters: None +# Returns: None + +__symlink_available_sites() { + local available_sites_dir="/etc/nginx/sites-available" + local sites_enabled_dir="/etc/nginx/sites-enabled" + + # Ensure the directories exist + mkdir -p "$available_sites_dir" + mkdir -p "$sites_enabled_dir" + + # Loop through files in available-sites directory + for file in "$available_sites_dir"/*; do + if [ -f "$file" ]; then + filename=$(basename "$file") + symlink="$sites_enabled_dir/$filename" + # Check for default site first. + if [[ "${filename,,}" = "readme.md" ]]; then + echo "Skipping readme." >/dev/null 2>&1 + elif [[ "$filename" = "default" ]]; then + # Check if symlink already exists + if [[ ! -L "$symlink" ]]; then + # Create symlink + ln -s "../sites-available/$filename" "$symlink" + _log_info "Created symlink $symlink" + fi + # Check if Re get a cert for domain + elif [[ "$(__cert_domain "$filename")" = "0" ]]; then + # Check if symlink already exists + if [[ ! -L "$symlink" ]]; then + # Create symlink + ln -s "../sites-available/$filename" "$symlink" + _log_info "Created symlink $symlink" + fi + fi + fi + done +} +# Function: __cert_domain +# Description: Acquires a SSL for given domain after checking. +# Parameters: None +# $1 - domain name for cert. +# Returns: +# 0 - Success. +# 1 - Failed. + +__cert_domain() { + local site_domain="$1" + local letsencrypt_live_dir + local base_domain + local www_domain + local verification_dir + local verification_string + + # Publicly available on point domains on http. + verification_dir="/var/www/.hosted" + verification_string=$(tr -dc 'A-Za-z0-9' "${verification_dir}/${www_domain}.txt" + echo "${verification_string}" >"${verification_dir}/${base_domain}.txt" + + sync + + # Allow www-data user/group access to the directory. + chown www-data:www-data "${verification_dir}" -R + + # Try get those files. + hosting_www_domain=$(curl --silent "http://$www_domain/.hosted/$www_domain.txt") + hosting_base_domain=$(curl --silent "http://$base_domain/.hosted/$base_domain.txt") + cert_www_domain=0 + cert_base_domain=0 + if [[ "$hosting_www_domain" == *"$verification_string"* ]]; then + cert_www_domain=1 + fi + if [[ "$hosting_base_domain" == *"$verification_string"* ]]; then + cert_base_domain=1 + fi + + # Delete verification files. + rm "${verification_dir}/${www_domain}.txt" + rm "${verification_dir}/${base_domain}.txt" + + # If not lets encrypt cert required return success + if [[ "${ENABLED_LETSENCRYPT}" != "true" ]]; then + # Signal success no cert required.. + if [[ "$cert_www_domain" = "1" || "$cert_base_domain" = "1" ]]; then + # Signal success domain hosted + echo 0 + else + # Signal failure no domain hosted + echo 1 + fi + return + fi + + if [[ "$cert_www_domain" = "1" || "$cert_base_domain" = "1" ]]; then + _log_to_file "[$(date "+%Y-%m-%d_%H-%M-%S")] DNS is set for $site_domain" + + # Attempt to obtain certificate for both the main domain and the www subdomain. + if [[ "$cert_www_domain" = "1" && "$cert_base_domain" = "1" ]]; then + certbot certonly --webroot -w /var/www -d "$base_domain" -d "$www_domain" + elif [[ "$cert_www_domain" = "1" ]]; then + certbot certonly --webroot -w /var/www -d "$www_domain" + elif [[ "$cert_base_domain" = "1" ]]; then + certbot certonly --webroot -w /var/www -d "$base_domain" + fi + + if [ $? -eq 0 ]; then + _log_to_file "[$(date "+%Y-%m-%d_%H-%M-%S")] Certificate obtained successfully for $base_domain and $www_domain" + # Signal success. + sync + echo 0 + return + else + _log_to_file "[$(date "+%Y-%m-%d_%H-%M-%S")] Failed to obtain certificate for $site_domain" + # Signal error no cert. + sync + echo 1 + return + fi + fi + + # Signal error no domain hosted. + echo 1 + return +} + +# Function: __delete_unused_certs +# Description: Handles deleting un-used lets encrypt certs. +# Parameters: None +# Returns: None + +__delete_unused_certs() { + local NGINX_DIR="/etc/nginx/sites-available" + local CERT_DIR="/etc/letsencrypt/live" + + # Get the list of domains in Nginx sites-available + nginx_domains=$(ls $NGINX_DIR) + + # Loop through all certificate directories + for cert_path in "$CERT_DIR"/*; do + if [ -d "$cert_path" ]; then + + cert_domain=$(basename "$cert_path") + + # Extract domains from the certificate + cert_domains=$(openssl x509 -in "$cert_path/fullchain.pem" -text -noout | grep -E 'Subject:|X509v3 Subject Alternative Name' -A 1 | awk -F 'DNS:|CN=' '/CN=/{print $2} /^ *DNS:/{print $2}') + # Flag to check if any domain matches + match_found=false + + # Check if any of the certificate domains match the Nginx configuration + for domain in $cert_domains; do + if echo "$nginx_domains" | grep -qw "$domain"; then + match_found=true + break + fi + done + # echo "$cert_domain match $match_found" + # If no match is found, print the unused certificate + if [ "$match_found" = false ]; then + _log_info "Unused certificate found for domains in: $cert_path" + certbot delete --cert-name "$cert_domain" + fi + fi + done +} + +# Function: _update_limits_conf +# Description: Function to update limits.conf. +# Parameters: None +# Returns: None + +__update_limits_conf() { + local limit_type=$1 + local new_limit=$2 + + if grep -q "\* $limit_type nofile" /etc/security/limits.conf; then + sed -i "s/\* $limit_type nofile.*/\* $limit_type nofile $new_limit/" /etc/security/limits.conf + else + echo "* $limit_type nofile $new_limit" >>/etc/security/limits.conf + fi +} + +# Function: _update_nginx_conf +# Description: Function to update nginx.conf. +# Parameters: None +# Returns: None + +__update_nginx_conf() { + local new_limit=$1 + + if grep -q "worker_rlimit_nofile" /etc/nginx/nginx.conf; then + _log_info "Updating Nginx worker_rlimit_nofile to $new_limit" + sed -i "s/worker_rlimit_nofile.*/worker_rlimit_nofile $new_limit;/" /etc/nginx/nginx.conf + else + # Add worker_rlimit_nofile at the top of the nginx.conf file + _log_info "Adding Nginx worker_rlimit_nofile to $new_limit" + sed -i "1s/^/worker_rlimit_nofile $new_limit;\n/" /etc/nginx/nginx.conf + fi +} + +# Function: _ensure_file_limits +# Description: Ensure the right file open limits are set. +# Parameters: None +# Returns: None + +__ensure_file_limits() { + # Desired limit value + local DESIRED_LIMIT=65536 + local restart_nginx=false + + # Check current limits + local current_soft_limit + current_soft_limit=$(ulimit -n) + local current_hard_limit + current_hard_limit=$(ulimit -Hn) + + #echo "Current soft limit: $current_soft_limit" + #echo "Current hard limit: $current_hard_limit" + + # Update limits if needed + if [ "$current_soft_limit" -lt "$DESIRED_LIMIT" ]; then + _log_info "Updating soft limit to $DESIRED_LIMIT" + __update_limits_conf soft $DESIRED_LIMIT + restart_nginx=true + fi + + if [ "$current_hard_limit" -lt "$DESIRED_LIMIT" ]; then + _log_info "Updating hard limit to $DESIRED_LIMIT" + __update_limits_conf hard $DESIRED_LIMIT + restart_nginx=true + fi + + # Update nginx.conf + __update_nginx_conf $DESIRED_LIMIT + + #echo "Restarting Nginx to apply changes..." + if [ $restart_nginx = true ]; then + systemctl restart nginx >/dev/null 2>&1 + fi +} + +# +# Function: update_sysctl_conf +# Description: Function to update sysctl.conf if the setting is not already set or needs to be updated. +# Parameters: None +# Returns: None + +__update_sysctl_conf() { + local key=$1 + local value=$2 + local current_value + current_value="$(sysctl -n "$key" 2>/dev/null)" + + if [ "$current_value" != "$value" ]; then + if grep -q "^$key" /etc/sysctl.conf; then + sed -i "s/^$key.*/$key = $value/" /etc/sysctl.conf + else + echo "$key=$value" >>/etc/sysctl.conf + fi + sysctl -q -w "$key=$value" >/dev/null 2>&1 + fi +} + +# Function: _pre_update +# Description: Performs pre update checks. +# Parameters: None +# Returns: None + +_pre_update() { + local dhparam_pid=false + local openssl_pid=false + + # Create Diffie-Hellman key exchange file if missing + if [[ ! -f "/etc/nginx/dhparam.pem" ]]; then + _log_info "Creating dhparam" + # Generate Diffie-Hellman parameters + openssl dhparam -out "/etc/nginx/dhparam.pem" 2048 >/dev/null 2>&1 & + # Capture the PID of the openssl command + dhparam_pid=$! + fi + # Create snakeoil cert if missing + if [[ ! -f "/etc/ssl/private/ssl-cert-snakeoil.key" || ! -f "/etc/ssl/certs/ssl-cert-snakeoil.pem" ]]; then + _log_info "Creating snakeoil" + # Generate a self-signed SSL certificate + openssl req -x509 -nodes -newkey rsa:4096 \ + -keyout "/etc/ssl/private/ssl-cert-snakeoil.key" \ + -out "/etc/ssl/certs/ssl-cert-snakeoil.pem" -days 3650 \ + -subj "/C=${APP_ENCODE: -2}/ST=$(echo "$APP_TIMEZONE" | cut -d'/' -f2)/L=$(echo "$APP_TIMEZONE" | cut -d'/' -f2)/O=CompanyName/OU=IT Department/CN=example.com" >/dev/null 2>&1 & + # Capture the PID of the openssl command + openssl_pid=$! + fi + + if [[ "$openssl_pid" != "false" ]]; then + _wait_pid_expirer "$openssl_pid" + _log_info "Finished generating self-signed SSL certificate." + fi + + if [[ "$dhparam_pid" != "false" ]]; then + _wait_pid_expirer "$dhparam_pid" + _log_info "Finished generating Diffie-Hellman parameters." + fi +} + +# Function: _post_update +# Description: Performs some post flight checks.. +# Parameters: None +# Returns: None + +_post_update() { + + if [[ ! -f "$HOME/${GIT_REPO_NAME}/.env" ]]; then + cp "$HOME/${GIT_REPO_NAME}/.env.production" "$HOME/${GIT_REPO_NAME}/.env" + fi + + __overwrite_nginx_configurations + + # Increase file descriptors limit + __update_sysctl_conf "fs.file-max" "2097152" + + # Increase network backlog + __update_sysctl_conf "net.core.somaxconn" "65535" + __update_sysctl_conf "net.core.netdev_max_backlog" "65535" + + # Enable TCP fast open + __update_sysctl_conf "net.ipv4.tcp_fastopen" "3" + + # Optimize TCP settings + __update_sysctl_conf "net.ipv4.tcp_fin_timeout" "30" + __update_sysctl_conf "net.ipv4.tcp_tw_reuse" "1" + __update_sysctl_conf "net.ipv4.tcp_syncookies" "1" + __update_sysctl_conf "net.ipv4.tcp_max_syn_backlog" "65535" + __update_sysctl_conf "net.ipv4.tcp_max_tw_buckets" "1440000" + + # Apply the settings + sysctl -q -p + + __ensure_file_limits + + sync + + # Clean-up + + _delete_old_project_files +} diff --git a/.deploy-cli/_utils.sh b/.deploy-cli/_utils.sh new file mode 100644 index 0000000..f63f0aa --- /dev/null +++ b/.deploy-cli/_utils.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +# This script variables +SCRIPT_NAME="${SCRIPT_NAME:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")" | sed 's/\.[^.]*$//')}" +SCRIPT="${SCRIPT:-$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")}" +SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}" +SCRIPT_DIR_NAME="${SCRIPT_DIR_NAME:-$(basename "$PWD")}" +SCRIPT_DEBUG=${SCRIPT_DEBUG:-false} + +# Terminal starting directory +STARTING_LOCATION=${STARTING_LOCATION:-"$(pwd)"} + +# Deployment environment +DEPLOYMENT_ENV=${DEPLOYMENT_ENV:-"production"} + +# Enable location targeted deployment +DEPLOYMENT_ENV_LOCATION=${DEPLOYMENT_ENV_LOCATION:-false} + +# Deployment location +ISOLOCATION=${ISOLOCATION:-"GB"} +ISOSTATELOCATION=${ISOSTATELOCATION:-""} + +# Git repo name +GIT_REPO_NAME="${GIT_REPO_NAME:-$(basename "$(git rev-parse --show-toplevel)")}" + +# if using GitHub, Github Details if not ignore +GITHUB_REPO_OWNER="${GITHUB_REPO_OWNER:-$(git remote get-url origin | sed -n 's/.*github.com:\([^/]*\)\/.*/\1/p')}" +GITHUB_REPO_URL="${GITHUB_REPO_URL:-"https://api.github.com/repos/$GITHUB_REPO_OWNER/$GIT_REPO_NAME/commits"}" + +SCRIPT_LOG_FILE=${SCRIPT_LOG_FILE:-"${SCRIPT_DIR}/${SCRIPT_NAME}.log"} +SCRIPT_LOG_EMAIL_FILE=${SCRIPT_LOG_EMAIL_FILE:-"$HOME/${SCRIPT_NAME}.mail.log"} +JSON_FILE_NAME=${JSON_FILE_NAME:-"${SCRIPT_DIR}/${SCRIPT_NAME}_${NOWDATESTAMP}.json"} +SCRIPT_RUNNING_FILE=${SCRIPT_RUNNING_FILE:-"${HOME}/${GIT_REPO_NAME}_running.txt"} + +LATEST_PROJECT_SHA=${LATEST_PROJECT_SHA:-0} + +# START - IMPORT FUNCTIONS +if [[ ! -n "$(type -t _registered)" ]]; then + if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" ]]; then + # shellcheck source=_functions.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" + fi +fi +# END - IMPORT FUNCTIONS + +_registered_utils() { + # This is used for checking is _function.sh has been imported or not + return 0 +} + +# Function: _is_present +# Description: Checks if the given command is present in the system's PATH. +# Parameters: +# $1: The command to check. +# Returns: +# 1 if the command is present, otherwise void. + +_is_present() { command -v "$1" &>/dev/null && echo 1; } + +# Function: _is_file_open +# Description: Checks if the given file is open by any process. +# Parameters: +# $1: The file to check. +# Returns: +# 1 if the file is open, otherwise void. + +_is_file_open() { lsof "$1" &>/dev/null && echo 1; } + +# Function: _is_in_screen +# Description: Function to run the DIY.com website scraper.. +# Parameters: None +# Returns: None + +_is_in_screen() { + if [[ ! -z "$STY" ]]; then + echo "1" + else + echo "0" + fi +} + +# Function: _interactive_shell +# Description: Checks if the script is being run from a headless terminal or cron job. +# Returns 1 if running from a cron job or non-interactive environment, 0 otherwise. +# Parameters: None +# Returns: +# 1 if running from a cron job or non-interactive environment +# 0 otherwise. + +_interactive_shell() { + # Check if the script is being run from a headless terminal or cron job + if [ -z "$TERM" ] || [ "$TERM" = "dumb" ]; then + if [ -t 0 ] && [ -t 1 ]; then + # Script is being run from an interactive shell or headless terminal + echo 1 + else + # Script is likely being run from a cron job or non-interactive environment + echo 0 + fi + else + # Script is being run from an interactive shell + echo 1 + fi +} + +# Function: _wait_pid_expirer +# Description: Waits for a process with the given PID to expire. +# Parameters: +# $1: The PID of the process to wait for. +# Returns: None + +_wait_pid_expirer() { + # If sig is 0, then no signal is sent, but error checking is still performed. + while kill -0 "$1" 2>/dev/null; do + sleep 1s + done +} + +# Function: _install_cronjob +# Description: Installs a cron job from the crontab. +# Parameters: +# $1: The cron schedule for the job. "* * * * * " +# $2: The command of the job. "/bin/bash command-to-be-executed" +# Returns: None + +_install_cronjob() { + + if [[ $# -lt 2 ]]; then + _log_info "Missing arguments <$(echo "$1" || echo "schedule")> <$([ ${#2} -ge 1 ] && echo "$2" || echo "command")>" + _exit_script + fi + + # Define the cron job entry + local cron_schedule=$1 + local cron_job=$2 + local cron_file="/tmp/.temp_cron" + + _log_info "Installing Cron job: ${cron_job}" + + # Load the existing crontab into a temporary file + crontab -l >"$cron_file" + + # Check if the cron job already exists + if ! grep -q "$cron_job" "$cron_file"; then + # Append the new cron job entry to the temporary file + echo "$cron_schedule $cron_job" >>"$cron_file" + + # Install the updated crontab from the temporary file + crontab "$cron_file" + + if [[ $? -eq 0 ]]; then + _log_info "Cron job installed successfully." + else + _log_error "Cron job installation failed: $cron_schedule $cron_job" + fi + else + _log_info "Cron job already exists." + fi + + # Remove the temporary file + rm "$cron_file" +} + +# Function: _remove_cronjob +# Description: Uninstalls a cron job from the crontab. +# Parameters: +# $1: The cron schedule for the job. "* * * * * " +# $2: The command of the job. "/bin/bash command-to-be-executed" +# Returns: None + +_remove_cronjob() { + if [[ $# -lt 2 ]]; then + _log_info "Missing arguments <$(echo "$1" || echo "schedule")> <$([ ${#2} -ge 1 ] && echo "$2" || echo "command")>" + _exit_script + fi + + # Define the cron job entry + local cron_schedule=$1 + local cron_job=$2 + local cron_file="/tmp/.temp_cron" + + _log_info "Removing cronjob: ${cron_job}" + + # Load the existing crontab into a temporary file + crontab -l >_temp_cron + + # Check if the cron job exists in the crontab + if grep -q "$cron_job" "$cron_file"; then + # Remove the cron job entry from the temporary file + sed -i "/$cron_schedule $cron_job/d" "$cron_file" + + # Install the updated crontab from the temporary file + crontab "$cron_file" + + if [[ $? -eq 0 ]]; then + _log_info "Cron job removed successfully." + else + _log_error "Failed to install cronjob: $cron_schedule $cron_job" + fi + else + _log_info "Cron job not found." + fi + + # Remove the temporary file + rm "$cron_file" +} + +# START - GEOLCATION FUNCTIONS + +# Function: _valid_ip +# Description: Checks if the given IP address is valid. +# Parameters: +# $1: The IP address to validate. +# Returns: +# 0 if the IP address is valid, 1 otherwise. + +_valid_ip() { + local ip="$1" + [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ && $( + IFS='.' + set -- "$ip" + (($1 <= 255 && $2 <= 255 && $3 <= 255 && $4 <= 255)) + ) ]] +} + +# Function: _set_location_var +# Description: Retrieves the public IP address and sets the ISOLOCATION variable based on the country. +# Parameters: None +# Returns: None + +_set_location_var() { + local public_ip + public_ip=$(_get_public_ip) + + if _valid_ip "${public_ip}"; then + # Whois public ip and grep first country code + ISOLOCATION="$(whois "$public_ip" -a | grep -iE ^country: | head -n 1)" + ISOLOCATION="${ISOLOCATION:(-2)}" + fi +} diff --git a/.github/workflows/ssh-deployment-update.md b/.github/workflows/ssh-deployment-update.md new file mode 100644 index 0000000..258965a --- /dev/null +++ b/.github/workflows/ssh-deployment-update.md @@ -0,0 +1,37 @@ +# ssh-deployment + +This action will run the update operation of the deploy-cli.sh script with a commit sha on live via ssh. + + +## setup + +Please ensure you have set up the following repository secrets: + +`Github.com > repo > settings > secrets and variables > actions > Repository secrets` + + +| Secret | Description | +| -------- | -------- | +| DEPLOYMENT_SSH_HOST | The hostname or IP address of the server for deployment. | +| DEPLOYMENT_SSH_HOST_KNOWN_ENTRY | The known host entry for the SSH host server. | +| DEPLOYMENT_SSH_USERNAME | The SSH username for connecting to the deployment server. | +| DEPLOYMENT_SSH_PRIVATE_KEY | The private SSH key used for connecting to the deployment server. | +| DEPLOYMENT_PATH | The path on the server to the live deployment. | +| DEPLOYMENT_GITHUB_TOKEN | A Github api token with access to this repos for commit logs. | + +### Have you setup repo deployment key for remote server cloning? + +You need to login to the server to be deployed to and run the following commands + +```bash +$ ssh-keygen -t ed25519 -N "" -C "server@domain.com" -f ~/.ssh/id_ed25519 +$ cat ~/.ssh/id_ed25519.pub +``` + +Take the output from the cat command and put that in as a deployment key. + +Then accept the github.com ssh key + +```bash +$ ssh git@github.com -T +``` \ No newline at end of file diff --git a/.github/workflows/ssh-deployment-update.yml b/.github/workflows/ssh-deployment-update.yml new file mode 100644 index 0000000..1ed1321 --- /dev/null +++ b/.github/workflows/ssh-deployment-update.yml @@ -0,0 +1,99 @@ +on: + push: + branches: + - main + workflow_dispatch: + inputs: + sshHost: + description: 'Setup DEPLOYMENT_SSH_HOST?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + localSshKnownEntry: + description: 'Setup DEPLOYMENT_SSH_HOST_KNOWN_ENTRY?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + sshUsername: + description: 'Setup DEPLOYMENT_SSH_USERNAME?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + sshPrivateKey: + description: 'Setup DEPLOYMENT_SSH_PRIVATE_KEY?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + sshAppPath: + description: 'Setup DEPLOYMENT_PATH?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + githubToken: + description: 'Setup DEPLOYMENT_GITHUB_TOKEN?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' +jobs: + ssh-deployment: + runs-on: ubuntu-latest + steps: + - name: Check if all required secrets are set and not empty + run: | + secrets=(DEPLOYMENT_SSH_HOST DEPLOYMENT_SSH_HOST_KNOWN_ENTRY DEPLOYMENT_SSH_USERNAME DEPLOYMENT_SSH_PRIVATE_KEY DEPLOYMENT_PATH DEPLOYMENT_GITHUB_TOKEN) + for secret in "${secrets[@]}"; do + if [ -z "${{ secrets[$secret] }}" ]; then + echo "$secret is not set or is empty. Exiting." + exit 1 + else + echo "$secret is set and not empty." + fi + done + - name: Check user inputs + run: | + if [[ "${{ inputs.sshHost }}" == "yes" ]] && \ + [[ "${{ inputs.localSshKnownEntry }}" == "yes" ]] && \ + [[ "${{ inputs.sshUsername }}" == "yes" ]] && \ + [[ "${{ inputs.sshPrivateKey }}" == "yes" ]] && \ + [[ "${{ inputs.sshAppPath }}" == "yes" ]] && \ + [[ "${{ inputs.githubToken }}" == "yes" ]]; then + echo "All required inputs are set to 'yes'. Proceeding with SSH connection." + fi + + - name: Set up SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOYMENT_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -p 22 ${{ secrets.DEPLOYMENT_SSH_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy to server + run: | + export DEBIAN_FRONTEND=noninteractive + ssh ${{ secrets.DEPLOYMENT_SSH_USERNAME }}@${{ secrets.DEPLOYMENT_SSH_HOST }} <<'ENDSSH' + # This one-liner combines the file existence check, sourcing the file, checking if the variable is set, and writing to the file if the variable is not set. + [[ -f "$HOME/.github_token" ]] && source "$HOME/.github_token"; [[ -z "${GITHUB_TOKEN+x}" ]] && echo "GITHUB_TOKEN=${{ secrets.DEPLOYMENT_GITHUB_TOKEN }}" > "$HOME/.github_token" + # Run deployment script. + bash ~/$(hostname)/deploy-cli.sh update ${{ github.sha }} + export DEBIAN_FRONTEND=dialog + ENDSSH + rm ~/.ssh/id_rsa; + rm ~/.ssh/known_hosts; diff --git a/.github/workflows/ssh-deployment.md b/.github/workflows/ssh-deployment.md new file mode 100644 index 0000000..e64ff07 --- /dev/null +++ b/.github/workflows/ssh-deployment.md @@ -0,0 +1,43 @@ +# ssh-deployment + +This action will install this repo on to a server via ssh. + + +## setup + +### Server to deploy to + +- Ssh key in github deployment key +- apt install git curl + + +Please ensure you have set up the following repository secrets: + +`Github.com > repo > settings > secrets and variables > actions > Repository secrets` + + +| Secret | Description | +| -------- | -------- | +| DEPLOYMENT_SSH_HOST | The hostname or IP address of the server for deployment. | +| DEPLOYMENT_SSH_HOST_KNOWN_ENTRY | The known host entry for the SSH host server. | +| DEPLOYMENT_SSH_USERNAME | The SSH username for connecting to the deployment server. | +| DEPLOYMENT_SSH_PRIVATE_KEY | The private SSH key used for connecting to the deployment server. | +| DEPLOYMENT_PATH | The path on the server to the live deployment. | +| DEPLOYMENT_GITHUB_TOKEN | A Github api token with access to this repos for commit logs. | + +### Have you setup repo deployment key for remote server cloning? + +You need to login to the server to be deployed to and run the following commands + +```bash +$ ssh-keygen -t ed25519 -N "" -C "server@domain.com" -f ~/.ssh/id_ed25519 +$ cat ~/.ssh/id_ed25519.pub +``` + +Take the output from the cat command and put that in as a deployment key. + +Then accept the github.com ssh key + +```bash +$ ssh git@github.com -T +``` diff --git a/.github/workflows/ssh-deployment.yml b/.github/workflows/ssh-deployment.yml new file mode 100644 index 0000000..ada1730 --- /dev/null +++ b/.github/workflows/ssh-deployment.yml @@ -0,0 +1,107 @@ +on: + workflow_dispatch: + inputs: + sshHost: + description: 'Setup DEPLOYMENT_SSH_HOST?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + localSshKnownEntry: + description: 'Setup DEPLOYMENT_SSH_HOST_KNOWN_ENTRY?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + sshUsername: + description: 'Setup DEPLOYMENT_SSH_USERNAME?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + sshPrivateKey: + description: 'Setup DEPLOYMENT_SSH_PRIVATE_KEY?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + sshAppPath: + description: 'Setup DEPLOYMENT_PATH?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + githubToken: + description: 'Setup DEPLOYMENT_GITHUB_TOKEN?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' + repoDeploymentKey: + description: 'Have you setup repo deployment key for remote server cloning?' + required: true + default: 'no' + type: choice + options: + - 'yes' + - 'no' +jobs: + ssh-deployment: + runs-on: ubuntu-latest + steps: + - name: Check if all required secrets are set and not empty + run: | + secrets=(DEPLOYMENT_SSH_HOST DEPLOYMENT_SSH_HOST_KNOWN_ENTRY DEPLOYMENT_SSH_USERNAME DEPLOYMENT_SSH_PRIVATE_KEY DEPLOYMENT_PATH DEPLOYMENT_GITHUB_TOKEN) + for secret in "${secrets[@]}"; do + if [ -z "${{ secrets[$secret] }}" ]; then + echo "$secret is not set or is empty. Exiting." + exit 1 + else + echo "$secret is set and not empty." + fi + done + - name: Check user inputs + run: | + if [[ "${{ inputs.sshHost }}" == "yes" ]] && \ + [[ "${{ inputs.localSshKnownEntry }}" == "yes" ]] && \ + [[ "${{ inputs.sshUsername }}" == "yes" ]] && \ + [[ "${{ inputs.sshPrivateKey }}" == "yes" ]] && \ + [[ "${{ inputs.sshAppPath }}" == "yes" ]] && \ + [[ "${{ inputs.repoDeploymentKey }}" == "yes" ]] && \ + [[ "${{ inputs.githubToken }}" == "yes" ]]; then + echo "All required inputs are set to 'yes'. Proceeding with SSH connection." + fi + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOYMENT_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -p 22 ${{ secrets.DEPLOYMENT_SSH_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy to server + run: | + export DEBIAN_FRONTEND=noninteractive + ssh -v -o StrictHostKeyChecking=no ${{ secrets.DEPLOYMENT_SSH_USERNAME }}@${{ secrets.DEPLOYMENT_SSH_HOST }} <<'ENDSSH' + # This one-liner combines the file existence check, sourcing the file, checking if the variable is set, and writing to the file if the variable is not set. + [[ -f "$HOME/.github_token" ]] && source "$HOME/.github_token"; [[ -z "${GITHUB_TOKEN+x}" ]] && echo "GITHUB_TOKEN=${{ secrets.DEPLOYMENT_GITHUB_TOKEN }}" > "$HOME/.github_token" + # Run deployment script. + [[ ! -d ~/$(hostname)/ ]] && git clone ${{ github.repositoryUrl }}; + bash ~/$(hostname)/deploy-cli.sh setup + export DEBIAN_FRONTEND=dialog + ENDSSH diff --git a/deploy-cli.sh b/deploy-cli.sh new file mode 100755 index 0000000..7414c5f --- /dev/null +++ b/deploy-cli.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +####################################################################### +# Server Deployment Script +####################################################################### +# +# This script is designed to management the deployment on a linux server. +# handling various configurations and checks based on environment +# variables and dependencies. +# +# Usage: +# ./deploy-cli.sh +# +# Authors: +# - admin@xoren.io +# +# Links: +# - https://github.com/xorenio/deploy-cli.sh +# +####################################################################### + +# START - Script setup and configs + +# Defaulting variables +NOWDATESTAMP=$(date "+%Y-%m-%d_%H-%M-%S") + +# This script variables +SCRIPT_NAME=$(basename "$(test -L "$0" && readlink "$0" || echo "$0")" | sed 's/\.[^.]*$//') +SCRIPT=$(basename "$(test -L "$0" && readlink "$0" || echo "$0")") +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT_DIR_NAME="$(basename "$PWD")" +SCRIPT_DEBUG=false # true AKA echo to console | false echo to log file +SCRIPT_CMD_ARG=("${@:1}") # Assign command line arguments to array +FUNCTION_ARG=("${@:2}") # Assign command line arguments to array + +# Terminal starting directory +STARTING_LOCATION="$(pwd)" +cd "$SCRIPT_DIR" || exit + +# Deployment environment +DEPLOYMENT_ENV="production" + +# Enable location targeted deployment +DEPLOYMENT_ENV_LOCATION=false + +# Deployment location +ISOLOCATION="GB" ## DEFAULT US +ISOSTATELOCATION="" ## DEFAULT EMPTY + +# Git repo name +GIT_REPO_NAME=$(basename "$(git rev-parse --show-toplevel)") + +# if using GitHub, Github Details if not ignore +GITHUB_REPO_OWNER=$(git remote get-url origin | sed -n 's/.*github.com:\([^/]*\)\/.*/\1/p') +GITHUB_REPO_URL="https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GIT_REPO_NAME}/commits" + +SCRIPT_LOG_FILE="${SCRIPT_DIR}/${SCRIPT_NAME}.log" +SCRIPT_LOG_EMAIL_FILE=${SCRIPT_LOG_EMAIL_FILE:-"$HOME/${SCRIPT_NAME}.mail.log"} +JSON_FILE_NAME="${SCRIPT_DIR}/${SCRIPT_NAME}_${NOWDATESTAMP}.json" +SCRIPT_RUNNING_FILE="$HOME/${GIT_REPO_NAME}_running.txt" + +# Working Schedule +# This is referenced in the update check function and will exclude updating in given time frames, or false to disable +# Define a single string with time ranges, where ranges can be specified like 1-5:07:00-16:00 +# Format: 1: Monday - 7: Sunday - day(s):start_time-end_time|... +# e.g., "1-5:07:00-16:00|6:09:00-15:00|7:09:00-15:00" +# WORKING_SCHEDULE="1-5:07:00-16:00|7:20:00-23:59" +WORKING_SCHEDULE=false + +# END - Script setup and configs + +# START - IMPORT FUNCTIONS +if [[ -f "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" ]]; then + # shellcheck source=.deploy-cli/_functions.sh + source "${SCRIPT_DIR}/.${SCRIPT_NAME}/_functions.sh" +else + echo "[ERROR] Missing _functions.sh" + exit +fi + +# END - IMPORT FUNCTIONS + +# START - SCRIPT PRE-CONFIGURE + +## SET LOGGING TO TTY OR TO deployment.log +if [[ "$(_interactive_shell)" = "1" ]]; then + if [ "$APT_IS_PRESENT" ]; then + export DEBIAN_FRONTEND=noninteractive + fi + SCRIPT_DEBUG=false +else + SCRIPT_DEBUG=true +fi + +if [[ "$DEBIAN_FRONTEND" != "noninteractive" ]]; then + if [[ "$(_is_present curl)" != "1" ]]; then + _log_error "Please install curl." + exit + fi + + if [[ "$(_is_present jq)" != "1" ]]; then + _log_error "Please install jq." + exit + fi +fi + +# if [[ "$DEPLOYMENT_ENV_LOCATION" = "true" && "$(_is_present whois)" != "1" ]]; then +# _log_error "Please install whois." +# exit +# fi + +## CHECK IF SCRIPT IS ALREADY RUNNING +_check_running_file + +## CHECK IF BACKGROUND TASKS ARE STILL RUNNING +# if [[ $SCREEN_IS_PRESENT == true ]]; then + +# _log_info "Script screen check." +# if screen -list | grep -q "${SCRIPT_DIR_NAME}_deployment"; then +# _log_error "${SCRIPT_DIR_NAME}_deployment screen still running." +# exit; +# fi +# fi + +## ECHO STARTTIME TO DEPLOYMENT LOG FILE +_create_running_file + +## CHECK FOR PROJECT VAR FILE +if [[ "$DEBIAN_FRONTEND" != "noninteractive" ]]; then + _check_project_secrets +fi +## SET DEPLOY ENV VAR TO LOCATION +if [[ "$DEPLOYMENT_ENV_LOCATION" = "true" ]]; then + _set_location_var + DEPLOYMENT_ENV="$ISOLOCATION" +fi + +## CHECK .env FILE +if [[ ! -f "$HOME"/"${GIT_REPO_NAME}"/.env ]]; then + + cp "$HOME"/"${GIT_REPO_NAME}"/.env."${DEPLOYMENT_ENV}" "$HOME"/"${GIT_REPO_NAME}"/.env +fi + +## LOAD .env VARS +# shellcheck disable=SC1090 +source "$HOME"/"${GIT_REPO_NAME}"/.env + +## SECRETS +_load_project_secrets + +# END - SCRIPT PRE-CONFIGURE + +__display_info() { + cat < ~/deployment.log - return - fi - echo $1 >> ~/deployment.log -} - -logConsole() { - echo $1 -} - - -## DEFINE HELPER FUNCTIONS -function isPresent { command -v "$1" &> /dev/null && echo 1; } -function isFileOpen { lsof "$1" &> /dev/null && echo 1; } -function isCron { [ -z "$TERM" ] || [ "$TERM" = "dumb" ] && echo 1; } -# [ -z "$TERM" ] || [ "$TERM" = "dumb" ] && echo 'Crontab' || echo 'Interactive' - - -## DEFINE HELPER VARS -SCREEN_IS_PRESENT="$(isPresent screen)" -JQ_IS_PRESENT="$(isPresent jq)" - - - -## LOCATION FUNCTIONS -## ARGE #1 $IP ADDRESS -## EXMAPLE: $ echo valid_ip 192.168.1.1 -function valid_ip() { - - local ip=$1 - local stat=1 - if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - OIFS=$IFS - IFS='.' - ip=($ip) - IFS=$OIFS - [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ - && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] - stat=$? - fi - return $stat -} - -function set_location_var() { - - ## GET PUBLIC IP - ip=$(curl -s -X GET https://checkip.amazonaws.com) - - ## VAILDATE AND COPY ENV FILE - if valid_ip ${ip}; then - - ISOLOCATION=$(whois $ip | grep -iE ^country:) - ISOLOCATION=$( echo $ISOLOCATION | head -n 1 ) - ISOLOCATION=${ISOLOCATION:(-2)} - - DEPLOYMENT_ENV=$ISOLOCATION - fi -} - - - -## ENV FILE FUNCTIONS -function writeEnvVars() { - - cat > ~/.${GITHUB_REPO_NAME}_vars < 3 ]]; then - - ## CHECK FOR = IN CONFIG LINE SEPERATE IF STATMENT FORMATTED DIFFERENTLY TO WORK - if echo $CONFIGLINE | grep -F = &>/dev/null; - then - CONFIGNAME=$(echo "$CONFIGLINE" | cut -d '=' -f 1) - CONFIGVALUE=$(echo "$CONFIGLINE" | cut -d '=' -f 2-) - # echo "CONFIGNAME: $CONFIGNAME" - # echo "CONFIGVALUE: $CONFIGVALUE" - # cat .env.production | grep "<$CONFIGNAME>" - - if cat .env.$DEPLOYMENT_ENV | grep "<$CONFIGNAME>" &>/dev/null; then - sed -i 's|<'$CONFIGNAME'>|'$CONFIGVALUE'|' .env.$DEPLOYMENT_ENV - fi - - fi - fi - done < ~/.${GITHUB_REPO_NAME}_vars - - ## REPLACED DEPLOYMENT VARS - sed -i 's||'$NEW_VERSION'|' .env.$DEPLOYMENT_ENV - sed -i 's||'$NOWDATESTAMP'|' .env.$DEPLOYMENT_ENV - - logInfo "END: Replacing APP environment variables" -} - - -## PROJECT UPDATE CHECKS FUNCTIONS -function getProjectRemoteVersion() { - - logInfo "Getting remote version" - - if [[ $GITHUB_REPO_PACKAGE_CHECK == true ]]; then - - getProjectVersionViaRepoPackage - else - - getProjectVersionViaRepo - fi - - logInfo "Local version: $APP_VERSION" - logInfo "Github version: $NEW_VERSION" -} - -function getProjectVersionViaRepo() { - - ## SEND REQUEST TO GITHUB FOR REPOSOTORY REPO DATA - logInfo "Sending request to github API for repo data" - - DATA=$( curl -s -H "Accept: application/vnd.github+json" \ - -H "Authorization: token $GITHUB_TOKEN" \ - $GITHUB_REPO_URL) - - logInfo "$(echo $DATA | jq -r .)" - - NEW_VERSION=$(echo $DATA | jq .[0].commit.tree.sha) -} - -function getProjectVersionViaRepoPackage() { - - ## SEND REQUEST TO GITHUB FOR REPO PACKAGE DATA - logInfo "Sending request to github API for package data" - - DATA=$( curl -s -H "Accept: application/vnd.github+json" \ - -H "Authorization: token $GIHHUB_TOKEN" \ - $GITHUB_REPO_PACKAGE_URL) - - logInfo "$(echo $DATA | jq -r .)" - - ## Using {'version_count': 53} as the update indicator - NEW_VERSION=$(echo $DATA | jq -r .version_count) -} - - -## APP FOLDER(S)/FILE(S) -function moveGameSaves() { - - logInfo "Moving vendor folder." - - ## IF SCREEN PROGRAM IS INSTALL - if [[ $SCREEN_IS_PRESENT ]]; then - - ## CHECK IF BACKGROUND TASKS ARE STILL RUNNING - if ! screen -list | grep -q "${GITHUB_REPO_NAME}_deployment_move_saves"; then - - logInfo "Running game saves files moving task in background." - - ## Create screen - screen -dmS "${GITHUB_REPO_NAME}_deployment_move_saves" - - ## Pipe command to screen - screen -S "${GITHUB_REPO_NAME}_deployment_move_saves" -p 0 -X stuff 'cp ~/'${GITHUB_REPO_NAME}'_'${NOWDATESTAMP}'/data/{backups,db,Logs,messaging,Saves,Statistic}/ ~/'${GITHUB_REPO_NAME}'/data/ --update --recursive && exit'$(echo -ne '\015') - ## Pipe in exit cmd separately to force terminate screen - # screen -S "${GITHUB_REPO_NAME}_deployment_move_saves" -p 0 -X stuff 'exit '$(echo -ne '\015') - - else # IF SCREEN FOUND - - logError "Task of moving game saves folder in background already running." - - # echo "${BoldText}DEBUG:${NormalText} The $ composer install command to backend is already running" - fi - else ## IF NO SCREEN PROGRAM - - ## Moving files in this process - logInfo "" - logInfo "Running game saves files moving task in foreground." - mv -u -f ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/valley_saves/ ~/${GITHUB_REPO_NAME}/ - logInfo "Finished moving vendor files." - logInfo "" - fi -} - - -## DOING UPDATE FUNCTION -function doUpdate() { - - logInfo "" - logInfo "Re-deployment Started" - logInfo "=====================" - logInfo "" - - ## ENTER PROJECT REPO DIRECTORY - cd ~/$GITHUB_REPO_NAME/ - - ## STOP DOCKER APP - logInfo "Stopping docker containers" - - if [[ -f ~/${GITHUB_REPO_NAME}/docker-compose.$DEPLOYMENT_ENV.yml ]]; then - docker-compose -f docker-compose.$DEPLOYMENT_ENV.yml down - else - docker-compose down - fi - - ## DELETE PROJECT DOCKER IMAGES - logInfo "Removing old docker images" - if [[ -f ~/${GITHUB_REPO_NAME}/docker-compose.$DEPLOYMENT_ENV.yml ]]; then - yes | docker-compose -f docker-compose.$DEPLOYMENT_ENV.yml rm #--all # dep - else - yes | docker-compose rm #--all # dep - fi - - yes | docker image rm danixu86/project-zomboid-dedicated-server:latest - - - ## LEAVE DIRECTORY - logInfo "Moved old project folder to ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}" - cd ~/ - ## MOVE AKA RENAME DIRECTORY - mv ~/$GITHUB_REPO_NAME/ ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP} - - ## GIT CLONE FROM GITHUB - logInfo "Cloned fresh copy from github $GITHUB_REPO_OWNER/${GITHUB_REPO_NAME}" - git clone git@github.com:$GITHUB_REPO_OWNER/$GITHUB_REPO_NAME.git - - ## FIX FOR INODE CHANGES - logInfo "Inode sync" - sync - sleep 2s - - ## ENTER NEWLY CLONED LOCAL PROJECT REPO - cd ~/${GITHUB_REPO_NAME} - - logInfo "Moving project secrets in to env file" - replaceEnvVars - - logInfo "Moving game saves" - moveGameSaves - # cp ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/data/{backups,db,Logs,messaging,Saves,Statistic,scripts}/ ~/${GITHUB_REPO_NAME}/data/ --update --recursive - #cp ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/data/db/ ~/${GITHUB_REPO_NAME}/data/ --update --recursive - #cp ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/data/Logs/ ~/${GITHUB_REPO_NAME}/data/ --update --recursive - #cp ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/data/messaging/ ~/${GITHUB_REPO_NAME}/data/ --update --recursive - #cp ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/data/Saves/ ~/${GITHUB_REPO_NAME}/data/ --update --recursive - #cp ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/data/Statistic/ ~/${GITHUB_REPO_NAME}/data/ --update --recursive - #cp ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/mods/ ~/${GITHUB_REPO_NAME}/mods/ --update --recursive - #cp ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/scripts/ ~/${GITHUB_REPO_NAME}/ --update --recursive - #cp ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP}/workshop-mods/ ~/${GITHUB_REPO_NAME}/ --update --recursive - - if [[ $SCREEN_IS_PRESENT ]]; then - while screen -list | grep -q "${GITHUB_REPO_NAME}_deployment_move_saves"; do - sleep 3 - done - fi - if [[ -f ~/${GITHUB_REPO_NAME}/docker-compose.$DEPLOYMENT_ENV.yml ]]; then - docker-compose -f docker-compose.$DEPLOYMENT_ENV.yml build - docker-compose -f docker-compose.$DEPLOYMENT_ENV.yml up -d - else - docker-compose build - docker-compose up -d - fi -} - - -## AFTER RUN CLEANUP -function deleteOldProjectFiles() { - - OLD_PROJECT_BYTE_SIZE=$(du ~/${GITHUB_REPO_NAME}_${NOWDATESTAMP} -sc | grep total) - - SPACESTR=" " - EMPTYSTR="" - TOTALSTR="total" - - SIZE=${SIZE/$SPACESTR/$EMPTYSTR} - - SIZE=${SIZE/$TOTALSTR/$EMPTYSTR} - - if [[ $SIZE -le 1175400 ]]; then - rm -R - - fi -} - -## DELETE RUNNING FILE -function deleteRunningFile() { - ## DELETE THE RUNNING FILE - if [[ -f ~/deployment_running.txt ]]; then - rm ~/deployment_running.txt - fi -} -# -# -# -# -## END - SCRIPT FUNCTIONS - - - - - - - - - -## START - SCRIPT PRE-CONFIGURE -# -# -# -# - -## SET LOGGING TO TTY OR TO deployment.log -if [[ isCron ]]; then - SCRIPT_DEBUG=false -else - SCRIPT_DEBUG=true -fi - - -## CHECK IF SCRIPT IS ALREADY RUNNING -if [[ -f ~/deployment_running.txt ]]; then - logInfo "Script already running." - exit -fi - - -## CHECK IF BACKGROUND TASKS ARE STILL RUNNING -if [[ $SCREEN_IS_PRESENT ]]; then - if screen -list | grep -q "${GITHUB_REPO_NAME}_deployment_move_saves"; then - logError "${GITHUB_REPO_NAME}_deployment_move_saves screen still running." - exit; - fi -fi - - -## SAYING SOMETHING -logInfo "" -logInfo "Starting deployment update check." - - -## ECHO STARTTIME TO DEPLOYMENT LOG FILE -echo ${NOWDATESTAMP} > ~/deployment_running.txt - - -## ENTER PROJECT DIRECTORY -cd ~/$GITHUB_REPO_NAME/ - - -## CHECK FOR GITHUB TOKEN -if [[ ! -f ~/.github_token ]]; then - - logError "" - logError "Failed deployment ${NOWDATESTAMP}" - logError "" - logError "Missing github token file .github_token" - logError "GIHHUB_TOKEN=ghp_####################################" - logError "public_repo, read:packages, repo:status, repo_deployment" - exit 1; -fi - - -## CHECK FOR PROJECT VAR FILE -if [[ ! -f ~/.${GITHUB_REPO_NAME}_vars ]]; then - - logError "" - logError "Failed deployment ${NOWDATESTAMP}" - logError "" - logError "Missing twisted var file ~/.${GITHUB_REPO_NAME}_vars" - - exit 1; -fi - -## SET DEPLOY ENV VAR TO LOCATION -if [[ $DEPLOYMENT_ENV_LOCATION ]]; then - set_location_var - DEPLOYMENT_ENV=$ISOLOCATION -fi - -## CHECK .env FILE -if [[ ! -f ~/$GITHUB_REPO_NAME/.env ]]; then - - cp ~/$GITHUB_REPO_NAME/.env.$DEPLOYMENT_ENV ~/$GITHUB_REPO_NAME/.env -fi - -## LOAD .env VARS and GITHUB TOKEN AND SECRETS -logInfo "Loading .env & github var" -source ~/$GITHUB_REPO_NAME/.env -source ~/.github_token -## SECRETS -source ~/.${GITHUB_REPO_NAME}_vars - -# -# -# -# -### END - SCRIPT PRE-CONFIGURE - - - - - - - - - - -### START - SCRIPT RUNTIME -# -# -# -# - -if [[ $# -eq 1 ]]; then - logInfo "" - logInfo "=================================" - logInfo "\/ Manually re-install started \/" - logInfo "=================================" - doUpdate - deleteRunningFile - exit -fi - -getProjectRemoteVersion - -## CHECK FOR DEFAULT VARS -if [[ $APP_VERSION == "" ]]; then - - ## replace with requested data version - logError "Current version AKA deployment failure somewhere" - sed -i 's||'$NEW_VERSION'|' ~/$GITHUB_REPO_NAME/.env -else - - ## IF LOCAL VERSION AND REMOTE VERSION ARE THE SAME - if [[ $APP_VERSION == $NEW_VERSION ]]; then - - logInfo "VERSION MATCH" - - deleteRunningFile - - logInfo "Finished deployment update check." - exit; - fi - doUpdate -fi - - -logInfo "Delete the running file" - - -deleteRunningFile - - -## TELL USER -logInfo "Finished deployment update check." - -exit 0; - -# -# -# -# -# END - SCRIPT RUNTIME