From 4e481509a2305d184ac0b798ca3c86bc7dbfa875 Mon Sep 17 00:00:00 2001 From: kbrianngeno Date: Fri, 13 Mar 2026 15:21:24 +0000 Subject: [PATCH] first commit --- .gitignore | 3 + docker-compose.yml | 308 +++++++++++++++++++ setup.sh | 714 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1025 insertions(+) create mode 100644 .gitignore create mode 100644 docker-compose.yml create mode 100755 setup.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51bb8d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +/data +/plane-app \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..470a163 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,308 @@ +x-db-env: &db-env + PGHOST: ${PGHOST:-plane-db} + PGDATABASE: ${PGDATABASE:-plane} + POSTGRES_USER: ${POSTGRES_USER:-plane} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane} + POSTGRES_DB: ${POSTGRES_DB:-plane} + POSTGRES_PORT: ${POSTGRES_PORT:-5432} + PGDATA: ${PGDATA:-/var/lib/postgresql/data} + +x-redis-env: &redis-env + REDIS_HOST: ${REDIS_HOST:-plane-redis} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_URL: ${REDIS_URL:-redis://plane-redis:6379/} + +x-minio-env: &minio-env + MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key} + MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key} + +x-aws-s3-env: &aws-s3-env + AWS_REGION: ${AWS_REGION:-} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-secret-key} + AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000} + AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} + +x-proxy-env: &proxy-env + APP_DOMAIN: ${APP_DOMAIN:-localhost} + FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} + CERT_EMAIL: ${CERT_EMAIL} + CERT_ACME_CA: ${CERT_ACME_CA} + CERT_ACME_DNS: ${CERT_ACME_DNS} + LISTEN_HTTP_PORT: ${LISTEN_HTTP_PORT:-80} + LISTEN_HTTPS_PORT: ${LISTEN_HTTPS_PORT:-443} + BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} + SITE_ADDRESS: ${SITE_ADDRESS:-:80} + +x-mq-env: &mq-env # RabbitMQ Settings + RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq} + RABBITMQ_PORT: ${RABBITMQ_PORT:-5672} + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane} + RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane} + RABBITMQ_VHOST: ${RABBITMQ_VHOST:-plane} + +x-live-env: &live-env + API_BASE_URL: ${API_BASE_URL:-http://api:8000} + LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW} + +x-app-env: &app-env + WEB_URL: ${WEB_URL:-http://localhost} + DEBUG: ${DEBUG:-0} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS} + GUNICORN_WORKERS: 1 + USE_MINIO: ${USE_MINIO:-1} + DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane} + SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} + AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane} + API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute} + MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0} + LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW} + +x-smtp-env: &smtp-env + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_PORT: ${EMAIL_PORT} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + EMAIL_USE_TLS: ${EMAIL_USE_TLS} + EMAIL_USE_SSL: ${EMAIL_USE_SSL} + EMAIL_FROM: ${EMAIL_FROM} + +services: + web: + image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-v1.2.2} + container_name: plane-web + deploy: + replicas: ${WEB_REPLICAS:-1} + restart_policy: + condition: any + depends_on: + - api + - worker + networks: + - nginx + + space: + image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-v1.2.2} + container_name: plane-space + deploy: + replicas: ${SPACE_REPLICAS:-1} + restart_policy: + condition: any + depends_on: + - api + - worker + - web + networks: + - nginx + + admin: + image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-v1.2.2} + container_name: plane-admin + deploy: + replicas: ${ADMIN_REPLICAS:-1} + restart_policy: + condition: any + depends_on: + - api + - web + networks: + - nginx + + live: + image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-v1.2.2} + container_name: plane-live + environment: + <<: [*live-env, *redis-env] + deploy: + replicas: ${LIVE_REPLICAS:-1} + restart_policy: + condition: any + depends_on: + - api + - web + networks: + - nginx + + api: + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.2.2} + container_name: plane-api + command: ./bin/docker-entrypoint-api.sh + deploy: + replicas: ${API_REPLICAS:-1} + restart_policy: + condition: any + volumes: + - ./data/logs_api:/code/plane/logs + environment: + <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env] #, *smtp-env] + depends_on: + # - plane-db + - plane-redis + - plane-mq + networks: + - nginx + + worker: + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.2.2} + container_name: plane-worker + command: ./bin/docker-entrypoint-worker.sh + deploy: + replicas: ${WORKER_REPLICAS:-1} + restart_policy: + condition: any + volumes: + - ./data/logs_worker:/code/plane/logs + environment: + <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env] #, *smtp-env] + depends_on: + - api + # - plane-db + - plane-redis + - plane-mq + networks: + - nginx + + beat-worker: + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.2.2} + container_name: plane-beat-worker + command: ./bin/docker-entrypoint-beat.sh + deploy: + replicas: ${BEAT_WORKER_REPLICAS:-1} + restart_policy: + condition: any + volumes: + - ./data/logs_beat-worker:/code/plane/logs + environment: + <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env] + depends_on: + - api + # - plane-db + - plane-redis + - plane-mq + networks: + - nginx + + migrator: + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.2.2} + container_name: plane-migrator + command: ./bin/docker-entrypoint-migrator.sh + deploy: + replicas: 1 + restart_policy: + condition: on-failure + volumes: + - ./data/logs_migrator:/code/plane/logs + environment: + <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env] + depends_on: + # - plane-db + - plane-redis + networks: + - nginx + + # Comment this if you already have a database running + # plane-db: + # image: postgres:15.7-alpine + # container_name: plane-db + # command: postgres -c 'max_connections=1000' + # deploy: + # replicas: 1 + # restart_policy: + # condition: any + # environment: + # <<: *db-env + # volumes: + # - ./data/pgdata:/var/lib/postgresql/data + # networks: + # - nginx + + plane-redis: + image: valkey/valkey:7.2.11-alpine + container_name: plane-redis + deploy: + replicas: 1 + restart_policy: + condition: any + volumes: + - ./data/redisdata:/data + networks: + - nginx + + plane-mq: + image: rabbitmq:3.13.6-management-alpine + container_name: plane-mq + deploy: + replicas: 1 + restart_policy: + condition: any + environment: + <<: *mq-env + volumes: + - ./data/rabbitmq_data:/var/lib/rabbitmq + networks: + - nginx + + # Comment this if you using any external s3 compatible storage + # plane-minio: + # image: minio/minio:latest + # container_name: plane-minio + # command: server /export --console-address ":9090" + # deploy: + # replicas: 1 + # restart_policy: + # condition: any + # environment: + # <<: *minio-env + # volumes: + # - ./data/uploads:/export + # networks: + # - nginx + + # Comment this if you already have a reverse proxy running + # proxy: + # image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-v1.2.2} + # container_name: plane-proxy + # deploy: + # replicas: 1 + # restart_policy: + # condition: any + # environment: + # <<: *proxy-env + # ports: + # - target: 80 + # published: ${LISTEN_HTTP_PORT:-80} + # protocol: tcp + # mode: host + # - target: 443 + # published: ${LISTEN_HTTPS_PORT:-443} + # protocol: tcp + # mode: host + # volumes: + # - ./data/proxy_config:/config + # - ./data/proxy_data:/data + # - ./data/caddy:/etc/caddy + # depends_on: + # - web + # - api + # - space + # - admin + # - live + # networks: + # - nginx + +volumes: + pgdata: + redisdata: + uploads: + logs_api: + logs_worker: + logs_beat-worker: + logs_migrator: + rabbitmq_data: + proxy_config: + proxy_data: + +networks: + nginx: + external: True diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..c26fba2 --- /dev/null +++ b/setup.sh @@ -0,0 +1,714 @@ +#!/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=artifacts.plane.so/makeplane +export PULL_POLICY=${PULL_POLICY:-if_not_present} +export GH_REPO=makeplane/plane +export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download" +export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deployments/cli/community" + +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 + +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 checkLatestRelease(){ + echo "Checking for the latest release..." >&2 + local latest_release=$(curl -sSL https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g') + if [ -z "$latest_release" ]; then + echo "Failed to check for the latest release. Exiting..." >&2 + exit 1 + fi + + echo $latest_release +} + +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 [ "$OS_NAME" == "Darwin" ]; then + value=$(echo "$value" | sed 's/|/\\|/g') + sed -i '' "s|^$key=.*|$key=$value|g" "$file" + else + value=$(echo "$value" | sed 's/\//\\\//g') + sed -i "s/^$key=.*/$key=$value/g" "$file" + fi + 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/$GH_REPO.git + git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1 + + cp "$PLANE_TEMP_CODE_DIR/deployments/cli/community/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 "" + + if [ "$APP_RELEASE" == "stable" ]; then + export APP_RELEASE=$(checkLatestRelease) + fi + + 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 + + RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)") + BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') + STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') + + if [ "$STATUS" -eq 200 ]; then + echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml + else + # Fallback to download from the raw github url + RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)") + BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') + STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') + + if [ "$STATUS" -eq 200 ]; then + echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml + else + echo "Failed to download docker-compose.yml. HTTP Status: $STATUS" + echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml" + mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml + exit 1 + fi + fi + + RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)") + BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') + STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') + + if [ "$STATUS" -eq 200 ]; then + echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env + else + # Fallback to download from the raw github url + RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)") + BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g') + STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://') + + if [ "$STATUS" -eq 200 ]; then + echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env + else + echo "Failed to download variables.env. HTTP Status: $STATUS" + echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env" + mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml + exit 1 + fi + fi + + 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") + + # Verify container exists + if [ -z "$api_container_id" ]; then + echo " Error: API container not found. Please check if services are running." + exit 1 + fi + + local idx2=0 + local api_ready=true # assume success, flip on timeout + local max_wait_time=300 # 5 minutes timeout + local start_time=$(date +%s) + + echo " Waiting for API Service to be ready..." + while ! docker exec "$api_container_id" python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/', timeout=3)" > /dev/null 2>&1; do + local current_time=$(date +%s) + local elapsed_time=$((current_time - start_time)) + + if [ $elapsed_time -gt $max_wait_time ]; then + echo "" + echo " API Service health check timed out after 5 minutes" + echo " Checking if API container is still running..." + if docker ps | grep -q "$SERVICE_FOLDER-api"; then + echo " API container is running but did not pass the health-check. Continuing without marking it ready." + api_ready=false + break + else + echo " API container is not running. Please check logs." + exit 1 + fi + fi + + local message=">> Waiting for API Service to Start (${elapsed_time}s)" + local dots=$(printf '%*s' $idx2 | tr ' ' '.') + echo -ne "\r$message$dots" + ((idx2++)) + sleep 1 + done + printf "\r\033[K" + if [ "$api_ready" = true ]; then + echo " API Service started successfully ✅" + else + echo " ⚠️ API Service did not respond to health-check – please verify manually." + fi + 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() { + local latest_release=$(checkLatestRelease) + + echo "" + echo "Current release: $APP_RELEASE" + + if [ "$latest_release" == "$APP_RELEASE" ]; then + echo "" + echo "You are already using the latest release" + exit 0 + fi + + echo "Latest release: $latest_release" + echo "" + + # Check for confirmation to upgrade + echo "Do you want to upgrade to the latest release ($latest_release)?" + read -p "Continue? [y/N]: " confirm + + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + echo "Exiting..." + exit 0 + fi + + export APP_RELEASE=$latest_release + + echo "Upgrading Plane to the latest release..." + echo "" + + 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 " 11) RabbitMQ" + echo " 0) Back to Main Menu" + echo + read -p "Service: " DOCKER_SERVICE_NAME + + until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 11 )); do + echo "Invalid selection. Please enter a number between 0 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";; + 11) viewSpecificLogs "plane-mq";; + 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";; + rabbitmq) viewSpecificLogs "plane-mq";; + *) echo "INVALID SERVICE NAME SUPPLIED";; + esac + else + echo "INVALID SERVICE NAME SUPPLIED" + fi +} +function backup_container_dir() { + local BACKUP_FOLDER=$1 + local CONTAINER_NAME=$2 + local CONTAINER_DATA_DIR=$3 + local SERVICE_FOLDER=$4 + + echo "Backing up $CONTAINER_NAME data..." + local CONTAINER_ID=$(/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH ps -q $CONTAINER_NAME") + if [ -z "$CONTAINER_ID" ]; then + echo "Error: $CONTAINER_NAME container not found. Make sure the services are running." + return 1 + fi + + # Create a temporary directory for the backup + mkdir -p "$BACKUP_FOLDER/$SERVICE_FOLDER" + + # Copy the data directory from the running container + echo "Copying $CONTAINER_NAME data directory..." + docker cp -q "$CONTAINER_ID:$CONTAINER_DATA_DIR/." "$BACKUP_FOLDER/$SERVICE_FOLDER/" + local cp_status=$? + + if [ $cp_status -ne 0 ]; then + echo "Error: Failed to copy $SERVICE_FOLDER data" + rm -rf $BACKUP_FOLDER/$SERVICE_FOLDER + return 1 + fi + + # Create tar.gz of the data + cd "$BACKUP_FOLDER" + tar -czf "${SERVICE_FOLDER}.tar.gz" "$SERVICE_FOLDER/" + local tar_status=$? + if [ $tar_status -eq 0 ]; then + rm -rf "$SERVICE_FOLDER/" + fi + cd - > /dev/null + + if [ $tar_status -ne 0 ]; then + echo "Error: Failed to create tar archive" + return 1 + fi + + echo "Successfully backed up $SERVICE_FOLDER data" +} + +function backupData() { + local datetime=$(date +"%Y%m%d-%H%M") + local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime + mkdir -p "$BACKUP_FOLDER" + + # Check if docker-compose.yml exists + if [ ! -f "$DOCKER_FILE_PATH" ]; then + echo "Error: docker-compose.yml not found at $DOCKER_FILE_PATH" + exit 1 + fi + + backup_container_dir "$BACKUP_FOLDER" "plane-db" "/var/lib/postgresql/data" "pgdata" || exit 1 + backup_container_dir "$BACKUP_FOLDER" "plane-minio" "/export" "uploads" || exit 1 + backup_container_dir "$BACKUP_FOLDER" "plane-mq" "/var/lib/rabbitmq" "rabbitmq_data" || exit 1 + backup_container_dir "$BACKUP_FOLDER" "plane-redis" "/data" "redisdata" || exit 1 + + 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=artifacts.plane.so/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 "$@"