diff --git a/lib/bashrc_watcher.sh b/lib/bashrc_watcher.sh new file mode 100755 index 0000000..82939ef --- /dev/null +++ b/lib/bashrc_watcher.sh @@ -0,0 +1,86 @@ +function sed_replace { + REGEX=$1 + STRING=$2 + FILE=$3 + if ! grep -Eq "$REGEX" "$FILE"; then + builtin echo -e "$STRING" >>"$FILE" + echo 'appended' + else + sed -Ei "s|$REGEX|$STRING|g" "$FILE" + echo 'replaced' + fi +} + +function bashrc_count_lines { + wc -l "$HOME/.bashrc" | cut -d' ' -f1 +} + +function bashrc_watch_start { + if [[ -z "${BASHRC_LINES+set}" ]]; then + BASHRC_LINES=$(bashrc_count_lines) + else + BASHRC_EVOLVED=true + fi +} + +function bashrc_watch_end { + bashrc_lines=$(bashrc_count_lines) + if [[ "$BASHRC_LINES" -lt "$bashrc_lines" || -n "${BASHRC_EVOLVED+set}" ]]; then + echo + echo '*****************************' + echo '* BASHRC has evolved! *' + echo '* please synchronize: *' + echo '* *' + echo "* source ~/.bashrc *" + echo '* *' + echo '*****************************' + echo + fi +} + +function _bashrc_env_with_prefix { + arg1="$1" + arg2="$2" + prefix="$3" + if printenv | grep -q "$arg1"; then + real_value=$(eval "echo \$$arg1") + echo "success for $arg1 = $real_value" + else + case "$prefix" in + export) + session_line="export ${arg1}=${arg2}" + regex="$session_line" + ;; + eval) + session_line="eval \"\$($arg2)\"" + regex="eval \"\\\$\\($arg2\\)\"" + ;; + *) echo "unknown prefix $prefix" && exit 10 ;; + esac + sed_replace "$regex" "$session_line" "$HOME/.bashrc" &>/dev/null + eval "$session_line" + set +e + trap - EXIT + eval "BASHRC_LINES=$BASHRC_LINES $0" + exit $? + fi + +} + +function bashrc_export { + _bashrc_env_with_prefix "$1" "$2" 'export' +} + +function bashrc_eval { + _bashrc_env_with_prefix "$1" "$2" 'eval' +} + +function on_exit() { + rv=$? + # [[ $rv -eq 0 ]] && bashrc_watch_end + bashrc_watch_end + exit $rv +} + +bashrc_watch_start +trap "on_exit" EXIT diff --git a/lib/common.bash b/lib/common.bash new file mode 100644 index 0000000..9cd5bc1 --- /dev/null +++ b/lib/common.bash @@ -0,0 +1,60 @@ +# library functions + +function confirm { + read -p "$1 ([y]es or [N]o): " + case $(echo $REPLY | tr '[A-Z]' '[a-z]') in + y | yes) echo "yes" ;; + *) echo "no" ;; + esac +} + +function load_app_name { + [[ -z "${PROJECT_DIR:-}" ]] && echo '`PROJECT_DIR` variable should be initialized first!' && exit 4 + local application_ruby="$PROJECT_DIR/config/application.rb" + [[ ! -f $application_ruby ]] && echo "ERROR: $application_ruby not found!" && exit 20 + APP_NAME=$(grep ^module $application_ruby | cut -d' ' -f2) + APP_NAME=${APP_NAME,,} # downcase +} + +function load_database_settings { + local rails_cmd=" + ActiveRecord::Base.configurations.configurations + # .find{it.env_name == '$RAILS_ENV'} + .find{_1.env_name == '$RAILS_ENV'} + .configuration_hash + .each do |k,v| + puts \"#{k}\\t#{v}\" + end + " + local db_settings=$(rails runner "$rails_cmd") + + ADAPTER=$(echo -e "$db_settings" | grep adapter | cut -f2) + DB_NAME=$(echo -e "$db_settings" | grep database | cut -f2) + DB_USER=$(echo -e "$db_settings" | grep user | cut -f2) + DB_PASSWORD=$(echo -e "$db_settings" | grep password | cut -f2) +} + +function load_storage_settings { + STORAGE_DIR=$(rails runner 'puts ActiveStorage::Blob.service.root') +} + +function load_db_version { + DB_VERSION=$(grep -E "define\(version:" "$PROJECT_DIR/db/schema.rb" | cut -d' ' -f2 | cut -d')' -f1) +} + +function load_yamal_d_extra_scripts { + load_app_name + load_db_version + echo "APP_NAME=$APP_NAME" + echo "DB_VERSION=$DB_VERSION" + if [[ -f $PROJECT_DIR/bin/yamal.d/$DB_VERSION.bash ]]; then + echo '------------------------' + echo "YAMAL_D extra script found" + source $PROJECT_DIR/bin/yamal.d/$DB_VERSION.bash + echo '------------------------' + fi +} + +## MAIN + +load_yamal_d_extra_scripts diff --git a/lib/setup-common.bash b/lib/setup-common.bash new file mode 100644 index 0000000..2cc372a --- /dev/null +++ b/lib/setup-common.bash @@ -0,0 +1,39 @@ +## library functions + +function install_pacapt { + if [[ $DISTRO == 'arch' ]]; then + PACMAN_CMD=$(which pacman) + echo 'pacman natively detected!' + else + if [[ ! -f $HOME/.local/bin/pacman ]]; then + [[ $DISTRO == 'debian' ]] && apt install -y apt-utils + mkdir -p $HOME/.local/bin + curl -sLo $HOME/.local/bin/pacman https://github.com/icy/pacapt/raw/ng/pacapt + chmod 755 $HOME/.local/bin/pacman + echo "pacapt/pacman ($PACMAN_CMD) successfully installed!" + fi + PACMAN_CMD="$HOME/.local/bin/pacman" + fi +} + +function install_idem_packages { + if ! pacman -Qi $1 &>/dev/null; then + sudo $HOME/.local/bin/pacman -S --noconfirm $1 >/dev/null + echo "$2 installed successfully" + else + echo "$2 already installed!" + fi +} + +function install_packages { + install_idem_packages "${DISTRO_PACKAGES[_]}" "generic packages" + [ -v "DISTRO_PACKAGES[${DISTRO}]" ] && install_idem_packages "${DISTRO_PACKAGES[${DISTRO}]}" "distro specific packages: [$DISTRO]" + true +} + +function postgres_newdb { + if ! (sudo -u postgres -- psql --csv -tc "SELECT 1 as found FROM pg_roles WHERE rolname = '$DB_USER'" | grep -q ^1$); then + sudo -iu postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_USER'" + sudo -iu postgres psql -c "ALTER USER $DB_USER WITH SUPERUSER" + fi +} diff --git a/lib/setup-prod-normal-user.bash b/lib/setup-prod-normal-user.bash new file mode 100644 index 0000000..22b74ae --- /dev/null +++ b/lib/setup-prod-normal-user.bash @@ -0,0 +1,93 @@ +## CONSTANTS + +REPO_URL=$1 +REPO_BRANCH=$2 +FORCE=${3:-false} + +RAILS_ENV=production +KEY_LENGTH=10 + +## FUNCTIONS + +function install_mise { + if [[ ! -f $HOME/.local/bin/mise ]]; then + echo -n 'installing mise...' + curl -s https://mise.run | sh 2>&1 >/dev/null + echo OK + fi + + if [[ ! -f $HOME/.bashrc ]] || ! grep -q '.local/bin/mise activate bash' $HOME/.bashrc; then + echo "eval \"\$($HOME/.local/bin/mise activate bash)\"" >>$HOME/.bashrc + echo "cd /opt/$USER" >>$HOME/.bashrc + source $HOME/.bashrc + mise version + fi + + if [[ ! -f $HOME/.config/mise/config.toml ]] || ! grep -q 'RAILS_ENV' $HOME/.config/mise/config.toml; then + mise self-update -y 2>&1 >/dev/null + mise settings add idiomatic_version_file_enable_tools ruby + mise set --global RAILS_ENV=production + mise set --global SECRET_KEY_BASE=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c $KEY_LENGTH) + echo mise install successfully for production with SECRET_KEY_BASE + fi + +} + +function pull_repo { + if [[ ! -d .git ]]; then + git config --global init.defaultBranch $REPO_BRANCH + git init + git remote add origin $REPO_URL + fi + + if git fetch origin $REPO_BRANCH --dry-run --verbose 2>&1 | grep -q " =.*\-> origin\/$REPO_BRANCH"; then + set +Eue + $FORCE || exit 100 # means no change + set -Eue + fi + git config pull.rebase true + git pull origin $REPO_BRANCH + git checkout -q $REPO_BRANCH +} + +function install_rails { + mise install + mise exec -- bundle check >/dev/null || mise exec -- bundle install --quiet + mise exec -- rails db:prepare + mise exec -- rails assets:precompile +} + +function assert_params { + [[ $# -ge 2 ]] || (echo '2 params required: REPO_URL REPO_BRANCH [FORCE]' && exit 1) +} + +function link_to_config { + for file in /etc/$USER/*; do + local destination=$(basename $file) + case "$destination" in + \*) + echo "WARNING: no configuration file provided, you may copy from ./config/*_sample.yml to location: /etc/$USER" + ;; + service.conf) + echo "environment (service.conf) converted to .env.production" + ln -sf $(realpath $file) .env.production + ;; + *.yml | *.yaml) + echo "yaml file: $(realpath $file) -> config" + ln -sf $(realpath $file) config/ + ;; + esac + done +} + +## MAIN +set -Eue +assert_params $* + +mkdir -p /opt/$USER +cd /opt/$USER + +pull_repo +link_to_config +install_mise +install_rails diff --git a/setup-prod.bash b/setup-prod.bash new file mode 100755 index 0000000..e83ccc7 --- /dev/null +++ b/setup-prod.bash @@ -0,0 +1,283 @@ +#!/usr/bin/env bash + +## CONSTANTS + +REPO_BRANCH=main +YAMAL_DIR=/home/yamal +YAMAL_CONFIG=$YAMAL_DIR/config +RAILS_BASE_PORT=900 +FORCE=false +DEBUG='' + +PACMAN_CMD='pacman' +SESSION_RESTART=false +OS_RELEASE=/etc/os-release +DISTRO=$(test -f "$OS_RELEASE" && (grep -s ^ID "$OS_RELEASE" | cut -d= -f2) || echo unknown_distro) + +# TODO: project introspection... +declare -A DISTRO_PACKAGES +DISTRO_PACKAGES[_]="postgresql shfmt" +DISTRO_PACKAGES[debian]="build-essential libssl-dev libyaml-dev zlib1g-dev libgmp-dev libpq-dev libvips42 poppler-utils redis-server" +DISTRO_PACKAGES[arch]="base-devel openssl libyaml zlib gmp libvips poppler valkey" + +## FUNCTIONS + +function usage { + echo "$(dirname $0)/$(basename $0) --repo-url|-r [--repo-name|-n ] [--project|-p ] [--branch|-b ] [--force|-f] [--debug|-d]" +} + +function assert_requirements { + [[ ! $SHELL =~ /bash$ ]] && (echo 'bash is mandatory!' && exit 1) + command -v curl >/dev/null || (echo 'curl is mandatory!' && exit 2) + true +} + +function parse_options { + while [[ $# -gt 0 ]]; do + case "$1" in + --repo-url | -r) + shift 1 + [[ -z ${1:-} || $1 =~ ^- ]] && usage && exit 1 + REPO_URL=$1 + ;; + --repo-name | -n) + shift 1 + [[ -z ${1:-} || $1 =~ ^- ]] && usage && exit 1 + REPO_NAME=$1 + ;; + --project | -p) + shift 1 + [[ -z ${1:-} || $1 =~ ^- ]] && usage && exit 1 + PROJECT_NAME=$1 + ;; + --branch | -b) + shift 1 + [[ -z ${1:-} || $1 =~ ^- ]] && usage && exit 1 + REPO_BRANCH=$1 + ;; + --origin | -o) + shift 1 + [[ -z ${1:-} || $1 =~ ^- ]] && usage && exit 1 + ORIGIN=$1 + ;; + --force | -f) + FORCE=true + ;; + --debug | -d) + DEBUG='bash -xl' + ;; + --help | -h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" + usage + exit 2 + ;; + esac + + shift 1 # Move to the next argument + done +} + +function assert_repo_url { + if [[ -z ${REPO_URL:-} ]]; then + >&2 echo -e "ERROR3: REPO_URL undefined!\n\tplease either use --repo-url or -r\n" + usage + exit 3 + fi +} + +function create_user { + if ! id -u $PROJECT_NAME >/dev/null 2>&1; then + [[ -d /opt/$PROJECT_NAME ]] && + echo -e "ERROR\n special opt folder not empty: /opt/$PROJECT_NAME\nplease specify unique project name with argument --project|-p" && + exit 20 + useradd -G ssh,yamal -rm --home-dir $YAMAL_DIR/projects/$PROJECT_NAME --shell /bin/bash $PROJECT_NAME + echo "user $PROJECT_NAME successfully created!" + fi + + if [[ ! -d /opt/$PROJECT_NAME ]]; then + mkdir -p /opt/$PROJECT_NAME + chown $PROJECT_NAME:$PROJECT_NAME /opt/$PROJECT_NAME + fi + + if [[ ! -d /etc/$PROJECT_NAME ]]; then + mkdir -p /etc/$PROJECT_NAME + echo "special folder: /etc/$PROJECT_NAME successfully created!" + fi + chown -R $PROJECT_NAME:$PROJECT_NAME /etc/$PROJECT_NAME +} + +function set_project_name { + [[ -z ${REPO_NAME:-} ]] && REPO_NAME=$(echo $REPO_URL | rev | cut -d/ -f1 | rev |cut -d. -f1) # get last url part minus '.git' + DB_USER=$REPO_NAME + [[ -z ${PROJECT_NAME:-} ]] && PROJECT_NAME=$REPO_NAME + true +} + +function define_rails_app { + if [[ -f $YAMAL_CONFIG ]]; then + local index=$(grep "^rails_app_.*=$PROJECT_NAME\$" $YAMAL_CONFIG | cut -d '=' -f1 | cut -d '_' -f3) + [[ -z $index ]] && index=$(grep "rails_app_count=" $YAMAL_CONFIG | cut -d '=' -f2) + fi + RAILS_APP=${index:-0} +} + +function inc_rails_app_count { + if [[ -f $YAMAL_CONFIG ]]; then + if ! grep -q "^rails_app_.*=$PROJECT_NAME\$" $YAMAL_CONFIG; then + echo "rails_app_$RAILS_APP=$PROJECT_NAME" >>$YAMAL_CONFIG + RAILS_APP=$(($RAILS_APP + 1)) + sed -i "s/^rails_app_count.*/rails_app_count=$RAILS_APP/" $YAMAL_CONFIG + echo "new rails app succesfully registered!" + fi + else + mkdir -p $(dirname $YAMAL_CONFIG) + echo "rails_app_count=1" >$YAMAL_CONFIG + echo "rails_app_$RAILS_APP=$PROJECT_NAME" >>$YAMAL_CONFIG + fi +} + +function install_systemd_service { + local service="/etc/systemd/system/$PROJECT_NAME.service" + local rails_port=$(($RAILS_BASE_PORT + $RAILS_APP)) + if [[ ! -f $service ]]; then + cat <$service +[Unit] +Description=$PROJECT_NAME +After=network.target + +[Service] +Type=simple +User=$PROJECT_NAME +SyslogIdentifier=$PROJECT_NAME +AmbientCapabilities=CAP_NET_BIND_SERVICE +PermissionsStartOnly=true +WorkingDirectory=/opt/$PROJECT_NAME +ExecStart=$YAMAL_DIR/projects/$PROJECT_NAME/.local/bin/mise exec -- rails server --port $rails_port + +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload + systemctl enable $PROJECT_NAME + fi + systemctl restart $PROJECT_NAME + systemctl is-active $PROJECT_NAME >/dev/null && echo "service $PROJECT_NAME running on port: $rails_port" +} + +function launch_normal_user_setup { + # clean temporary files in case mise install ruby fails! + rm /tmp/mise-ruby-build -rf + + echo "launch setup as normal user: $PROJECT_NAME" + local cmd="${DEBUG:-bash -l} -- $(realpath $(dirname "$0"))/lib/setup-prod-normal-user.bash $REPO_URL $REPO_BRANCH $FORCE" + if sudo -iu $PROJECT_NAME $cmd; then + : #ok + else + local exit_code=$? + [[ $exit_code == 100 ]] && echo 'up-to-date: no change!' && exit 0 + echo "an error $exit_code has occured during 'setup-prod-normal-user'" && exit $exit_code + fi +} + +function enhance_mise_user_from_origin { + [[ -z ${ORIGIN:-} ]] && return + if [[ -d $ORIGIN/.local/share/mise/installs/ruby ]] && [[ ! -f /home/$PROJECT_NAME/.local/bin/mise ]]; then + echo -n "copy mise ruby from origin=$ORIGIN..." + mkdir -p /home/$PROJECT_NAME/.local/share/mise/installs/ruby + cp -r $ORIGIN/.local/share/mise/installs/ruby /home/$PROJECT_NAME/.local/share/mise/installs/ + chown -R $PROJECT_NAME:$PROJECT_NAME /home/$PROJECT_NAME/.local + echo OK + fi +} + +function install_pacapt { + if [[ ! -f $HOME/.local/bin/pacman ]]; then + mkdir -p $HOME/.local/bin + curl -sLo $HOME/.local/bin/pacman https://github.com/icy/pacapt/raw/ng/pacapt + chmod 755 $HOME/.local/bin/pacman + else + if [[ $DISTRO == 'arch' ]]; then + echo 'pacman natively detected!' + else + PACMAN_CMD="$HOME/.local/bin/pacman" + # echo "pacapt/pacman ($PACMAN_CMD) already installed!" + fi + fi +} + +function install_idem_packages { + if ! pacman -Qi $1 &>/dev/null; then + sudo $HOME/.local/bin/pacman -S --noconfirm $1 >/dev/null + echo "$2 installed successfully" + else + echo "$2 already installed!" + fi +} + +function install_packages { + install_idem_packages "${DISTRO_PACKAGES[_]}" "generic packages" + [ -v "DISTRO_PACKAGES[${DISTRO}]" ] && install_idem_packages "${DISTRO_PACKAGES[${DISTRO}]}" "distro specific packages: [$DISTRO]" + true +} + +function assert_etc_config_for_user { + local config_dir="/etc/$PROJECT_NAME" + if [[ -d $config_dir ]]; then + chown $PROJECT_NAME $config_dir + chmod -R 750 $config_dir + else + echo -e "-----\nERROR\n-----\nplease provide configuration file from location:\n $config_dir" + false + fi +} + +function postgres_newdb { + if ! (sudo -u postgres -- psql --csv -tc "SELECT 1 as found FROM pg_roles WHERE rolname = '$DB_USER'" | grep -q ^1$); then + echo "creating postgresql user: $DB_USER" + sudo -iu postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_USER'" + sudo -iu postgres psql -c "ALTER USER $DB_USER WITH SUPERUSER" + fi +} + +function postgres_global_encoding_utf8 { + if ! sudo -iu postgres -- psql -c '\l template1' | grep -q UTF8; then + echo 'defining postgresql template1 with default UTF8 encoding' + sudo -iu postgres -- psql <