Files
db-middleware/scripts/manager.sh

747 lines
20 KiB
Bash
Executable File

#!/bin/bash
APP_DIR="$HOME/.db-middleware"
CODE_DIR="$APP_DIR/code"
CONFIG_DIR="$APP_DIR/configs"
CONFIG_FILE="$CONFIG_DIR/app.conf"
REPO_URL="https://gitea.abdulhade.com/abdulhade/db-middleware.git"
LOADED_CONFIG=0
# Default values
APP_NAME="Database Middleware"
IMAGE_NAME="db-middleware"
CONTAINER_NAME="con-db-middleware"
EXECUTION_MESSAGE="NO-RETURN"
print_header() {
local HEADER_TEXT="$1"
local MAX_LENGTH=0
# Split the header text into lines and find the maximum line length
while IFS= read -r LINE; do
local LINE_LENGTH=${#LINE}
if (( LINE_LENGTH > MAX_LENGTH )); then
MAX_LENGTH=$LINE_LENGTH
fi
done <<< "$HEADER_TEXT"
local BORDER_LENGTH=$((MAX_LENGTH + 2)) # Add 4 for padding (2 spaces)
echo
# Print the top border
printf '+%*s+\n' "$BORDER_LENGTH" "" | tr ' ' '-'
# Print each line of the header text
while IFS= read -r LINE; do
printf "| %-*s |\n" "$MAX_LENGTH" "$LINE"
done <<< "$HEADER_TEXT"
# Print the bottom border
printf '+%*s+\n' "$BORDER_LENGTH" "" | tr ' ' '-'
echo
}
build_docker_image() {
if ! cd "$CODE_DIR"; then
print_header "Failed to navigate to $CODE_DIR."
return 1
fi
echo "Building Docker image..."
# Check if BuildKit is installed
if docker buildx version &>/dev/null; then
echo "BuildKit detected. Enabling BuildKit..."
export DOCKER_BUILDKIT=1
else
echo "BuildKit not found. Using legacy builder..."
export DOCKER_BUILDKIT=0
fi
echo
echo
echo
if docker build -t "$IMAGE_NAME" .; then
echo "Docker image built successfully."
return 0
else
echo "Failed to build Docker image."
return 1
fi
}
set_up_middleware() {
print_header "Rebuilding the Docker Container..."
if ! build_docker_image; then
print_header "Can't build the Docker image."
return 1 # Error
fi
if ! set_up_scripts; then
print_header "Failed to set up scripts."
return 1 # Error
fi
print_header "Rebuilt the Docker Container."
return 0
}
test() {
echo "I am here"
return 1
}
exec_in_container() {
# Returns 0 if the command was successful
local CONTAINER_NAME=$CONTAINER_NAME
local COMMAND="$1"
# Check if the container is running
if ! docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
print_header "Error: Container '$CONTAINER_NAME' is not running.
We can't execute commands inside it, you can run it with:
>>> $0 start"
return 1
fi
# Execute the command in the container
OUTPUT=$(docker exec "$CONTAINER_NAME" bash -c "$COMMAND" 2>&1)
local EXIT_CODE=$?
# Check if the command succeeded
if [[ $EXIT_CODE -eq 0 ]]; then
EXECUTION_MESSAGE="$OUTPUT" # Return the output
return 0
else
print_header "Error: Command failed in container '$CONTAINER_NAME' with exit code $EXIT_CODE.
Output: $OUTPUT"
return $EXIT_CODE
fi
}
set_up_scripts() {
echo "Setting up scripts..."
# Copy scripts
if ! cp "$CODE_DIR/scripts/"* "$APP_DIR/scripts/"; then
print_header "Failed to copy scripts."
return 1
fi
# Give execution permission
if ! chmod +x "$APP_DIR/scripts/"*; then
print_header "Failed to set execution permissions."
return 1
fi
# Create ~/.local/bin if it doesn't exist
mkdir -p "$HOME/.local/bin"
# Create symlink
if ! ln -sf "$APP_DIR/scripts/manager.sh" "$HOME/.local/bin/db-middleware"; then
print_header "Failed to create symlink."
# TODO Here we should handle the case of having the symlink created previously.
return 1
fi
# Update PATH
if ! grep -q "$HOME/.local/bin" ~/.bashrc; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
fi
# Reload bashrc
if ! source ~/.bashrc; then
print_header "Failed to reload ~/.bashrc."
return 1
fi
echo "Scripts set up successfully."
return 0
}
create_user(){
read -p "Enter username: " USERNAME
read -p "Enter role (admin/user): " ROLE_INPUT
local COMMAND="python3 -c 'from utils.scripts import create_user_script_sync;create_user_script_sync(username=\"$USERNAME\", role_input=\"$ROLE_INPUT\")'"
exec_in_container "$COMMAND"
local RETURN_CODE=$?
if [[ $RETURN_CODE -eq 0 ]]; then
print_header "$EXECUTION_MESSAGE"
else
print_header "Couldn't create the user due to the above error."
fi
}
update_config() {
print_header "Update configs."
echo "Key: API_PORT, Current value: \`$API_PORT\`"
echo "This is the internal port on your server's localhost network, where the Middleware will be listening to."
read -p " > Enter new value (leave empty to pass): " NEW_API_PORT
echo
if [[ $NEW_API_PORT == "" ]]; then
NEW_API_PORT=$API_PORT
fi
echo "Key: CONTAINER_NAME, Current value: \`$CONTAINER_NAME\`"
echo "This is the name of the Docker Container we will create for this middleware."
read -p " > Enter new value (leave empty to pass): " NEW_CONTAINER_NAME
echo
if [[ $NEW_CONTAINER_NAME == "" ]]; then
NEW_CONTAINER_NAME=$CONTAINER_NAME
fi
echo "Key: HAS_LOCAL_DBS, Current value: \`$HAS_LOCAL_DBS\`"
echo "This will change the type of the network of the docker container, between a bridge or a host network."
echo "Enter [0] If your database is on this server"
echo "Enter [1] if your database is on another server"
read -p " > Enter new value (leave empty to pass): " NEW_HAS_LOCAL_DBS
echo
if [[ $NEW_HAS_LOCAL_DBS == "" ]]; then
NEW_HAS_LOCAL_DBS=$HAS_LOCAL_DBS
fi
print_header "Here are the old and new values:
API_PORT: \`$API_PORT > $NEW_API_PORT\`
CONTAINER_NAME: \`$CONTAINER_NAME > $NEW_CONTAINER_NAME\`
HAS_LOCAL_DBS: \`$HAS_LOCAL_DBS > $NEW_HAS_LOCAL_DBS\`"
read -p " > To confirm these changes, enter (y) otherwise enter anything: " CONFIRM_CHANGES
if [[ $CONFIRM_CHANGES == "y" ]]; then
API_PORT=$NEW_API_PORT
CONTAINER_NAME=$NEW_CONTAINER_NAME
HAS_LOCAL_DBS=$NEW_HAS_LOCAL_DBS
write_config
print_header "Saved the new configs! You can now rebuild the docker container."
return 0
fi
print_header "Discarded the changes."
}
write_config() {
CONFIG_TEXT="
# Application settings
APP_NAME=\"$APP_NAME\"
API_PORT=\"$API_PORT\"
# Docker Container settings
IMAGE_NAME=\"$IMAGE_NAME\"
CONTAINER_NAME=\"$CONTAINER_NAME\"
# Databases settings
HAS_LOCAL_DBS=\"$HAS_LOCAL_DBS\"
"
echo "$CONFIG_TEXT" > "$CONFIG_FILE"
}
load_config() {
if [[ -f "$CONFIG_FILE" ]]; then
if source "$CONFIG_FILE"; then
LOADED_CONFIG=1
echo "Config file loaded successfully."
return 0
else
print_header "Failed to load config file."
return 1
fi
else
print_header "Config file not found: $CONFIG_FILE"
return 1
fi
}
get_run_command() {
local RUN_COMMAND="docker run -d --name $CONTAINER_NAME"
if [[ $HAS_LOCAL_DBS -eq 1 || $HAS_LOCAL_DBS == "1" ]]; then
RUN_COMMAND+=" --env API_HOST=localhost --env API_PORT=${API_PORT:-8080} --network host"
else
RUN_COMMAND+=" --env API_HOST=0.0.0.0 --env API_PORT=${API_PORT:-8080} -p ${API_PORT:-8080}:8080"
fi
RUN_COMMAND+=" -v /home/$USER/.db-middleware/files:/app/files $IMAGE_NAME"
echo $RUN_COMMAND
}
show_config() {
if [[ $LOADED_CONFIG -eq 0 ]]; then
print_header "Config not loaded. Please load the config file first."
return 1
fi
local RUN_COMMAND=$(get_run_command)
print_header "Current Config:
CONTAINER_NAME: ${CONTAINER_NAME:-Not set}
API_PORT: ${API_PORT:-Not set}
HAS_LOCAL_DBS: ${HAS_LOCAL_DBS:-Not set}
Run Command: $RUN_COMMAND"
return 0
}
convert_ports_to_docker_args() {
local ports="$1"
local docker_args=""
if [[ -z "$ports" ]]; then
echo "No ports provided."
return 1
fi
# Split the ports by comma and trim whitespace
IFS=',' read -r -a port_array <<< "$ports"
# Loop through the ports and format them as Docker arguments
for port in "${port_array[@]}"; do
port=$(echo "$port" | xargs) # Trim whitespace
if [[ ! "$port" =~ ^[0-9]+$ ]]; then
echo "Invalid port: $port"
return 1
fi
docker_args+=" -p $port:$port"
done
echo "$docker_args"
return 0
}
install() {
print_header "Installing the Middleware..."
if ! cd "$CODE_DIR"; then
print_header "Failed to navigate to $CODE_DIR."
return 1
fi
update_config
print_header "Building the Docker Image..."
if ! build_docker_image; then
print_header "Failed to build Docker image."
return 1
fi
print_header "Installed the Middleware Successfully!
- You can run the middleware simply using the manager:
>>> db-middleware start
- Or directly by running the docker container:
>>> $(get_run_command)"
return 0
}
pull_clone_repo() {
local UP_TO_DATE=-1 # Default: No changes
echo "Setting up repository..."
# Create directories if they don't exist
mkdir -p "$APP_DIR/{code,configs,scripts}"
# Navigate to the code directory
if ! cd "$CODE_DIR"; then
echo "Failed to navigate to $CODE_DIR."
return 1 # Error
fi
# Check if the directory is empty
if [[ -z "$(ls -A $CODE_DIR)" ]]; then
echo "Directory is empty. Cloning repository..."
if ! git clone "$REPO_URL" .; then
echo "Failed to clone repository."
return 1 # Error
fi
echo "Repository cloned successfully."
UP_TO_DATE=0 # Changes were applied
else
# Check if the directory contains a Git repository
if [[ -d ".git" ]]; then
echo "Directory contains a Git repository."
# Fetch the latest changes
if ! git fetch origin; then
echo "Failed to fetch changes from remote."
return 1 # Error
fi
# Get the local and remote HEAD commit hashes
echo
LOCAL_HEAD=$(git rev-parse HEAD)
REMOTE_HEAD=$(git rev-parse origin/main)
exec_in_container "git rev-parse HEAD"
local RETURN_CODE=$?
CONTAINER_HEAD=$EXECUTION_MESSAGE
EXECUTION_MESSAGE="NO-RETURN"
echo
echo "- Commit hash in remote origin: $REMOTE_HEAD"
echo "- Commit hash in local repo: $LOCAL_HEAD"
if [[ $RETURN_CODE -eq 0 ]]; then
echo "- Commit hash in container: $CONTAINER_HEAD"
else
echo "Failed to get commit hash from container."
fi
echo
if [[ "$CONTAINER_HEAD" == "$LOCAL_HEAD" && "$LOCAL_HEAD" == "$REMOTE_HEAD" ]]; then
echo "Repo is up to date."
UP_TO_DATE=-1
elif [[ "$LOCAL_HEAD" != "$REMOTE_HEAD" ]]; then
echo "Remote repository has new changes. Pulling latest changes..."
echo
if ! git pull "$REPO_URL"; then
echo "Failed to pull changes."
return 1 # Error
fi
echo
echo "Local Repository updated successfully."
LOCAL_HEAD=$(git rev-parse HEAD)
UP_TO_DATE=0 # Changes were applied
else
echo "Local Repository is already up to date."
UP_TO_DATE=-1 # No changes
# echo $UP_TO_DATE
fi
if [[ "$LOCAL_HEAD" != "$CONTAINER_HEAD" ]]; then
echo "Container Repository is not up to date."
UP_TO_DATE=0
fi
else
echo "Directory is not empty and does not contain a Git repository."
echo "Please ensure the directory is empty or contains a valid Git repository."
return 1 # Error
fi
fi
echo "Repository setup completed successfully."
return $UP_TO_DATE # -1 = No changes, 0 = Changes applied
}
update_code() {
print_header "Checking for updates..."
pull_clone_repo
local UP_TO_DATE=$?
case $UP_TO_DATE in
# 255 is -1, because Bash return codes are unsigned 8-bit integer, limited to the range 0 to 255.
-1|255)
print_header "No changes detected."
return -1 # No changes
;;
0)
print_header "Changes were detected and applied to local repo, but not to the container.
Need to rebuild the container, run:
>>> db-middleware upgrade
"
return 0 # Changes applied
;;
1)
print_header "Failed to update or clone repository."
return 1 # Error
;;
*)
print_header "Wrong return code: \`$UP_TO_DATE\` from pull_clone_repo."
return 1
;;
esac
}
upgrade() {
print_header "Upgrading the Middleware..."
update_code
local UPDATE_RESULT=$?
case $UPDATE_RESULT in
# 255 is -1, because Bash return codes are unsigned 8-bit integer, limited to the range 0 to 255.
-1|255)
print_header "No changes detected. Skipping upgrade..."
return -1
;;
0)
if ! set_up_middleware; then
print_header "Failed to rebuild Docker image."
return 1 # Error
fi
;;
1)
print_header "Failed to update code. Upgrade aborted."
return 1 # Error
;;
esac
print_header "Upgraded the Docker image.
We will stop & delete the current container and start a new one..."
stop
docker remove $CONTAINER_NAME
start
print_header "Upgraded the Middleware Successfully!"
return 0
}
status() {
print_header "Checking container status..."
# Check if the container exists
if ! docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
print_header "Container '$CONTAINER_NAME' does not exist."
return 1
fi
# Get container status
local CONTAINER_STATUS
CONTAINER_STATUS=$(docker ps --filter "name=$CONTAINER_NAME" --format "{{.Status}}")
if [[ -z "$CONTAINER_STATUS" ]]; then
print_header "Container '$CONTAINER_NAME' is not running."
return 0
fi
# Get detailed container information using `docker inspect`
local CONTAINER_INFO
CONTAINER_INFO=$(docker inspect "$CONTAINER_NAME" 2>/dev/null)
if [[ -z "$CONTAINER_INFO" ]]; then
print_header "Failed to inspect container '$CONTAINER_NAME'."
return 1
fi
# Extract useful information
local CPU_PERCENT MEM_USAGE MEM_PERCENT NET_IO BLOCK_IO IP_ADDRESS PORTS RUN_COMMAND NETWORK_MODE
# Resource usage (CPU, memory, storage)
STATS_OUTPUT=$(docker stats --no-stream --format "{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}" "$CONTAINER_NAME")
# Split the output into individual variables
IFS=$'\t' read -r CPU_PERCENT MEM_USAGE MEM_PERCENT NET_IO BLOCK_IO <<< "$STATS_OUTPUT"
# Network information
NETWORK_MODE=$(echo "$CONTAINER_INFO" | grep -oP '"NetworkMode": "\K[^"]+')
if [[ "$NETWORK_MODE" == "host" ]]; then
IP_ADDRESS="Host Network (No separate IP)"
PORTS="Host Network (Ports are directly bound to host)"
else
IP_ADDRESS=$(echo "$CONTAINER_INFO" | grep -oP '"IPAddress": "\K[^"]+')
# Extract port mappings from docker ps --no-trunc
PORTS=$(docker ps --filter "name=$CONTAINER_NAME" --no-trunc --format "{{.Ports}}")
if [[ -z "$PORTS" ]]; then
PORTS="No port mappings"
fi
fi
# Run command (from docker ps --no-trunc)
RUN_COMMAND=$(docker ps --filter "name=$CONTAINER_NAME" --no-trunc --format "{{.Command}}")
# Build the output string
local OUTPUT
OUTPUT="Database Middleware Status:
[Container]
Name: $CONTAINER_NAME
Status: $CONTAINER_STATUS
[Performance]
CPU Usage: $CPU_PERCENT
Memory Usage: $MEM_USAGE ($MEM_PERCENT)
Block I/O: $BLOCK_IO
Network I/O: $NET_IO
[Network]
Network Mode: $NETWORK_MODE
IP Address: $IP_ADDRESS
Ports: $PORTS
[App]
Run Command: $RUN_COMMAND"
print_header "$OUTPUT"
return 0
}
start() {
RUN_COMMAND=$(get_run_command)
print_header "Starting the Container...
With run command:
>>> $RUN_COMMAND"
# Check if a container with the same name already exists
if docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
# Check if the container is already running
if docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
print_header "Container '$CONTAINER_NAME' is already running.
You can restart it with:
>>> $0 restart"
return 0
else
# Start the existing container
if docker start "$CONTAINER_NAME"; then
print_header "Started the existing container '$CONTAINER_NAME' successfully."
return 0
else
print_header "Failed to start the existing container '$CONTAINER_NAME'."
return 1
fi
fi
else
# Run a new container
if eval "$RUN_COMMAND"; then
print_header "Started the container '$CONTAINER_NAME' successfully."
return 0
else
print_header "Failed to start the container '$CONTAINER_NAME'."
return 1
fi
fi
}
restart() {
print_header "Restarting the Container..."
# Check if a container with the same name exists (running or stopped)
if docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
# Restart the container
if docker restart "$CONTAINER_NAME"; then
print_header "Restarted the container '$CONTAINER_NAME' successfully."
return 0
else
print_header "Failed to restart the container '$CONTAINER_NAME'."
return 1
fi
else
print_header "Container '$CONTAINER_NAME' does not exist. Cannot restart.
You can start it with:
>>> $0 start"
return 1
fi
}
stop() {
print_header "Stopping the Container..."
# Check if the container exists (running or stopped)
if docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
# Check if the container is running
if docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
# Stop the container
if docker stop "$CONTAINER_NAME"; then
print_header "Stopped the container '$CONTAINER_NAME' successfully."
return 0
else
print_header "Failed to stop the container '$CONTAINER_NAME'."
return 1
fi
else
print_header "Container '$CONTAINER_NAME' is not running."
return 0
fi
else
print_header "Container '$CONTAINER_NAME' does not exist. Cannot stop."
return 1
fi
}
help() {
print_header "help:
Source Code Management:
> install
> update_code
> upgrade
> rebuild
App Running:
> status
> start
> restart
> stop
Users Management:
> create_user
Configurations:
> show_config
> update_config
Help:
> help
"
exit 1
}
main() {
# Check if an argument is provided
if [[ $# -eq 0 ]]; then
help
fi
if ! load_config; then
print_header "Failed to load config. Exiting."
exit 1
fi
# Handle the argument
case "$1" in
install) install ;;
update_code) update_code ;;
create_user) create_user ;;
upgrade) upgrade ;;
rebuild) set_up_middleware;;
start) start ;;
restart) restart;;
status) status ;;
stop) stop ;;
show_config) show_config ;;
update_config) update_config ;;
help) help ;;
*) echo "Invalid argument: $1"; help ;;
esac
}
# Run the script with the provided arguments
main "$@"