Files
plane/deploy/selfhost/install.sh

569 lines
17 KiB
Bash
Executable File

#!/bin/bash
BRANCH=${BRANCH:-master}
SCRIPT_DIR=$PWD
SERVICE_FOLDER=plane-app
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE="stable"
export DOCKERHUB_USER=makeplane
export PULL_POLICY=${PULL_POLICY:-if_not_present}
CPU_ARCH=$(uname -m)
OS_NAME=$(uname)
UPPER_CPU_ARCH=$(tr '[:lower:]' '[:upper:]' <<< "$CPU_ARCH")
mkdir -p $PLANE_INSTALL_DIR/archive
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
SED_PREFIX=()
if [ "$OS_NAME" == "Darwin" ]; then
SED_PREFIX=("-i" "")
else
SED_PREFIX=("-i")
fi
function print_header() {
clear
cat <<"EOF"
--------------------------------------------
____ _ /////////
| _ \| | __ _ _ __ ___ /////////
| |_) | |/ _` | '_ \ / _ \ ///// /////
| __/| | (_| | | | | __/ ///// /////
|_| |_|\__,_|_| |_|\___| ////
////
--------------------------------------------
Project management tool from the future
--------------------------------------------
EOF
}
function spinner() {
local pid=$1
local delay=.5
local spinstr='|/-\'
if ! ps -p "$pid" > /dev/null; then
echo "Invalid PID: $pid"
return 1
fi
while ps -p "$pid" > /dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr" >&2
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b" >&2
done
printf " \b\b\b\b" >&2
}
function initialize(){
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${UPPER_CPU_ARCH} support." >&2
if [ "$CUSTOM_BUILD" == "true" ]; then
echo "" >&2
echo "" >&2
echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
echo "build"
return 1
fi
local IMAGE_NAME=makeplane/plane-proxy
local IMAGE_TAG=${APP_RELEASE}
docker manifest inspect "${IMAGE_NAME}:${IMAGE_TAG}" | grep -q "\"architecture\": \"${CPU_ARCH}\"" &
local pid=$!
spinner "$pid"
echo "" >&2
wait "$pid"
if [ $? -eq 0 ]; then
echo "Plane supports ${CPU_ARCH}" >&2
echo "available"
return 0
else
echo "" >&2
echo "" >&2
echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
echo "" >&2
echo "build"
return 1
fi
}
function getEnvValue() {
local key=$1
local file=$2
if [ -z "$key" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
grep -q "^$key=" "$file"
if [ $? -eq 0 ]; then
local value
value=$(grep "^$key=" "$file" | cut -d'=' -f2)
echo "$value"
else
echo ""
fi
fi
}
function updateEnvFile() {
local key=$1
local value=$2
local file=$3
if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then
echo "Invalid arguments supplied"
exit 1
fi
if [ -f "$file" ]; then
# check if key exists in the file
grep -q "^$key=" "$file"
if [ $? -ne 0 ]; then
echo "$key=$value" >> "$file"
return
else
# if key exists, update the value
sed "${SED_PREFIX[@]}" "s/^$key=.*/$key=$value/g" "$file"
fi
else
echo "File not found: $file"
exit 1
fi
}
function updateCustomVariables(){
echo "Updating custom variables..." >&2
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
echo "Custom variables updated successfully" >&2
}
function syncEnvFile(){
echo "Syncing environment variables..." >&2
if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then
updateCustomVariables
# READ keys of plane.env and update the values from plane.env.bak
while IFS= read -r line
do
# ignore is the line is empty or starts with #
if [ -z "$line" ] || [[ $line == \#* ]]; then
continue
fi
key=$(echo "$line" | cut -d'=' -f1)
value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak")
if [ -n "$value" ]; then
updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH"
fi
done < "$DOCKER_ENV_PATH"
fi
echo "Environment variables synced successfully" >&2
}
function buildYourOwnImage(){
echo "Building images locally..."
export DOCKERHUB_USER="myplane"
export APP_RELEASE="local"
export PULL_POLICY="never"
CUSTOM_BUILD="true"
# checkout the code to ~/tmp/plane folder and build the images
local PLANE_TEMP_CODE_DIR=~/tmp/plane
rm -rf $PLANE_TEMP_CODE_DIR
mkdir -p $PLANE_TEMP_CODE_DIR
REPO=https://github.com/makeplane/plane.git
git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1
cp "$PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml"
cd "$PLANE_TEMP_CODE_DIR" || exit
/bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
if [ $? -ne 0 ]; then
echo "Build failed. Exiting..."
exit 1
fi
echo "Build completed successfully"
echo ""
echo "You can now start the services by running the command: ./setup.sh start"
echo ""
}
function install() {
echo "Begin Installing Plane"
echo ""
local build_image=$(initialize)
if [ "$build_image" == "build" ]; then
# ask for confirmation to continue building the images
echo "Do you want to continue with building the Docker images locally?"
read -p "Continue? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Exiting..."
exit 0
fi
fi
if [ "$build_image" == "build" ]; then
download "true"
else
download "false"
fi
}
function download() {
local LOCAL_BUILD=$1
cd $SCRIPT_DIR
TS=$(date +%s)
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
then
mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml
fi
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/docker-compose.yaml https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s)
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/variables-upgrade.env https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s)
if [ -f "$DOCKER_ENV_PATH" ];
then
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
fi
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
syncEnvFile
if [ "$LOCAL_BUILD" == "true" ]; then
export DOCKERHUB_USER="myplane"
export APP_RELEASE="local"
export PULL_POLICY="never"
CUSTOM_BUILD="true"
buildYourOwnImage
if [ $? -ne 0 ]; then
echo ""
echo "Build failed. Exiting..."
exit 1
fi
updateCustomVariables
else
CUSTOM_BUILD="false"
updateCustomVariables
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull --policy always"
if [ $? -ne 0 ]; then
echo ""
echo "Failed to pull the images. Exiting..."
exit 1
fi
fi
echo ""
echo "Most recent version of Plane is now available for you to use"
echo ""
echo "In case of 'Upgrade', please check the 'plane.env 'file for any new variables and update them accordingly"
echo ""
}
function startServices() {
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --pull if_not_present --quiet-pull"
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
if [ -n "$migrator_container_id" ]; then
local idx=0
while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
local message=">> Waiting for Data Migration to finish"
local dots=$(printf '%*s' $idx | tr ' ' '.')
echo -ne "\r$message$dots"
((idx++))
sleep 1
done
fi
printf "\r\033[K"
echo ""
echo " Data Migration completed successfully ✅"
# if migrator exit status is not 0, show error message and exit
if [ -n "$migrator_container_id" ]; then
local migrator_exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container_id)
if [ $migrator_exit_code -ne 0 ]; then
echo "Plane Server failed to start ❌"
# stopServices
echo
echo "Please check the logs for the 'migrator' service and resolve the issue(s)."
echo "Stop the services by running the command: ./setup.sh stop"
exit 1
fi
fi
local api_container_id=$(docker container ls -q -f "name=$SERVICE_FOLDER-api")
local idx2=0
while ! docker logs $api_container_id 2>&1 | grep -m 1 -i "Application startup complete" | grep -q ".";
do
local message=">> Waiting for API Service to Start"
local dots=$(printf '%*s' $idx2 | tr ' ' '.')
echo -ne "\r$message$dots"
((idx2++))
sleep 1
done
printf "\r\033[K"
echo " API Service started successfully ✅"
source "${DOCKER_ENV_PATH}"
echo " Plane Server started successfully ✅"
echo ""
echo " You can access the application at $WEB_URL"
echo ""
}
function stopServices() {
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH down"
}
function restartServices() {
stopServices
startServices
}
function upgrade() {
echo "***** STOPPING SERVICES ****"
stopServices
echo
echo "***** DOWNLOADING STABLE VERSION ****"
install
echo "***** PLEASE VALIDATE AND START SERVICES ****"
}
function viewSpecificLogs(){
local SERVICE_NAME=$1
if /bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH ps | grep -q '$SERVICE_NAME'"; then
echo "Service '$SERVICE_NAME' is running."
else
echo "Service '$SERVICE_NAME' is not running."
fi
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH logs -f $SERVICE_NAME"
}
function viewLogs(){
ARG_SERVICE_NAME=$2
if [ -z "$ARG_SERVICE_NAME" ];
then
echo
echo "Select a Service you want to view the logs for:"
echo " 1) Web"
echo " 2) Space"
echo " 3) API"
echo " 4) Worker"
echo " 5) Beat-Worker"
echo " 6) Migrator"
echo " 7) Proxy"
echo " 8) Redis"
echo " 9) Postgres"
echo " 10) Minio"
echo " 0) Back to Main Menu"
echo
read -p "Service: " DOCKER_SERVICE_NAME
until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 10 )); do
echo "Invalid selection. Please enter a number between 1 and 11."
read -p "Service: " DOCKER_SERVICE_NAME
done
if [ -z "$DOCKER_SERVICE_NAME" ];
then
echo "INVALID SERVICE NAME SUPPLIED"
else
case $DOCKER_SERVICE_NAME in
1) viewSpecificLogs "web";;
2) viewSpecificLogs "space";;
3) viewSpecificLogs "api";;
4) viewSpecificLogs "worker";;
5) viewSpecificLogs "beat-worker";;
6) viewSpecificLogs "migrator";;
7) viewSpecificLogs "proxy";;
8) viewSpecificLogs "plane-redis";;
9) viewSpecificLogs "plane-db";;
10) viewSpecificLogs "plane-minio";;
0) askForAction;;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
fi
elif [ -n "$ARG_SERVICE_NAME" ];
then
ARG_SERVICE_NAME=$(echo "$ARG_SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
case $ARG_SERVICE_NAME in
web) viewSpecificLogs "web";;
space) viewSpecificLogs "space";;
api) viewSpecificLogs "api";;
worker) viewSpecificLogs "worker";;
beat-worker) viewSpecificLogs "beat-worker";;
migrator) viewSpecificLogs "migrator";;
proxy) viewSpecificLogs "proxy";;
redis) viewSpecificLogs "plane-redis";;
postgres) viewSpecificLogs "plane-db";;
minio) viewSpecificLogs "plane-minio";;
*) echo "INVALID SERVICE NAME SUPPLIED";;
esac
else
echo "INVALID SERVICE NAME SUPPLIED"
fi
}
function backupSingleVolume() {
backupFolder=$1
selectedVolume=$2
# Backup data from Docker volume to the backup folder
# docker run --rm -v "$selectedVolume":/source -v "$backupFolder":/backup busybox sh -c 'cp -r /source/* /backup/'
local tobereplaced="plane-app_"
local replacewith=""
local svcName="${selectedVolume//$tobereplaced/$replacewith}"
docker run --rm \
-e TAR_NAME="$svcName" \
-v "$selectedVolume":/"$svcName" \
-v "$backupFolder":/backup \
busybox sh -c 'tar -czf "/backup/${TAR_NAME}.tar.gz" /${TAR_NAME}'
}
function backupData() {
local datetime=$(date +"%Y%m%d-%H%M")
local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime
mkdir -p "$BACKUP_FOLDER"
volumes=$(docker volume ls -f "name=$SERVICE_FOLDER" --format "{{.Name}}" | grep -E "_pgdata|_redisdata|_uploads")
# Check if there are any matching volumes
if [ -z "$volumes" ]; then
echo "No volumes found starting with '$SERVICE_FOLDER'"
exit 1
fi
for vol in $volumes; do
echo "Backing Up $vol"
backupSingleVolume "$BACKUP_FOLDER" "$vol"
done
echo ""
echo "Backup completed successfully. Backup files are stored in $BACKUP_FOLDER"
echo ""
}
function askForAction() {
local DEFAULT_ACTION=$1
if [ -z "$DEFAULT_ACTION" ];
then
echo
echo "Select a Action you want to perform:"
echo " 1) Install"
echo " 2) Start"
echo " 3) Stop"
echo " 4) Restart"
echo " 5) Upgrade"
echo " 6) View Logs"
echo " 7) Backup Data"
echo " 8) Exit"
echo
read -p "Action [2]: " ACTION
until [[ -z "$ACTION" || "$ACTION" =~ ^[1-8]$ ]]; do
echo "$ACTION: invalid selection."
read -p "Action [2]: " ACTION
done
if [ -z "$ACTION" ];
then
ACTION=2
fi
echo
fi
if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ];
then
install
# askForAction
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ];
then
startServices
# askForAction
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ];
then
stopServices
# askForAction
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ];
then
restartServices
# askForAction
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ];
then
upgrade
# askForAction
elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ];
then
viewLogs "$@"
askForAction
elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ];
then
backupData
elif [ "$ACTION" == "8" ]
then
exit 0
else
echo "INVALID ACTION SUPPLIED"
fi
}
# if docker-compose is installed
if command -v docker-compose &> /dev/null
then
COMPOSE_CMD="docker-compose"
else
COMPOSE_CMD="docker compose"
fi
if [ "$CPU_ARCH" == "x86_64" ] || [ "$CPU_ARCH" == "amd64" ]; then
CPU_ARCH="amd64"
elif [ "$CPU_ARCH" == "aarch64" ] || [ "$CPU_ARCH" == "arm64" ]; then
CPU_ARCH="arm64"
fi
if [ -f "$DOCKER_ENV_PATH" ]; then
DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH")
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
PULL_POLICY=$(getEnvValue "PULL_POLICY" "$DOCKER_ENV_PATH")
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
if [ -z "$APP_RELEASE" ]; then
APP_RELEASE=stable
updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
fi
if [ -z "$PULL_POLICY" ]; then
PULL_POLICY=if_not_present
updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
fi
if [ -z "$CUSTOM_BUILD" ]; then
CUSTOM_BUILD=false
updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
fi
fi
print_header
askForAction "$@"