From 854efbcff0399222f61cfd693d282c4ba38f687b Mon Sep 17 00:00:00 2001 From: Ahson Shaikh Date: Thu, 28 Aug 2025 18:49:17 +0500 Subject: [PATCH 1/2] Removed Twenty, Updated Plane --- plane/README.md | 44 +- plane/code/.env.example | 29 +- plane/code/README.md | 630 ------------------ plane/code/build.yml | 30 - plane/code/docker-compose.yml | 53 +- plane/code/images/download.png | Bin 22985 -> 0 bytes plane/code/images/migrate-error.png | Bin 14139 -> 0 bytes plane/code/images/restart.png | Bin 15664 -> 0 bytes plane/code/images/started.png | Bin 21489 -> 0 bytes plane/code/images/stopped.png | Bin 16638 -> 0 bytes plane/code/images/upgrade.png | Bin 55240 -> 0 bytes plane/code/migration-0.13-0.14.sh | 118 ---- plane/code/restore-airgapped.sh | 144 ---- plane/code/restore.sh | 123 ---- plane/code/swarm.sh | 612 ----------------- plane/{code/install.sh => setup.sh} | 16 +- plane/update.js | 19 +- twenty/README.md | 4 - twenty/code/.env.example | 18 - twenty/code/Makefile | 27 - twenty/code/docker-compose.yml | 126 ---- .../datasources/clickhouse-datasource.yaml | 15 - twenty/code/k8s/README.md | 116 ---- twenty/code/k8s/manifests/deployment-db.yaml | 58 -- .../code/k8s/manifests/deployment-redis.yaml | 45 -- .../code/k8s/manifests/deployment-server.yaml | 76 --- .../code/k8s/manifests/deployment-worker.yaml | 57 -- twenty/code/k8s/manifests/ingress.yaml | 24 - twenty/code/k8s/manifests/pv-db.yaml | 11 - twenty/code/k8s/manifests/pv-docker-data.yaml | 11 - twenty/code/k8s/manifests/pv-server.yaml | 12 - twenty/code/k8s/manifests/pvc-db.yaml | 13 - .../code/k8s/manifests/pvc-docker-data.yaml | 13 - twenty/code/k8s/manifests/pvc-server.yaml | 13 - twenty/code/k8s/manifests/service-db.yaml | 18 - twenty/code/k8s/manifests/service-redis.yaml | 18 - twenty/code/k8s/manifests/service-server.yaml | 19 - twenty/code/k8s/terraform/.terraform-docs.yml | 48 -- twenty/code/k8s/terraform/README.md | 73 -- twenty/code/k8s/terraform/deployment-db.tf | 87 --- twenty/code/k8s/terraform/deployment-redis.tf | 60 -- .../code/k8s/terraform/deployment-server.tf | 138 ---- .../code/k8s/terraform/deployment-worker.tf | 99 --- twenty/code/k8s/terraform/ingress.tf | 30 - twenty/code/k8s/terraform/main.tf | 23 - twenty/code/k8s/terraform/namespace.tf | 9 - twenty/code/k8s/terraform/pv-db.tf | 19 - twenty/code/k8s/terraform/pv-docker-data.tf | 19 - twenty/code/k8s/terraform/pv-server.tf | 19 - twenty/code/k8s/terraform/pvc-db.tf | 15 - twenty/code/k8s/terraform/pvc-docker-data.tf | 15 - twenty/code/k8s/terraform/pvc-server.tf | 15 - twenty/code/k8s/terraform/secret.tf | 28 - twenty/code/k8s/terraform/service-db.tf | 18 - twenty/code/k8s/terraform/service-redis.tf | 18 - twenty/code/k8s/terraform/service-server.tf | 19 - twenty/code/k8s/terraform/variables.tf | 136 ---- .../otel-collector/otel-collector-config.yaml | 24 - twenty/code/podman/README.md | 48 -- .../code/podman/install-systemd-user-service | 11 - .../manual-steps-to-deploy-twenty-on-podman | 71 -- twenty/code/podman/podman-compose.yml | 56 -- twenty/code/podman/twentycrm.service | 15 - twenty/code/scripts/1-click.sh | 22 - twenty/code/scripts/install.sh | 167 ----- twenty/code/twenty-postgres-spilo/Dockerfile | 68 -- twenty/code/twenty-website/Dockerfile | 43 -- twenty/code/twenty/Dockerfile | 82 --- twenty/code/twenty/entrypoint.sh | 54 -- twenty/update.js | 25 - utils.js | 14 + 71 files changed, 128 insertions(+), 3972 deletions(-) delete mode 100644 plane/code/README.md delete mode 100644 plane/code/build.yml delete mode 100644 plane/code/images/download.png delete mode 100644 plane/code/images/migrate-error.png delete mode 100644 plane/code/images/restart.png delete mode 100644 plane/code/images/started.png delete mode 100644 plane/code/images/stopped.png delete mode 100644 plane/code/images/upgrade.png delete mode 100755 plane/code/migration-0.13-0.14.sh delete mode 100755 plane/code/restore-airgapped.sh delete mode 100755 plane/code/restore.sh delete mode 100755 plane/code/swarm.sh rename plane/{code/install.sh => setup.sh} (95%) delete mode 100644 twenty/README.md delete mode 100644 twenty/code/.env.example delete mode 100644 twenty/code/Makefile delete mode 100644 twenty/code/docker-compose.yml delete mode 100644 twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml delete mode 100644 twenty/code/k8s/README.md delete mode 100644 twenty/code/k8s/manifests/deployment-db.yaml delete mode 100644 twenty/code/k8s/manifests/deployment-redis.yaml delete mode 100644 twenty/code/k8s/manifests/deployment-server.yaml delete mode 100644 twenty/code/k8s/manifests/deployment-worker.yaml delete mode 100644 twenty/code/k8s/manifests/ingress.yaml delete mode 100644 twenty/code/k8s/manifests/pv-db.yaml delete mode 100644 twenty/code/k8s/manifests/pv-docker-data.yaml delete mode 100644 twenty/code/k8s/manifests/pv-server.yaml delete mode 100644 twenty/code/k8s/manifests/pvc-db.yaml delete mode 100644 twenty/code/k8s/manifests/pvc-docker-data.yaml delete mode 100644 twenty/code/k8s/manifests/pvc-server.yaml delete mode 100644 twenty/code/k8s/manifests/service-db.yaml delete mode 100644 twenty/code/k8s/manifests/service-redis.yaml delete mode 100644 twenty/code/k8s/manifests/service-server.yaml delete mode 100644 twenty/code/k8s/terraform/.terraform-docs.yml delete mode 100644 twenty/code/k8s/terraform/README.md delete mode 100644 twenty/code/k8s/terraform/deployment-db.tf delete mode 100644 twenty/code/k8s/terraform/deployment-redis.tf delete mode 100644 twenty/code/k8s/terraform/deployment-server.tf delete mode 100644 twenty/code/k8s/terraform/deployment-worker.tf delete mode 100644 twenty/code/k8s/terraform/ingress.tf delete mode 100644 twenty/code/k8s/terraform/main.tf delete mode 100644 twenty/code/k8s/terraform/namespace.tf delete mode 100644 twenty/code/k8s/terraform/pv-db.tf delete mode 100644 twenty/code/k8s/terraform/pv-docker-data.tf delete mode 100644 twenty/code/k8s/terraform/pv-server.tf delete mode 100644 twenty/code/k8s/terraform/pvc-db.tf delete mode 100644 twenty/code/k8s/terraform/pvc-docker-data.tf delete mode 100644 twenty/code/k8s/terraform/pvc-server.tf delete mode 100644 twenty/code/k8s/terraform/secret.tf delete mode 100644 twenty/code/k8s/terraform/service-db.tf delete mode 100644 twenty/code/k8s/terraform/service-redis.tf delete mode 100644 twenty/code/k8s/terraform/service-server.tf delete mode 100644 twenty/code/k8s/terraform/variables.tf delete mode 100644 twenty/code/otel-collector/otel-collector-config.yaml delete mode 100644 twenty/code/podman/README.md delete mode 100755 twenty/code/podman/install-systemd-user-service delete mode 100644 twenty/code/podman/manual-steps-to-deploy-twenty-on-podman delete mode 100644 twenty/code/podman/podman-compose.yml delete mode 100644 twenty/code/podman/twentycrm.service delete mode 100644 twenty/code/scripts/1-click.sh delete mode 100755 twenty/code/scripts/install.sh delete mode 100644 twenty/code/twenty-postgres-spilo/Dockerfile delete mode 100644 twenty/code/twenty-website/Dockerfile delete mode 100644 twenty/code/twenty/Dockerfile delete mode 100755 twenty/code/twenty/entrypoint.sh delete mode 100644 twenty/update.js diff --git a/plane/README.md b/plane/README.md index 86611bc..ce423fb 100644 --- a/plane/README.md +++ b/plane/README.md @@ -1,4 +1,42 @@ -# Plane +# Plane - Project Management Script -- copied from https://github.com/makeplane/plane -- removed `ports` +This directory contains the Plane project management tool deployment for Easypanel. + +## Update Process + +To update Plane to the latest version, follow these steps in order: + +### Step 1: Run the Setup Script for Upgrade + +First, run the `setup.sh` script and choose the upgrade option: + +``` +./setup.sh +``` + +When prompted, select option **5** for "Upgrade". This will: + +* Check for the latest available release +* Download the latest stable version + +### Step 2: Run the Update Script + +After the setup script completes successfully, run the `update.js` script: + +``` +node update.js +``` + +This script will: + +* Removes the existing code directory and changes the name for the newly fetched directory to code.  +* Rename `plane.env` to `.env.example` +* Rename `docker-compose.yaml` to `docker-compose.yml` +* Remove container names and ports from docker-compose.yml +* Update environment variables to use Easypanel's `PRIMARY_DOMAIN` variable + +## Important Notes + +* **Always run setup.sh first** - This ensures you get the latest official Plane release +* **Then run update.js** - This applies Easypanel-specific customizations +* The update process will preserve your existing data and configuration \ No newline at end of file diff --git a/plane/code/.env.example b/plane/code/.env.example index 78031a4..fd92cb4 100644 --- a/plane/code/.env.example +++ b/plane/code/.env.example @@ -1,5 +1,6 @@ -APP_DOMAIN=localhost -APP_RELEASE=stable +APP_DOMAIN=$(PRIMARY_DOMAIN) +APP_RELEASE=v0.28.0 +SSL=false WEB_REPLICAS=1 SPACE_REPLICAS=1 @@ -9,10 +10,12 @@ WORKER_REPLICAS=1 BEAT_WORKER_REPLICAS=1 LIVE_REPLICAS=1 -NGINX_PORT=80 -WEB_URL=http://${APP_DOMAIN} +LISTEN_HTTP_PORT=80 +LISTEN_HTTPS_PORT=443 + +WEB_URL=https://$(PRIMARY_DOMAIN) DEBUG=0 -CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN} +CORS_ALLOWED_ORIGINS=https://$(PRIMARY_DOMAIN) API_BASE_URL=http://api:8000 #DB SETTINGS @@ -38,6 +41,19 @@ RABBITMQ_PASSWORD=plane RABBITMQ_VHOST=plane AMQP_URL= +# If SSL Cert to be generated, set CERT_EMAIl="email " +CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory +TRUSTED_PROXIES=0.0.0.0/0 +SITE_ADDRESS=:80 +CERT_EMAIL= + + + +# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL +# CERT_ACME_DNS="acme_dns " +CERT_ACME_DNS= + + # Secret Key SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5 @@ -61,3 +77,6 @@ MINIO_ENDPOINT_SSL=0 # API key rate limit API_KEY_RATE_LIMIT=60/minute +DOCKERHUB_USER=artifacts.plane.so/makeplane +PULL_POLICY=if_not_present +CUSTOM_BUILD=false diff --git a/plane/code/README.md b/plane/code/README.md deleted file mode 100644 index ba7af0e..0000000 --- a/plane/code/README.md +++ /dev/null @@ -1,630 +0,0 @@ -# Self Hosting - -In this guide, we will walk you through the process of setting up a self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization. - -We will cover two main options for setting up your self-hosted environment: using a cloud server or using your desktop. For the cloud server, we will use an AWS EC2 instance. For the desktop, we will use Docker to create a local environment. - -Let's get started! - -## Setting up Docker Environment - -
- Option 1 - Using Cloud Server -

Best way to start is to create EC2 machine on AWS. It must have minimum of 2vCPU and 4GB RAM.

-

Run the below command to install docker engine.

- -`curl -fsSL https://get.docker.com | sh -` - -
- ---- - -
- Option 2 - Using Desktop - -#### For Mac - -
    -
  1. Download Docker Desktop for Mac from the Docker Hub.
  2. -
  3. Double-click the downloaded `.dmg` file and drag the Docker app icon to the Applications folder.
  4. -
  5. Open Docker Desktop from the Applications folder. You might be asked to provide your system password to install additional software.
  6. -
- -#### For Windows: - -
    -
  1. Download Docker Desktop for Windows from the Docker Hub.
  2. -
  3. Run the installer and follow the instructions. You might be asked to enable Hyper-V and "Containers" Windows features.
  4. -
  5. Open Docker Desktop. You might be asked to log out and log back in, or restart your machine, for changes to take effect.
  6. -
- -After installation, you can verify the installation by opening a terminal (Command Prompt on Windows, Terminal app on Mac) and running the command `docker --version`. This should display the installed version of Docker. - -
- ---- - -## Installing Plane - -Installing plane is a very easy and minimal step process. - -### Prerequisite - -- Docker installed and running -- OS with bash scripting enabled (Ubuntu, Linux AMI, macos). Windows systems need to have [gitbash](https://git-scm.com/download/win) -- User context used must have access to docker services. In most cases, use sudo su to switch as root user -- Use the terminal (or gitbash) window to run all the future steps - -### Downloading Latest Release - -``` -mkdir plane-selfhost - -cd plane-selfhost -``` - -#### For *Docker Compose* based setup - -``` -curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh - -chmod +x setup.sh -``` - -#### For *Docker Swarm* based setup - -``` -curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/swarm.sh - -chmod +x setup.sh -``` - ---- - -### Proceed with setup - -Above steps will set you ready to install and start plane services. - -Lets get started by running the `./setup.sh` command. - -This will prompt you with the below options. - -#### Docker Compose -```bash -Select an Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 1 -``` - -For the 1st time setup, type "1" as action input. - -This will create a folder `plane-app` and will download 2 files inside that - -- `docker-compose.yaml` -- `plane.env` - -Again the `options [1-8]` will be popped up, and this time hit `8` to exit. - -#### Docker Swarm - -```bash -Select an Action you want to perform: - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 1 -``` - -For the 1st time setup, type "1" as action input. - -This will create a create a folder `plane-app` and will download 2 files inside that - -- `docker-compose.yaml` -- `plane.env` - -Again the `options [1-7]` will be popped up, and this time hit `7` to exit. - ---- - -### Continue with setup - Environment Settings - -Before proceeding, we suggest used to review `.env` file and set the values. -Below are the most import keys you must refer to. _You can use any text editor to edit this file_. - -> `NGINX_PORT` - This is default set to `80`. Make sure the port you choose to use is not preoccupied. (e.g `NGINX_PORT=8080`) - -> `WEB_URL` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`) - -> `CORS_ALLOWED_ORIGINS` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`) - -There are many other settings you can play with, but we suggest you configure `EMAIL SETTINGS` as it will enable you to invite your teammates onto the platform. - ---- - -### Continue with setup - Start Server (Docker Compose) - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to start the sevices - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 2 -``` - -Expect something like this. -![Downloading docker images](images/download.png) - -Be patient as it might take sometime based on download speed and system configuration. If all goes well, you must see something like this - -![Downloading completed](images/started.png) - -This is the confirmation that all images were downloaded and the services are up & running. - -You have successfully self hosted `Plane` instance. Access the application by going to IP or domain you have configured it (e.g `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`) - ---- - -### Stopping the Server / Remove Stack - -In case you want to make changes to `plane.env` variables, we suggest you to stop the services before doing that. - -#### Docker Compose - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `3` to stop the sevices - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 3 -``` - -If all goes well, you must see something like this - -![Stop Services](images/stopped.png) - -#### Docker Swarm - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `2` to stop the sevices - -```bash -Select an Action you want to perform: - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 2 -``` - -If all goes well, you will see the confirmation from docker cli - ---- - -### Restarting the Server / Redeploy Stack - -In case you want to make changes to `plane.env` variables, without stopping the server or you noticed some abnormalies in services, you can restart the services with `RESTART` / `REDEPLOY` option. - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `4` to restart the sevices - -#### Docker Compose -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 4 -``` - -If all goes well, you must see something like this - -![Restart Services](images/restart.png) - -#### Docker Swarm - -```bash - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 4 -``` - -If all goes well, you will see the confirmation from docker cli - ---- - -### Upgrading Plane Version - -It is always advised to keep Plane up to date with the latest release. - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release. - -#### Docker Compose - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 5 -``` - -By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`. - -You must expect the below message - -![Alt text](images/upgrade.png) - -Once done, choose `8` to exit from prompt. - -> It is very important for you to validate the `plane.env` for the new changes. - -Once done with making changes in `plane.env` file, jump on to `Start Server` - -#### Docker Swarm - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release. - -```bash - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 5 -``` - -By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `plane.env`. - -Once done, choose `7` to exit from prompt. - -> It is very important for you to validate the `plane.env` for the new changes. - -Once done with making changes in `plane.env` file, jump on to `Redeploy Stack` - ---- - -### View Logs - -There would a time when you might want to check what is happening inside the API, Worker or any other container. - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. - -This time select `6` to view logs. - -#### Docker Compose - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 6 -``` - -#### Docker Swarm - - -```bash - 1) Deploy Stack - 2) Remove Stack - 3) View Stack Status - 4) Redeploy Stack - 5) Upgrade - 6) View Logs - 7) Exit - -Action [3]: 6 -``` - -#### Service Menu Options for Logs -This will further open sub-menu with list of services -```bash -Select a Service you want to view the logs for: - 1) Web - 2) Space - 3) API - 4) Worker - 5) Beat-Worker - 6) Migrator - 7) Proxy - 8) Redis - 9) Postgres - 10) Minio - 11) RabbitMQ - 0) Back to Main Menu - -Service: 3 -``` - -Select any of the service to view the logs e.g. `3`. Expect something similar to this -```bash -api-1 | Waiting for database... -api-1 | Database available! -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | Waiting for database migrations to complete... -api-1 | No migrations Pending. Starting processes ... -api-1 | Instance registered -api-1 | ENABLE_SIGNUP loaded with value from environment variable. -api-1 | ENABLE_EMAIL_PASSWORD loaded with value from environment variable. -api-1 | ENABLE_MAGIC_LINK_LOGIN loaded with value from environment variable. -api-1 | GOOGLE_CLIENT_ID loaded with value from environment variable. -api-1 | GITHUB_CLIENT_ID loaded with value from environment variable. -api-1 | GITHUB_CLIENT_SECRET loaded with value from environment variable. -api-1 | EMAIL_HOST loaded with value from environment variable. -api-1 | EMAIL_HOST_USER loaded with value from environment variable. -api-1 | EMAIL_HOST_PASSWORD loaded with value from environment variable. -api-1 | EMAIL_PORT loaded with value from environment variable. -api-1 | EMAIL_FROM loaded with value from environment variable. -api-1 | EMAIL_USE_TLS loaded with value from environment variable. -api-1 | EMAIL_USE_SSL loaded with value from environment variable. -api-1 | OPENAI_API_KEY loaded with value from environment variable. -api-1 | GPT_ENGINE loaded with value from environment variable. -api-1 | UNSPLASH_ACCESS_KEY loaded with value from environment variable. -api-1 | Checking bucket... -api-1 | Bucket 'uploads' does not exist. Creating bucket... -api-1 | Bucket 'uploads' created successfully. -api-1 | Public read access policy set for bucket 'uploads'. -api-1 | Cache Cleared -api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Starting gunicorn 21.2.0 -api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) -api-1 | [2024-05-02 03:56:01 +0000] [1] [INFO] Using worker: uvicorn.workers.UvicornWorker -api-1 | [2024-05-02 03:56:01 +0000] [25] [INFO] Booting worker with pid: 25 -api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Started server process [25] -api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Waiting for application startup. -api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] ASGI 'lifespan' protocol appears unsupported. -api-1 | [2024-05-02 03:56:03 +0000] [25] [INFO] Application startup complete. - -``` - -To exit this, use `CTRL+C` and then you will land on to the main-menu with the list of actions. - -Similarly, you can view the logs of other services. - ---- - -### Backup Data (Docker Compose) - -There would a time when you might want to backup your data from docker volumes to external storage like S3 or drives. - -Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `7` to Backup the data. - -```bash -Select a Action you want to perform: - 1) Install (x86_64) - 2) Start - 3) Stop - 4) Restart - 5) Upgrade - 6) View Logs - 7) Backup Data - 8) Exit - -Action [2]: 7 -``` - -In response, you can find the backup folder - -```bash -Backing Up plane-app_pgdata -Backing Up plane-app_redisdata -Backing Up plane-app_uploads - -Backup completed successfully. Backup files are stored in /....../plane-app/backup/20240502-1120 -``` - ---- - -### Restore Data (Docker Compose) - -When you want to restore the previously backed-up data, follow the instructions below. - -1. Make sure that Plane-CE is installed, started, and then stopped. This ensures that the Docker volumes are created. - -1. Download the restore script using the command below. We suggest downloading it in the same folder as `setup.sh`. - - ```bash - curl -fsSL -o restore.sh https://github.com/makeplane/plane/releases/latest/download/restore.sh - chmod +x restore.sh - ``` - -1. Execute the command below to restore your data. - - ```bash - ./restore.sh - ``` - - As an example, for a backup folder `/opt/plane-selfhost/plane-app/backup/20240722-0914`, expect the response below: - - ```bash - -------------------------------------------- - ____ _ ///////// - | _ \| | __ _ _ __ ___ ///////// - | |_) | |/ _` | '_ \ / _ \ ///// ///// - | __/| | (_| | | | | __/ ///// ///// - |_| |_|\__,_|_| |_|\___| //// - //// - -------------------------------------------- - Project management tool from the future - -------------------------------------------- - Found /opt/plane-selfhost/plane-app/backup/20240722-0914/pgdata.tar.gz - .....Restoring plane-app_pgdata - .....Successfully restored volume plane-app_pgdata from pgdata.tar.gz - - Found /opt/plane-selfhost/plane-app/backup/20240722-0914/redisdata.tar.gz - .....Restoring plane-app_redisdata - .....Successfully restored volume plane-app_redisdata from redisdata.tar.gz - - Found /opt/plane-selfhost/plane-app/backup/20240722-0914/uploads.tar.gz - .....Restoring plane-app_uploads - .....Successfully restored volume plane-app_uploads from uploads.tar.gz - - - Restore completed successfully. - ``` - -1. Start the Plane instance using `./setup.sh start`. - ---- - -### Restore for Commercial Air-Gapped (Docker Compose) - -When you want to restore the previously backed-up data on Plane Commercial Air-Gapped version, follow the instructions below. - -1. Download the restore script using the command below - - ```bash - curl -fsSL -o restore-airgapped.sh https://github.com/makeplane/plane/releases/latest/download/restore-airgapped.sh - chmod +x restore-airgapped.sh - ``` - -1. Copy the backup folder and the `restore-airgapped.sh` to `Commercial Airgapped Edition` server - -1. Make sure that Plane Commercial (Airgapped) is extracted and ready to get started. In case it is running, you would need to stop that. - -1. Execute the command below to restore your data. - - ```bash - ./restore-airgapped.sh - ``` - -1. After restoration, you are ready to start Plane Commercial (Airgapped) will all your previously saved data. - ---- - -
-

Upgrading from v0.13.2 to v0.14.x

- -This is one time activity for users who are upgrading from v0.13.2 to v0.14.0 - -As there has been significant changes to Self Hosting process, this step mainly covers the data migration from current (v0.13.2) docker volumes from newly created volumes - -> Before we begin with migration, make sure your v0.14.0 was started and then stopped. This is required to know the newly created docker volume names. - -Begin with downloading the migration script using below command - -``` - -curl -fsSL -o migrate.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/migration-0.13-0.14.sh - -chmod +x migrate.sh - -``` - -Now run the `./migrate.sh` command and expect the instructions as below - -``` -****************************************************************** - -This script is solely for the migration purpose only. -This is a 1 time migration of volume data from v0.13.2 => v0.14.x - -Assumption: -1. Postgres data volume name ends with _pgdata -2. Minio data volume name ends with _uploads -3. Redis data volume name ends with _redisdata - -Any changes to this script can break the migration. - -Before you proceed, make sure you run the below command -to know the docker volumes - -docker volume ls -q | grep -i "_pgdata" -docker volume ls -q | grep -i "_uploads" -docker volume ls -q | grep -i "_redisdata" - -******************************************************* - -Given below list of REDIS volumes, identify the prefix of source and destination volumes leaving "_redisdata" ---------------------- -plane-app_redisdata -v0132_redisdata - -Provide the Source Volume Prefix : -``` - -**Open another terminal window**, and run the mentioned 3 command. This may be different for users who have changed the volume names in their previous setup (v0.13.2) - -For every command you must see 2 records something like shown in above example of `redisdata` - -To move forward, you would need PREFIX of old setup and new setup. As per above example, `v0132` is the prefix of v0.13.2 and `plane-app` is the prefix of v0.14.0 setup - -**Back to original terminal window**, _Provide the Source Volume Prefix_ and hit ENTER. - -Now you will be prompted to _Provide Destination Volume Prefix_. Provide the value and hit ENTER - -``` -Provide the Source Volume Prefix : v0132 -Provide the Destination Volume Prefix : plane-app -``` - -In case the suffixes are wrong or the mentioned volumes are not found, you will receive the error shown below. The image below displays an error for source volumes. - -![Migrate Error](images/migrate-error.png) - -In case of successful migration, it will be a silent exit without error. - -Now its time to restart v0.14.0 setup. -
\ No newline at end of file diff --git a/plane/code/build.yml b/plane/code/build.yml deleted file mode 100644 index b65d297..0000000 --- a/plane/code/build.yml +++ /dev/null @@ -1,30 +0,0 @@ -services: - web: - image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest} - build: - context: . - dockerfile: ./web/Dockerfile.web - - space: - image: ${DOCKERHUB_USER:-local}/plane-space:${APP_RELEASE:-latest} - build: - context: ./ - dockerfile: ./space/Dockerfile.space - - admin: - image: ${DOCKERHUB_USER:-local}/plane-admin:${APP_RELEASE:-latest} - build: - context: ./ - dockerfile: ./admin/Dockerfile.admin - - api: - image: ${DOCKERHUB_USER:-local}/plane-backend:${APP_RELEASE:-latest} - build: - context: ./apiserver - dockerfile: ./Dockerfile.api - - proxy: - image: ${DOCKERHUB_USER:-local}/plane-proxy:${APP_RELEASE:-latest} - build: - context: ./nginx - dockerfile: ./Dockerfile diff --git a/plane/code/docker-compose.yml b/plane/code/docker-compose.yml index 65fb84e..284b80e 100644 --- a/plane/code/docker-compose.yml +++ b/plane/code/docker-compose.yml @@ -24,9 +24,16 @@ x-aws-s3-env: &aws-s3-env AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} x-proxy-env: &proxy-env - NGINX_PORT: ${NGINX_PORT:-80} - BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} + SSL: ${SSL:-false} + 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: # RabbitMQ Settings @@ -55,8 +62,7 @@ x-app-env: &app-env services: web: - image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-stable} - command: node web/server.js web + image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-v0.28.0} deploy: replicas: ${WEB_REPLICAS:-1} restart_policy: @@ -66,8 +72,7 @@ services: - worker space: - image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-stable} - command: node space/server.js space + image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-v0.28.0} deploy: replicas: ${SPACE_REPLICAS:-1} restart_policy: @@ -78,8 +83,7 @@ services: - web admin: - image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-stable} - command: node admin/server.js admin + image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-v0.28.0} deploy: replicas: ${ADMIN_REPLICAS:-1} restart_policy: @@ -89,8 +93,7 @@ services: - web live: - image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-stable} - command: node live/dist/server.js live + image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-v0.28.0} environment: <<: [ *live-env ] deploy: @@ -102,7 +105,7 @@ services: - web api: - image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable} + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v0.28.0} command: ./bin/docker-entrypoint-api.sh deploy: replicas: ${API_REPLICAS:-1} @@ -118,7 +121,7 @@ services: - plane-mq worker: - image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable} + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v0.28.0} command: ./bin/docker-entrypoint-worker.sh deploy: replicas: ${WORKER_REPLICAS:-1} @@ -135,7 +138,7 @@ services: - plane-mq beat-worker: - image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable} + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v0.28.0} command: ./bin/docker-entrypoint-beat.sh deploy: replicas: ${BEAT_WORKER_REPLICAS:-1} @@ -152,7 +155,7 @@ services: - plane-mq migrator: - image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable} + image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v0.28.0} command: ./bin/docker-entrypoint-migrator.sh deploy: replicas: 1 @@ -214,17 +217,31 @@ services: # Comment this if you already have a reverse proxy running proxy: - image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-stable} - environment: - <<: *proxy-env + image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-v0.28.0} + command: + [ + "caddy", + "run", + "--config", + "/etc/caddy/Caddyfile", + "--adapter", + "caddyfile" + ] deploy: replicas: 1 restart_policy: condition: on-failure + environment: + <<: *proxy-env + volumes: + - proxy_config:/config + - proxy_data:/data depends_on: - web - api - space + - admin + - live volumes: pgdata: @@ -235,3 +252,5 @@ volumes: logs_beat-worker: logs_migrator: rabbitmq_data: + proxy_config: + proxy_data: diff --git a/plane/code/images/download.png b/plane/code/images/download.png deleted file mode 100644 index bb0d1183e68a8d455ce7959b6689bb97da679ab0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22985 zcmZVm1z6q8);2u!i`|a!6 zBs}SD10e_K?6+1ydK-DZnMHQq( zMTry~?MyAKO+Y}XzPWsrmjN?C8YFD~NJ`G z!?juDP}I2$QQ-#tW~ox9I56;VqcU(RiA=6d&VM(V_aZ%$8vgZq?=Wyn^;^(~MRe zsjU8kM))E+Q3eQ)n@9&|$YBP!=>6R4E5r+wVW@H(}I7X$=6J`@BR*rEVmu^h0!XTj)l!2j-pWW8S~tSl-e1#Fd#98FAYoy_f= z>#BRlfvy%TR5YA5WIyv5+1W4{7~2_|Fu2>;zc&HlbLRneZA_dEh}>u%^v@;ER{SIyvI<0^c8(@Q91KhhOe6wuL_|be`uN@5cKZ4Uh6Coy++ zw&!7FbaQiKaARe#b2MXQ=H}*RWMW}tVW9`Epm*}HbvAIPw{;@@PbYu-5i@ZzaQ7M*n{PGfoqCi~rrp*6F{G1w0_*`x!=N1}4UT`v#iwy&vUK zuy8lA))cd_0mK8`Lx7o`i|?Q7|DQAeyW{_9sqw#-?Cc!>x8?si^Z(yc)yc$B)XoOD zrL(~QX6C<*|L@8FHsoV`fAaq$iT{ZCpQC`B1>pD?|D7`dIJC<}SP&2)5GgTX6?f3H zFVHDy0}DUuzd=G^B()fS&G8R_j_hS5ii8zbR1_kE%Oim!;Zp#GB_ZhniHBs?2NU*? zhMG-yKtf0VHV)Q{g6SHN*(DdlzPh?PJ3GtDyISvcem~({QnspU=2>R9S=KhB+GMTC zS<3_#4MN%t^Ur2WA0iJ=hqDUj?}7K@D4-k~4-)ziSfCRA$nTpW(r&1z9Y2yPqW@i1 z)6;wZZ#UgATm(QPyf;(_NbJ9@B>X>ObVC2ziA47$Oij)0*Q_1NOIhcPUK2MyZCkH(%wK_N1cEsfOgdK976JSKvY992^Tu zsVCK4Sx_Hw$gbPgZF%gsKzyE#iW^KPaAfv^Mw~Z&?vV)|a5=1DBw{w}2z?(6IQU*) zpY1$vrX}kWt)}xJ`5$)^8f;cMa0I;Fo7nJFN)!S%+;=|Wu$sc#thIbwX|$5O-}DnC zCnH0$Pv>=Gpx3M;ljIiY`6~f27O*6E47Q7zLYdIB3dMY=m&b$bPzzVNSS;qiyu7>; z$vA?!X1mQAEIKBTU%!5xs&HrXNmlZWa`0`=+(Io{d69F zbXwRv&Lw<)Pd6547U#Z~zXnQuU!Lk>xFQVb^n=^7xSb>n_Z?dg?^f;ZleY2y&Loi_ z5mG}>yf#ru*zdcu)je{C_EVN#NvT|skP`Fhyep(ttE$$}{qa;$!M8uG>%fR<(#Lmb z&yw|gb8C%;P@)T1Oh(HU87N32wUo*S8ii;ASgT`p+bee;}! z(2uyI8BA(Kqg3(GzMUIB_g^e(y6pp))e1f#xBa;qD-{hxn8^pvVfI(OkYIyq6C}--tD_1ajc@t~ z)OtT&eckd;J8LCI!1uJTt_#64P8TaSkXr)UZ?Yx+`v_UYG7~tF9U)7($q6 zyz!UiMyq6w59h3Z^&xVcKRu!hmsU(}oYTS2X`?uP3dXQplz!&`%wn(m-`gdD+w4r6 zkXi8X;07BJIHAnr72KMwjwamBp^p@emh}oa6 z*b?yjhlhp0l0rcp@4@DFEMU9oeE!Yr!y#b$uN-gyIbhgCA*b&F8e=gFZdo8YDU84~ z{G#UV&-G-f*<{vLuFu7mu*XOEZ&cI2cQWoY+N)*zj&pW*jSEHOLI;#~erEEHIdzUs zq%e!(C}|LfpbH{>4rND$SLDAMCcO&sp5nJ!pzGVl{b5$@^L*Ffa-zm9`N{7iJY^}m zG*f}D`!=Kx#{U@+0cf&@;x8|{z3hDhU04doUjgO=`TkNZ?p&fpa+x@F+a!lUl-tAK zNS9oK^{*VNb$=AN^-G87dkcTh-KqXuZ#rS@jSi35({4nDrPjNK4qx~`NW*vk6zOn??0DAEI5q5 zVq*6LGrO>;6b~=5$bwKZX(tPnG8#t(>>A@U-+$THN|*ot_92W-m+kvhY78}qD@c*q zdADM9188NgLYr2tIG2-!5tAu;t%k3cs@)ouAJgreFCVWD#%76R=b|*zMselTEL>za zSpKe1IUypN6lw35+rqT@9N}2HNDu5RXePuyB#|2(asLvy zt=x@9D|Nf9kp#+evU4?NCR#8ty2nfon>1rJ-4^>?y7LJ-*NVfref8&Xm8imz62_Kr zwB~s1@&+2*L$yZgjEb3ng>TpYz9Fpm?TupI^h_kbPgNr&$nHt1pq zGW-BzOwNheA;?toyYtzWM!hP7h9sFX{piN@rZ(t-?SbgCV1KmgN*G*M@$^sodgzt_qO?KNV{P`SD`+94-kZw z0E1A}dx7mB**X%QV(+-^Ye&JymnHPMLGA(Z_s`Tk8yXx`vb411XiQ-=Bulx8Y^B*5 zic1!i+Im*ZJ+8IGSer)o;%oTaEE&Zi z?{At9WXsv;HlCNm!&6{MI_;0=DiunPoIZ7Y>f6<6K)rToF#I0sK~?fRudYWQ_Os^g zcl)_#)X{CZk$aK?W{q?L~AD*W5n{$==x97mMEVReP?x(@O zlfL-ZF%Li@+WLej1G6$f0|7QpGtmtCU!Txd*#BeFpDP(h-oH+JfOoWierRw76Nco9 z-E^)fv{sWfb|#O@sP^re{uc&UY(+z<-0U<6`a1Mo9l%OLi*_77Axj$lFu7q94I?Aq$rbLZ+F6 z)_Ksb7I00(VP2_@{|x(DI=|@RaF4DSZ*mrAdB?g9O@z8&VrNi&j( zkO_(8D$_V#U`98;zC5G$3j6ODd}(*}wX#}}Oe>@w$UXJ?E(e&DZ4n|W#3)0n!7_rD zx*oS3c_cvynJDCGj7yOV*N7$RR|p5y|O!dnPn$q0tI zRKM};VLsw=L`#2Ahaj+2tA-vp1er?pCxvxT78-t-QA8D-B@IcnsTAvagf-36wn@1kel~07d zOmeymNM33Y|{J`{8C`f1+}xS(n?e z#NI@9gUf}+pw}XM7KL{z%Sk;v+488H>3XuP*4#8&@XHevMUr=an;IfHq2>~0h-C{!;>J<;T+uNyef|ya>=xH#G=xfA%lf`q*7py z#($%aQ7KekB!1K-SS0o0WTuW3nidrb&rkO;;U~U~(tvUD{fc-LjJ(2-*ptOGw}r9I zx~Fc@N8hS~7RJ01?YN-1;&b7ZSEumM;yb+LK{$qAEBUPc8Q=HCsjgfvL_mZKEZ3eR zt3<_UD58ebZig4PI_+qBSoiei4y)X3R zm29Q!*$T_4U^K#4wBzNp%R8_yo}iTRNu-~cxk(8}si4eBGu=_aL|H<;rULrL0*BkQ z6f4Gti_tqweodg^b9OMQZ4`}uzg$XJE$^T9H$zl9(Ti0FO?O zLO)D28!U_?N-3v{Vlz@eupR}wLmBl3yO)i9Sh`j=Phf|{$EMg@CH$K0=PD(P!8Y9s6%r9ZFZMNF=dTB>g!2!Ulxj-G^k}<`P65BO?)U$joC{iwZK-| z3ko8}-%dXIpZ*|jYbv0?WlAR}H0QwfC?Kswg>P;fdN1DdcMq4miLp8E;B{QNJt-vN-;sJF?t~K z2;0u8bmXr%H@p2m&X>^j4wg=m@|_XIp*^L%BAAKcd>Lh7T@@p!2wOt0}2 z;a@-?80oOU0&8Q!XI9>jb)_$ag6VWx6y$ianG6E?2JyJyKU$Si1kO{VOLk+1j;37l zgZWZb*wo}ej=n_s&{|InLr=@K1wo4m{>e3?lX%5pw;Yy39t0UQGPso#H;FOF zX2y6nveNBvuPqj#*z}|@W6do4vSb*oi_J-K{6p%ZB* zhgm0wPPsyE@FG%G_%%4V!XW*4KJPHH4R$Vn5p?Blmb`8M*gQ>uUQ_>Uy2bPiQn)Nd z3OtLN_!vhr2+FJD3p9exckIC!L5h^TdR*$2wt}Ml&#QD-&7C_r8Q`pr?kN1zoKjeH z&kY0CXYDL?ReN$>R39-5hovmafLW~fMGtJMa24cSN}F)*{nQHJ3v7G}@(q(^04%507CNojN5!gjo3|LUL|q&L zU0{_gc650-{eww7sx~p?!{l-O6W>Ocpy=l!dWIx%uV@Tr8gg=KYOQ0un~Jb*#d7_D z(OGAwD8m`tI?oBnR0z@i-jL6pa=X<^%lW&%IUEda6&spLwtb6ZVS{1&!-?yCo;|h+ zFx6D33JS2GEHzSDXkqujFx@g8HMKIoC#jZ)uLgl4+QEf-6Qx9jBtx?k6C8B!gqVyn z^(Mr}n*;a-7rpvGcgr-R6M{ss3f{Kf_yfVVAd$M@B2vwnK~FjgMN3&L2G+uIok>(- zWS(f>6f^|S1@t@|4^u@$PZ=m*$F}X@*U9bxaFZrLw~tmDk`D&B>Ul-yl6~)U6mTK9 zHmHcyWgJaC>JJv_*A-rGv~~F2*Dne$PA#kpZ&9d+;=Pe(#0m{hsss zB0bh*=CPNkZVq@48zT)GE_sOn zKr}3h#g4_HbgjD5Y$vbXY@2v`w(@7Gxm>w=XkM`5W49bNQ z{@MscN4zaf`>M*Kx%;hrr2qn!P^|+X}#n+ zw?B%94kGZXX34;c!{`f#kt2r@?!EE|KA`{3O9z?z*uxgdO-}TJ%j-(lXg=L*cf!OLbl5~h zFjvP=9vC^|adSBB2m}53r!?AZEl*P$B<(Q(p1^_*B8hY8RFp{EDp!<*cYJU)t&t`r zfYeY#R+Dw_{@uKe;ordW{$1(R_ANw{Prhm-`!I7q8{x7CePWI~zGk8=BS8Ki zT9^?bloh;xKhTB;pWwuF!4nhwvDOd(AjplnA!ZpE#{LR;7ZbwTOCt~z#6fW!BEh|n zS~7N*|D*<)){={P%j#diL{5UJ=0l z-zV;d*%biuU?DRu*1TVHLiD*`cVoGW%Dft2AeE*&%H#K1ON!iF;EA=Ik4*3od_JW+ zTeYjqAm20i$>#Iq+&fy)(7@*TupQRteY?frHiYk{5J$jYfbzLRx877%2+qmKS7|y= z95w*;{(O^A;fo__z2@_1BCRAfHFc|l=Fl;lkrJnj@<+oYmkreazkYJ#v{OI;rZ8D>#D>+-|?|T#n}=(%7ve04TbFulPl)#h!Q_5D&y3LXTZ! zsU-jdSN8y76_=aW=kr-0d-OwGE{ji!OVI2m-Z^)pGJrGz?h~`o;3pQj`nyJ}#n|6n zZ@$?4*yX>GwmnAT$)yt~Z2+THDxWQ&(ro($(7=SZ=bM6(y8I01KkjuutrT}5)mNM# zZyNz?ss=v{a>_te9tf37bboj{b^%GISkaqG&e6zN-bfz7x@?>;UC9!$bsRl+6QyP) z>gQeL{Qf&QCix;cWK%MyLtzK6F+=*XvW%Z(syZr}dL19@qQvU_E7Ud@YwXi=*Mlq`Q3{TipRl=Kumlo*-Q8 zAJ+avr`w^CrB`bRDK04~0~gAB-l3e;r!4?+km%0=28C#j3k3F}D_apXllgH1(kTZJ z`}X0Q&pfWDR39q=BQ&*8sSPU;kN%$RfDN4nVBGq7d2O{gnUa;_AE7H@mr$S8t8}O> zg!IE76M(a`06yUK$Cb(~spzJc3J=H$@a(>8M=qFv9C#ppfsZ9@Ij&_#?TBGFpCXdP z$OxMb;_LtfHqva-k$KF!(*Ce-w2z`fBO@a-q;*z{45cDR1E5#slZ}hIG8!JC-N8ukW8JKZ zK>ivGloB4Xl4vuF-wOad1mcU4`4njfqA-h}Zck>z2z?Lv&&qwaZX+WizNugn;SAUl zag}d8~yedkkvJ;ttZ|7uu#$t|m)<{hlol2?tQ|_vqYx ztm}8%NuBCjS$P$Sb0?FluJ5BsWK>lCqOMmAfGB6iI=GX&KZ}|{zLJdFbc^NU;~lOh1Nd~`WPSd6LX{q3TAwZEA zu8kNZTc`Ge{vH9cmD6=5V;xd0dNNV9Qt(#2C;5oRFr=V}(pZ@8529UaOkvP$SX!B# zD`EbzNc}v4-9mxg8dsEle=fr&={oI&1N2u9uJhi=n8w-!gSj%1sjy&{I5ei1p+^@w zyzXsBX5h`YsV&x8n73_i0Lu!|2e~H9>w5Y%3!~%97eLu*U$8mPq)dXCnzx+TwH;B+ z>Xm-RmcIrdrhL#z0yKvE`WuaNvcLm!ERWX#)CP|+$hIe2IgS95_0kl=BI5%G{zA!fIO)D>FuYr!P+Jj}(D zxCTe$BRZF5rwhJHIgN&Gd+7ReAo!+8^Ftd;`NC3P4!(nWluHTw0weBOm$q{SI-iu3 zGzfTGB(d^TovglZ=glJzcQWeakS^UlmO|N+E z8N(2%KyNADZjuOtP^a7ADOanc@dI^NOk=Yk5e!4XAKgz{>;&!23HWA2QCga1kmGS) zG}CsAX>oW>x!YIZ^SsqTcRo%HK(V^tc-*85C>Fh(VYOvOGg>M8r-U=y^C>J^xZ7S#)@^?~7vq zQ=`xz_7Tgo#~y57R$mv60%Ox{(`SP&g%m1oy#2iMgg*?w{?32_?ik7l@>)^<0U9-W z8Xi-Xy8TRfFQ$nJr@6&3br{?Z;7ptaB#46?3^Le7b(Z4D~bP z5{rQAiX(C0zp2H@`+-)4awxANTf%Kh+LMC=0<;3T~hm(oDOX zodsW?)8#u%>VzfHQ)9B(>9fZiZHcvn6^||G+rbBImxF=@2@9(^ND6O`*9S5^cC|MK zOXwGf33`SHgQfLQZ9bs)%*3oN`!l2`B$tBy9Lh6#rE0!$VI63)@Ts0jywBC$H)YO5sqxRN}}Y->kIZ=m5oC!-O4o= z<~3#^8M(MjW>D4yH>m4>u>waJvs;`vMUejoE9ewkutbjA&)Cf5p%^xx>;Jmk`O?uX zrJ+dtwdFwTTYIb7Cn{fknbA4;vy?)`kzP$v5it2P1*NJ2$R#!S7p z6+aCPAg}_~M7x{905Fg>Ex>@FMH%?Oit>d&`U?s`*wt#VL>7i1b)d^y`);~LJ|7x zrIUt4$|b42YeZp>H>(@GVf0$Iirn@I)un`OSal!bb)W|<33JJP`dKECCIxXOX3V?H zLXbvH)aZm698MG0K3&r^cLo)XTL#koCdEP?t*o90>l;iyoMoB|Ok{EQ?`s(x<)TMa zJzHtC09aFaBfOAUS}H&mu&=bB?uM^KM}{t(xHhX zjxvCY$|H)!7w~DmOtHbM*#R0WoFEZ1im~4roTo^eEa)bQ{#Svp(9RR4qFLg;sLL^+pj_V>D~qMPtNJcC zHl;l;R13V-wP2S$>M9-6MnCDcLnyOa^DB~lK@ZA^;5%FqVGs(z{EaJ!$A$aXVG$9R z&R_;%xNEV}TKFopDF)BtKwpLCj+9nC-&)o9Ekj~5+=WHXN=%|QdS<1P_qXBtjz zh0|8+#rC8+jRwFJLHV`mb($iEyucTT3h;acNMX?9O;rV|VOK@Od zO_b{x5cVSL5~ltL?0FTFFai45RWZ){^W5yhg5MV>KW64u=(V_{)@q~|LMq9BhLF@D zL!YGjS_j153cvvKzE0UNOSy%^MEFh2!#12$JA2j`{d}ZyTgR4U6ku!#;K(Q(%4SRP zDW4sIpW+dEU{XiwRAn?U*v-)ALgl<-EvFH= zhu+tiIM|~v91fD7=&3g9L?8hIvlQDGzqy?**c?bC7md0-vreKLO=%n1IS~XMfpkkT zW_bIU3c5nT+koxFR*YS5b8`OU6Yt=aL$Dmk2(<|D;fUNBlTMgO@vOu6hfu3xqu$#w zL73oieTU{!`QPoh58_Y(n@DDmc76{;r%foqgwUWQ8)!Gy6Z-K3+(>-^$mrt}S{_#; zl;r93Gr7=q%1D0jQ(E{dF9JS@BZqZiN^Gc#?AR1?RMbgW#ZhwXw4sp{+*|8`KS68; z5hg4+uo}TT!84VGYPGuFUY}Xe1VZh*xz>whaqY;#O0VrQ0kvj&Kjc_m2A_<9(;WA}`)ne6Lp7;lB=MSF99Ul~ViC?jDOelh? zv;8h@!ijin5-7wETYLO)zc;v*b+Zbk*NQr&9TdR@E3R(X9EK~oBZ&z82%xHq1QQeQ zme9(9U(B6~IFX194%%L+up2msLRahv9Fv;a66fXzK2P^F2e+2#WlMs^*{NmO`?}vSWWLkiUn}rWS17QMr zNPeLNX9Ov#@&SV`9OAGg7TMpwQmXm4nhVuk7L>EYjzDCULIy*{Dz&CxWrG_KOUI`} z65lxl`Cart2Q%$R>)TU-x_$VC=MBWqEY^Y@j6-7Pnp4Z7-ELq#gwQ%-p3^Ks<;|@8 z;0>M8T|2t`A3h)?n6Er_UC$lwYxK0EkZ%wflEMB;s_gv1@_G{p+V98sR8 zQa1>{ua1W@lzEMiD42mi0dE^!04NVXe?rkD9|a8hq!fOSw^Y)B4YYBKpD2{M9INRd z0y9I;pHPo47D2=MEYa;xhYhR149a@G4oWcdu-Dkg!H6P z)o?s$E#}#t$J9~}MeG_(L*lUT$nernZfW9@QXdiY#lY=@}W`hOH$Q`s|nydlAEk? zAu&`b1rzJreKKDawqs?K)7)8at^>ElYB8G*=zajn%~gA+@JN9}+I`mVgY3zO2iPxC zcd9Ru0hQX#qk;ox&6^DMBGBQ76-+|3>rXZ#VkkU$RKo5GTjD(eKV{v|*w!EXo_}P| z%|$gB7^RsQK`t6V9g4vPr_Dw$H{ISlk>T$czyMhjY@(F=(0O;6WQJRgK1}ci z#?TXeXHQ0)9^PEj7LF5bgMx0aSWIR3lUzms%@+Y!SD%_w+2e;@4pr{#e`8p_!+qR? z5eP7Ya2>Q7A3=KH2;sresg=YCjDEtYK?`pw0`Y+(z_RQ`LZsNHan)fOrI8_M8;Zsj z1xRcpEtEYV_#cWyh$BIt-XV;k&d$MT8X)nA0w0F+czmMFb_)5Cwf58V@fcjg^`x_m zgU0tVT)XgZ@KfY6gYMWl#qu&JebQOXL`*{JrFinBy3)CYdhTk}swG=_1KDbM)ydg; zxszt|+iz(DpP&vO+~dwlv2xU_uD(BCK1;JX&KXn_sD_b8jVZU*)m{mM#7%d&#m%nH z@{8Kg#4X=tx@~;>{lvNH&8p4e2HCSghm}JN1e^8DLQYx__Mop=96ds5L_6A|953N1}B-!8D9sOV393ELBh`GrnEKGf9e^qUP2V6!xN&@-x9WFXJ zg^d-F?xr5Y6U#TW!1*r?=88uQbH@QaV5AVXvRD|FYnpqCRR*j@rsGb_jkJ_rHQ3+{ zGueyUfOU=MUSZgA7NEtX>vPe4Lu^=7?}J-$>b$FD(SgpJU2G(|^|nQ_OuMaguMH09 z?}Z5l1N9OW5!QWtxZL56Mz54EIEg_o*s&QM*AONkHG(`)R#Mt!Lc7bP`6Vv)ndYY^ zFOyIlj;2HhcUCdKSByHvnc!B@dONEV1_!s(O^J4YJZwd@Qdr?Gz^7nzIbBlBj9+ab zkPSapCvtNn8Q`BuVX^vtSLCGSZXZAi*7Z@~+u@hX4JJeAz7J|90F*yhEKm5T$4QGC zF2~CMEyZEezZIfusO{mWTvPK~Gp5h_ z4*nrx4P(M^jp~nJhC7n}C1!z25beS&X)oYiS-PWzMq;yZmKB@e{y50;Is_t6(#8hH zsgp#;gFI2{K~Y|bwv@ePn8ac3q0^E5F7$dzv!S61LN=MPs)0YBR@w-k6O~>sP`-~cMdBS5REeS%5qd*EgzLA zMESz;O*897WcLt$JnQt)g9#Ls&rqYM*&8H7YI+3p{$5 zmLyD+(J`n1_)W7%EzDkZvwp-?dSt{%EI|V_+V$9^_1N~N))V2={wbg^*KYtlUa43v zv!iSrOZ-PFyz8IZm_T|Q;+{*4LPTS;9Jws%PvJ#`obHC16&en@S?zzW%=MP(GtH6? zCU!TFxzZh4uyg2NQV=H4;EIg;?@IK#I{*0PrRGi@DYVq7exSvz4F~Z-dx*1b-NQ%~ zaQm$zLYNORJ_z{x%3)1SraqZnuj31ltw^LurVxP~8es297Gn;pc5H1{!!~~>7|;YH zYlJcG#1!VpGCm1yKkIyZyZl*Ud!BJcgY>j0!J$U+7I$jvw(5EJ&HMmc0XrFLJ0 z6Em3UyC(!Ox)443U}(%yUIGkclvsuB6AH5g0*86w>-n0=?_~q(vBT0qS2oWN zcb!e5S-l*$x=`vVp<}U=^&z~8^{r-Xn5-vC!Wkt@^%DCZFL#C#7@JHB-mboRAqoSU!vpICP~j-{rJ#)8=_qS_3;#mCMNixyQvK z?zFKab|L+8Kse2%@U8f;N+{Y0ya-T?JR0FbVq@`u^&3Y-MC1ZlnB#e4UMOs0p485` z#IFauZqhl2RG2=U{ZBPW^?Z^HV@^%!#Jva?OvZP+Q?KU0gKP0=YWE0rCxt;;cN`gY zH39hrjmxbpHj8*MtOVoBgLycL$v}2zK*#GYD8N7ZyI|)%Pz!x65o?cgI>r)}q6%xJ zKjz!-dq72ECb@$<_W!BH=q{>cBeY)8ADzm3&c**A1iNw}YI5}D@vfb8^>#Mjd<1U<^IwnExJjUO)&CrjsYjSAR)fXTte&jy7`j=TaoD_){$( zs>!@$xg-sS9-=Fi@XK>zQWvpCRxKYdTKr{ia{L~4RL86&-Pqt&2H}iIfTHfPJHQ7Z z^J00$M5^$dmIio=yTqZAJs=%GF^tDrr;k^!H{<8D#sAnc0kxN}ow3<|udO;?!aIMK z2#WgV`zjnUO9PYg=hjS`5lG%qk|Nzr{dvVoo#?LXT{tc8z|JqJX5or!Wh@h&J_v$_ z8B7)wk??7iM*jlPTyH-$KV3Z;nR>W#jIq{J-m|kG&QF!Ub~3;Wjc4f~`I^`ol_1sZ z8Ndz<%Ag4NtlEb`3!ewJbrdH zgoGC^M`>hKZ2MYMn?GAo&3ZCXZ0l0X5qfZl&pRLdo%zaB`>u&Yx}RdvlA^~6eca8Jy1 z0LnPUWf|lOasaE+T&T++-;g+qo_Sqn)cdHw&P&|;#{ttk#{HX@WmGXRbe-uoPyI^4N>?A973mj!H#qltDEL zn~iq3bWs-TepIMoU`=rP&B|>=W!@7AdGmBqt(-03Q@5^ZYJIQoA>sEtLYX~vIGt~z zr(LPlXubF7j|_te29%l*;jY!2#Z}PWp&x{6?!DBqlm3uR=V&k*LhuCQsvmg5lY(y@ z@K9m;Hb4-W59DB_s)+2Ly7h7R+-o{cfig-eQc_VI-mzrH;^1J2Qu%pUMb~DV)glBR zTfbLwTK_bG1kE&o>iHe-zP647uGUzuLuehZSQ9&eVkthyt$6&xa2`0w6Bbr(Owt-| zT3?p_^j=9=bLs9+gwaGTtbUlsaQZ;Nx&Wg`~R~#`K zVUW8*045C!Kps24Qrej=BAF7Lwo*z_whbNC7W<;sfx?-2gQiO4RsJU>EXs>fq9O!7 zHo9mbT@rn-8Ttj> z|EyVv-&EB!tlZGAJ0Vdc%hD6kOTWw2s~MtzTFYw3len$w0^MGo46F z6csK>&2ZA`-h!Z#xB+|)3=g<#2xc8fdCm`{Uz3571hQb}2_VC+TF2O~!w?uX8wto~ z;l$qr5v3NIz+K2w8c0J?*6R<$fXy)e*=EwjBtT;~CPN*-ykfVQ5%V#9!4U5(;g-OjhG+*x7_}p+s5QNbKeg<* z%o&CZA55Er_^|_3rD~p-!=RGhoF@4xCjr(-{SDw$qEKui;BtJ<>T|f>pEv?UQO(zM z#cnSI+g7N`Y`5UHS^7!O$0suMhkfAqOb6s=*DGM4XZICIyUig(cL0@8Uu~ty9mj;MkqP_HjA6no-PJ&7YqozL3c#U- ziGO#&fK}FoXHx+(`KP-L@6<$W0y!4{Ucaa?w;ms>+zaO)rAjFDQ^_FASeido>bx^q+D^7n zpmv#9^}vz|OaR%w7kt@lk4u=O#&`t9a=r|Gpuc~(H6R7*zf?0ql+p6gE0=!@10{$Y zkzv}y_OiTwe=B#bX(JMZ3p_GlNuP%u&g~ZJzsMK4WB@QOCyPA%mvI^!Xm$o=B;;_m zCYxZ_sln3Gv(0g90y$eIhx9Lu&8wE;UD)|enrSm%rk0!fY-{w@o{naVO{TbSNrMfa zr$iAIfe49g$BOD^d49T7nGzs>XlTG=&{eU*V zWO*!5Rc_1$SMf)u$vRbO&n+taSYF_64k-;CY`opx6n?pBUD5N=I9=V%!$35&VfYk{`>30-M!=Kv@H z;eQI^BmlZ)9??EZIiZl!NA?uTct8BRug+QkymZEe! za-pbiss+|Vu}}08dHlZuB*9Wrwx?2HvfHm!(@h+U_HNnEaS1`J^jU9x-vLs#{vnr; zdGF2M12=K)lk^TT+t*(w$8~qG_>Pc!AFt1%d_T=Kemu@?Sr*={2eeaa=4WPFUOWDP zD#CV#-YlXwsyMu}WOj((jk@_^abSurO*N=N?v8106u&@#B+K?ysF)jhV1?55e=wT^ z6Y)UYy@JpRl?ta0a^JN|Ka1ImJmf*H&&PUc9t7M=<)24&Ik7M$T@?lslr zkOmTSE!VCpFRDg!s=_=i6_er0?9#wjEPlAP|Gd$I%8k7cURJmk`vnB(E^w2o@?$=? zWI>KpWj8wnBh)ybN^FpYnotG1S*!+Fx9~pMU;iOvs&K|=LtEVA1pv8u z5iAF43#V_79r~b&C2O`QG*iOxNEA~H31TDwQtR2eq3~8(PiZZ*P+wsEY%f=Zv^dRd z3{={8B(kiPA>8(>>ANg+uDX=6iiq3icC}$4bJ>sRb##nxE(cng-=nlx+&|Fy%jnP? zUSwcxH41%aVID`yNf>0<1q#PHzojoSFC4pU80=!?=aUeHT7y+kO;Ser%hrB@cJ+*`^9v{9~gd*=mtSY-(19?*iHyG6Ic3W^rf9cqtdc6PFDH&ygo zd=_ybOP%4k4+TZq) zERf{bc)0HEr@i8;X217-_Twv@T}IefsYAnP%N#QMN8yUO_7;V+c4+i6+_KS;nuPWq+0``NCoH?S*^hh7gsL%%qKB8Mmk%h9;164X#&K z4c230*G3a@v`SZ+*V%q2I66{7{5qmM@&j-JV&7PzP)OtyX?YB-v=jxPznKMU=2+xz}bbJdB+VlF0)m?#S4PxI}-OY{HeDX>!;x&QbnvA_C$7R3>N=6sxz zT!9{0ma-Jl`i^}Eqf2zXX+hMAa{=CYcu>+P-ou;Rk>fRh{kRFet-B_+U@@ zs5VLRDR{g}oWk%^_(A%p;*HjTDLm;T9($cNP1S$|0P7du&IA9FXtgejdWR`1N*=qH zE9O=0jJqdj>bq|P$kGqKXPh%at{H+tw4QJY6qSB>^36%Y|&CktN6(D4gq*n3u4F9&m7@(V-PsZ4D2fk420XjX3z9 zwL!#U`a`HXuV*V6Z{_#OV5>C*7Ot+>Bj!BUIhf*ji7#Z_Q+Hj&2GN?H{I%>%y(P)I znE0B4{3r3Lf}k%CgzmN&S|yH9SdqxvVgs|l?zE#B5<^XHv!f3m)Vfi zYrDUa_`MeKjcSC_HiqY@kLH0v_cnbg4e9JQ2D{#m7+A1|pIUz+aJ0$R;RFy`s*MJ3 zxa=mTrV$jZ80NW1O08Jqqp9MSjr7Col<=tw6kxs&E27yh*7j?%;*PJP*d~FJP`^?7 ztV?&}xaE>9d|(Il>g|C01F%Znr>US`DUWfSibO^AT9tg;olYkNm5n6{lq?CPOY;-R z?0E#n+Vt6~s}`u!^S~Exr7F+RLghS}42T^i_r&$$dj*rw+mBCa>osD0DxVq#cx911WQcPBddP};E$*)yS#;aI>1Wbj zdwr$h+0h$J;(tY>%CI8{1Z!;3AK|av$zg=I;~AZVtbVZTh2gtHZny%Qe2a*=cTqpR z@-^8cWSCqhofEgwwJw(p!~wm;2}j#KXIDM2~fJ71(3 zCV$Y8w5BruP4x8x_!75)$r5_A`I5BNH)$4ZYm<@_8qmUO_C2u&>G7u%&P|v*boq?T zhl4lU+)QUY1t!MScm+m_x92<&FKt#HzP~h69qi4xX<_qo9c2R|YhI@=s=S9~7q#%j zvVkEE>v5bp--;-GOTvCS#OO55airX^Ul8Z^mw#XMoCmu=D04Nt@oq~|a@q4Kay${r zoE{auOi7~!amkwHM_saLElDB|dE)JODCSro%(6h1ZGxK5XdZJdEMeX%h`-)2v+$Y! z>!bt#*ji|SqYJ;T)^}=*s`Ah3qg-~Q@<@L_Q=6p>%9h$RGqnFxnx*hDCuJ*-z${i0Ud#+fr@aEiN9OvD zc2|Rz7}FkEh<*WC=k8tto+uqFkQ|`nRH4VlC{E}P6ah~}&Q=L!ym^!*6zueHD8M&+ z6M4xHc7?}gXFneP*&Y+J&48zo?7$`DiJgQ&7eWazJvyA z2P`3BWo!hUs`%AY@#C(XSQmz%^X}S;O@TCNW?qp{ZRR|+oTP#ug%zdnyuylF-xo6U z1)<+z?*`qvRT)$O*rg&oBp(bl@a}j4FZtG@$7lbtW>rp3p_ea&N$3&3w-FzzHtUqL z)GHz1Sn%_b;a7}h@YfxVoImV}OlOM5`GH&`<>oPE&F?GH4jzQ=t&P^52C@1sK9V>g zBw+|KA3`BO{DNfw_n|C1;xn79RBP+FGZ}z`wDl^@IoU~*TBB8g@=O>Z{BnnD^Q zR{Z$auKaMO2`e#QT=koHiV!g=v3AuXKxQrWVYr zYO_B3x#Wt={FK>w2M>H4F=8}$VHB%2bY-_G@210adhhGxwmrn~JNH|GrI+MAO)VNn zZp4mUPhBiUcP)lP224KUHg`-vUApVDO`4J-gfV2P^mtSJc;J(>+}OE4PnBGW`csez zeflI!$gs!ias{9SNLoF4F*adoz+l%In#co(XIzIUrw4%pDAfcM1l}>efrUN)9*3;Y zU<9ULVUA=U@`j{Q(4D6J$rZDuB>wN2#vj6UU&hG%0m8a^x)WhhFnD1M`!qedk6^z; zX%m95j6(GDDp&7u>Kw(~1@3+P4wqsI8`OK3zNX8~6bcR(jz1j}!9@QT*>5H4770Y= zw||+}m=u?Dw@N-e%6C1WW+C0&yuvB7Qz2)d^ibkv%*QtcZqG%QC#0W09ebLXbMb`$ zyRFLSF-kS_JM0n0!=+v7%U}0jBQxoTHj+8oJPQg$Hw3)nDfvPA!9mj76ulrWn(XJG z%P6Yp{DV=3cpBczi$#MAAVHU)OHUO*)YUf6>)$61ajHuT={wYon5G@Tv~fSHzzhOr`Fdacl+s2bo(N+O_UOCIl!oH#pdqVE;@1K9xES;! zJtzjAF+GE$(kL`F5$*4Q4FdCFn@QpxGw9;3Wy_be4p%^U)44d=t*{HZe)u`DgoYjR zyAT>fWk@NpUzgH~4ww$b)(f2=Cjh;1;)FXrGpf|ncLwlm;&uEOvqkwep*WjO zX_m*1^1TL(dHM?+vQf+N#g441oL%e!Uy>49v83ijOmnrZ-%7TyrE#rp?Ia0s^&NQ z(s>BPof+U;mWS;IdXA=1WLH-N&D2UY7R6^fd+L0CY$=fKMMdBqbyJe|b%?r?=}@&v zA21^yNm;;0OM1#K+}FgUN%Rpj62zWcgL`yFX@1#MtPVB>zG8UkD7$pTTh*w6Soshf z9F=AuZec@i57ENBy?IkF4$7PoOQB-jdtYO_2{n&ysMnU(GaM)walg`?EblHA6N^jV z$?CQ*th&+jJ617U12Xx>p-FJjoT%7PgnwDz0F0S~F0HX8Q`v z@mJ(SX$GMwq@RvJ)S*&z1~suHslCA78v+~&x2kCJKe+%pF4K7^TE)cMq!X)(hMZoW zFB2&JsZb-2zO+kNIQacFp#hYO5`tpNfj(6x%923L*d?Sd6KOQ78p?*C{*hNoM?o)x zr3(r`m;!XzhR#qi)*b8T?}6y2QLg?{^z-VwTs~sD;gX@XQqHv{_9j>fIH+vgBtl!6Rpo!{HyLMZ2At`;7&7_QjxxzO4c1ydSifL}% zK;}gQJg{L8ua;zJ*%Ml3c0N(?Mqm7^BlD6#JnP__MW&8l5S%<tg;)R)iM)UN18pV_XK0gZ6X#@t03_VkN#y03xUN~uPvPECa)$W)_GM&2Et ztwrP7;{!bf<)45y*fs}nFd~lqkh#fhJ)b1Gw|*`CT)PX#=?Y9=U|qeVaPc+@<%5E& zxVTSu%Yf*;KtvLkw7oxj%Lv|LuQS zhd{z<2swy|tR!IqSg?Q4nb1aUAm*sCBe%T&A0-t=24z5ocaZ63MgCpqul>3O1*Vu= z(BS6(X|;GNi|ar diff --git a/plane/code/images/migrate-error.png b/plane/code/images/migrate-error.png deleted file mode 100644 index f42ec441a7d1c7cb52d8dd24b4b1bd4311b843fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14139 zcmc(`byQSu*Z&WK(%nes2m?b2QX>sRgT&BX(p>{6B_$z4i8M$`qjcwhB1ou!G)PEF zhrcu4pXa&n=l87j{p-8dm$hc*)V|KSu5+DzUi-Z_vD%u-MELah7#J8tFck$|3=B*| zU^x!=9`MbyfoBG6Fm;lb*M`ZDmSQ#3NJ9e#tL_$e^T(W2Bv`B75;N=sRB0?)5OD*qF+Os)E|dNcDRu9$6T2c z6-naN;laG8zf8WoRTTrawxEk}MlN|qx?%rk;a*x98n#Sry?9PLaW!hh?2N}A>p_qh z%HgHyC&u<&3kH%Ozu(}7q;7`Y@J$#^fz!5e?DXv365*29wc7}P3hwXV;!{Kn8=aja zy?a)T`x=tf$QH@{D>+M4t`)#kuYEdVdDmS}oN+os6oikC5yWs93Y36B(v3XRnN ztTMZwKlzH?$FJ@pql_nd!U3}a$+LgzeikRF(l4m+VD);u>(8dMiH{*07B)}kdoHXd z!_eFsqKO)|+3sncC+h2)mEjbPGpmA0efs$~!@i5u&p!)jhj}cVRqV`+KYOpd?F+?> zIkA@fUAPMdhTFgn4zt(Lz~BLvaWODoIALG|OPIii9{6BjV7-dQzyW^AfsaD|o&Va4 zX_$}oU&|PA=mTY+$ira3?-Ls@J3BXTM|Yn>tM9TvQ!`F_a38pax`d6pE5DVkyR{vE zfU5`E1Vbu70$6ml^RZ$HaCLF>mI#n$`+I~0u#DaeVPpAwh>x>08(c%1Mc&=ZjzyGT zfM0+OiqFErBIRXkFQKcT^p82PCe7yPL)7?I_rJ+ju#7_&B+{v7p2XHKe#7lZQYK$Xlae_{9 z^f2zcHRZ+78=XWq{an?sTI02dXNGxD`PGJ4S60-mkweZpfAdm!l{3y9jGiv}CetQy z#HbPRW!*HH6Kzt+a!}&I@4flSce>)na5MDxgz{cVhPjvjk3fg>y7o_{nmOAQhBbRX zLe9)SIDO$dOA5PYPaP)-J;2gw9)J7?v0>c#m`C>>vzC*s4D9w|Jj^fbX2&ALh|vSG z=!0WxOM~vQF6kqg7Ts;(ajDhNKUa%w2Jehp_|00IXU&5@-Yk=Zfnr#}`r!AsH&<=H z<~}ed&o1~c{z{ZO8pf*L3@%`^Raq(dD#VfIa7F|)h9Ha4t7Z;8FSxo|{Ikb|IFOyK zw^N&a?>d*@l>-fFHEH!J2mPo)tn<3fy?5Pf-Kq9oyqx(k|50pl_vv)S`suH^O=>WF zsaB8ms9dKg`kG17uZlrOf!DCwhp^-YF(c2cdEh#g7~ig{c`Qhq9d*HFc;i!CW*u}8NMwUZsPXvzS+U0J;c;tQ*0ZprBjzr7eLR%Ye zbu-(O6-NL0WxxF&zm$@g7kt}u{v2_)=dw9tiirU>L4Ao+9(R3{a7Qfbk`sg~aSDM!-by5hrvUv{>r>ucM? zc#G@ao9oTTGy|x+liox$J%>ZgH3KSHkgn@`kR*R1gSajw6SXftGM@0@WIycB+=B0{ z*`$+c$jP+ZpC1H!-yHI&80a59MLuYCTWH-Ips_$6KcZ{?R;tB()`P>WG9+Uzdt|kt zMl;qIdjC$eOduPnQ_Fj&FM|(VzfS@E^V~Zgf9x;p@M&!2_GH$ACTe(KGdK9tbdAMg zfsdHWSC$!CgVi4$!3&%jrYl_PnJjL1!D6_~ZkgJJ@aIn#wnM7qtA4y-4uw;SQCk|L z^rOFMN@`s9oGtk0H7m}%GvQ{G+nIT{upfFRc}NkqAH3*YX;|ZTZqRkULhvEraD`A( zM+mWce~~onr|IqKhpyO@pkGb1s`SkE!vxeiTS9lglK{^W$Qhe0jh8|bq{@|1h+MAF zudI)hxL8YFzwW2d^0}6E?>I@9AE8zSy#_9o0Mdh3Ta55dlQo10e8U#ZQpPEm*?6KQ zz255gi|0(cIa|yo*rHYa-f183Jfi5ItE|V#b^N)#vY{J7 z5l+szckz|NLx5Xq5&i-H_NIvWX87hf?~l)BURdYVo_|;HvqhomXQp;Cbn?i8uB1mR zxM#Ol`=cjA%wh86U+(cFCR99Ih|EJN$}~;4uaD8)3&w7T z&Nj{q>kWoHOVZ^tj9z!h3-S97X3Mh?ieDC^fHkvaE>(bfTl*K|cq2xH4IaugOS4pY zR2}lxwCmd13Gv5fQubfo943W<16MS)(RaLb>z5JwHU$DzC<62x#&2>KxJMUxONYzhM?PJ1Y zS4v2RjKK{CD=<%apM5o1dhJ~8x$D%;L^Nv=7NT_et-)cmpM?e(tgsQor@ONryLCgY zu@u}W8qfJv8=TW%OU{TCvEf{b_qsl09uim#JyL<|x3fBjwDfo*-gxv8iK*o7w;v}m zG?35z^)C$13qi9%C)?tk_SVe$;0K*gUyC^DDm?-VpW<5n;<}>r`T3dEJ1M8(E2e5J zByU&?uW~o;hMCMU$Zt@0d$uMo`=N9C+UR$9l>%>kKh%4@!i@!+tvHI4n?#QoRqei=WK)w7xZoe zaa7g=^|tYduRo(ptgmy09TF`qfR~R4wVxX>_?!y)AXJ)$8XP%Jqlc)Ds}WG~Y|}U- zoVz+K?YIA~%pB5~i#njWOZ_8?AZjGE!FjeiEvMo9)PiQ~$}byb%W%B9WAUfeWr;kU zb;EN%(Fhb6Q`7Zzefx{nU8LA3t8~5fN$Iyu7LCBu1YlxNk~}Zea6E*8RL)~eByF)>6UyVv}L`lTS z@%&)xam?s8nzOs!Sl81a)`(Gtpe z*SNB~51nU_CI~-z#bvYRfM`}x1JBHGp#N9ZVO@Yn>Pu3}U1h$#J?nx<>MBA>J<0xn zRG;{dy?z|lDOD1eeDF{KoWHuZuaeX8cj(k zpr0T^F5}w5siEN+1c*#DRr9-R9P2{^ezkdEiL! z+;O(r%-F!%e@t$|rewSG{jS=m7Z@oRIkzxdvm-LY>W)^dMbFF!)3{7TA z&7Hlf61JnG&rZO>Bk(@IUG5-WFgnTa*X3n{g6c%#-w}EWFWvj>+Z%kko04g(&JZrl zPln4}O)lx3@)sRZSkDXH=6gNTvO?|O7Y%=yqqJcAHQyr4W3=)R4CU(RU7K^|xpq;^ z+Zmi~L1O*Up5l3KdQ|6tW~L)DP=(h!#OXVQzupf;@x1Wdbo7bHD(WvdPO@5}ntZ;R zjG!lZ6d4E=Y4G4se5b0x_JQGd#XnI;#^2VdQ0@XD@zi|t4@%##Wxz+QI_Fs=PUlEL z{C5;}s=+0b&qAvxEnk!ZZ#lEh5D0Q1S2|b%3Y*UQ;}MD=8|c@nLps}K(4?2Ljb~}w zlH8d&su$`*Ix7~s1~PA>pR0O0yngQ5T|tzD4wBV`lv=gQ?Bth;syKKOLapx?IItqA z0d)z-sw;Sj_K|vE^l4FSYPInA*mab|6u^HdXc5BHM)FspTM|+ElU+=6bX`7SmteJm~jVH9v`~7jmc@?s$Shb4N&Kg65kXPssh=TFO(UeXu7(XOcJysTL26%B5Gwrhu)3BKOYdhBio!J?iJZY0{D z88IZp=)>cQ^T`Y%)sZK1ZzPJ49-HI|IrzEHCT(Uo^7ZJ7bPzdBUkh!9(SMFN zKBkI9UH8>TeI+NIq%~;ft=$E{gmd{x^N|0|#j8-o^|=$?#EfF0<8KA=QW3#zq1T5) z)&pNA&Ovw?Ai61uq>8yzO$nGptw&ZXJ6tyqN{2Y>UH{g?s?CGVqtLPv(Qn*OzP29y zb(Jb!P#vDFjJ9C0Nd^;?xRvVzuD_%U?iLyBq>t`KvI6e z_~iIm(|0`Q$nNE7T;2D-d&PpiYUK3YbX4v8iQ4hU&CGFPGh|;f!`*(NhZ0<(C_M7e zr(E}YSX7{<;p1EGrgx)uW&W9#NA|$9oWi9X@X{Ymh-EYvU6H)&?_!Nh3yi(wIR$;R zly;SDC6&il*ErwC-Dgn`=Rdex7!{2aQ_5>`+JA|C6}L_hd>&TW~diNFxL ze&Kf$x2l+_eqP$VEx3F*=9=r^MO4_J9dFERSxCb0tt8+TrpNi~t-gzg&zGkLtdyyUwos}D1`Bf^As?wKN*UItBG?x6 z9fr94p1zfV%8aY19OY_n>Y zyFIVK$%CQEFj_|Bj(v;Ls0ke@IRXs&A7;<1e5(-#UyEU>SVFd87}O0cbR^%`U-{ZV95 zYq|6jTA@&VzePaN#_&zLkMk>kl?FrWzG^Ka!*-Wz&Q8IPJL;jpwUm)!k8K*W)*8PB zJiA93yCv#G+Z~gu3++I0(b@sF!RQolEyTWP9Ny;ZUK0aE0eo5Dnxnd(W&z;cU62T9 z67n5)Gq2Gv2UgsE9>Wy_r~WDY2Q7rct`mh(k#AUc6O?@U9%lDXOnzQ$Ie4YDKt{H5 zzM7N--T?H56u5ng81MNC;B>Y@U4;)Us?usuD^ErxGn24!I+nr-i0#S91II68o|WPW z1bnm05SR0i$qPBnDw<#f`)ua;t1d~ip?JvVc7Who6B!}E=es@SsbI$0X#j28an2f$ zF!vha-1}LS1Z4?#HoYy)3vInGXdNkeG|bM)Pv==r&2I`yqOdvxXq@ZLm-K$$slvEVo7DsH8V5B0Vd`0bItZd_w7?cYzP z+}z^585h;tkjkZj;^-;7FBMP=Z-O_HK+i1)d0tESR%8p=#Rc!RY_0`@^Cb70`#P?V zM!gbO*rxm{4c>7`r+)#|1n#rTlby}i51qN}Y#uHI?Y!GdGc@;v8%J(c>sO{LL=U_w zWMJkqd9TjaZSFInxCe9wS-SurB!Qkm?ji#&GuDyu-dGx|FRDypVD#AOAt7+ zv|99}RI@txY>#ezs9~u+_{^;*dRchFELJee`B_X$aUc*#F;Gi2lhcV-nKFl6AF9{! z8dP}@&eeize?+7pHpjA7?-4VAsBbquW(zwNhDrf3D!CBjhkHHb5hp`Xs++5`42e?V z$Lq)^#Wr*GcJaGbvOeF75+$~uX_C>Q37{uSGu-dq{>N-F|B%byDc4JO8)I3$Ob6acIfoeQ1LlJv}?m< zCgh-WYtJ*oJde<-4%?3O**rTGwkFRi*dKY6!aCf0eR0e<(@fr_lMggCsb*b{4v7ng zTU44|C_e&EA5X!jIs;bvh9n8UU69p#x;oNZ2^60kx5|AL-72JfPF~PkD7~GMp*xrHwvSKN$Apr!`i_$EdF&QxnMsvf;pI{GV@h7i z49(+p+Chl`$o7XMEepxxrdYI6Lrg0OnigT_59sst0b(N95K5)U_*ZAEwvb4z2aO7U>XE!s^IoHT^<-)p{ATkZ zXA2paTa8~dQ)!+pv&*h%cWVP^v!1!JxKq=`-)+6{BY3~#Rpf7smo-S5bQ-e9gwg^K zZd9E4E0qFZs(u9VSp>EZH{Z#RCc57W&=zXndYFYS>b3y47KIkAdhAyRUrv4{NRf@l zAUS#>$6BXDJR=mjALm5_F@Lj#{7Ko9rgVL=nOB-i`1EuB^QFXnK%~_-w%v%4@-c?T zJ2?S3CDht-b2=BLg}7|j1L-0;Ac@BCwGjnCoT_SUSd!jLx-X_VPwB!IIfpfg_t@O! zJ03M(S42)MB*(V?j|HuB2b1q_^qhsc-eDFiB9c%-QnGh8HU^zMb;2QR8HuoJOrkkD zv7$C~dqtQ~?4u`ZR#{SU!AisaPT<0J{KdG(ZCXg~I(5EB5OuUO{OS3k@q3pC(rj_W zy~fXKv9?~SOp*xf%9$fO<9wO$!=V|Lvl8AD zg}n?_DMWl3Sl?OwGW!X++3Y7Bcx3Cp`cf=Tar`PO*ycxjpr?zi+3ZAqG-K&a zD^#QHE!H6UfOZ@`rk&tzSE`(C5~^lumRP$QMXKu38;Q3XN0!S))W9;>+?la{6Bf3UHuxxa>tSY<3 zL0~8ioBX4iC2{B=lXOrs(6{((1wNR5kh{WHl6C1_r?6q z;dPNi33RLP-4)H_&Xor{$XI0sl9EghP`l>v#`zGq#vIQ%tst93hwIJ zeH5NDJCJgf$Q_MBATGZf>1OBuvYrw!?z2E#tY?%OR1Y(8F+(n0DD8tF+nF3#P9D6I z?0$y%D$n?(PrVPnI=pZ`93?Pja{lI0;lWC$%od+Trzw~&`sFz|i_~^2kK8wE zAUeDtCxu@f+_b{P%hnq?X*0|;-E%b=bqtd@`{%LnXE;$%hC!FiaW4WU5Fwdc!aX*? zvq0|XKUTVguSa7okW{t^tKUR9=+UJG@{M}kC8d{yQ1r*Q*?*)TBIJ+EL;Aj|%83ln zsg+`}=XTm1(`!(ol%vCWfcPZM6OPA=1ce$#o*^K1&rQHbde3AFTSKtLEUDg!{ffKY{DXDfEF6oqm!9 zR}d8D=^@JB$vWHp#x8sXCvU{@#k*@rv;>)+Q0MB4VYcikrs^LpNT+0f9;WMyPM~bM6LS6CSi;0?u^7~95>KR zBR?jJu*)K7Z#BwCC-cGk`J}=}bUz8V5I98i93yD$SIl_^OlMVl^un+2?X{|9OeHpf zH0%^{KINtAnJ^E&U-|5tvg>cF!CHJ;HYGA*mmHb6*8R3H>!?uoLLx?Y&>-UGaw-CR;)fyZfVX*7|g|kEQ46d!9H-xaQl|4h`uIIY`+KI}# zamroeANp9B(bXhU2QpOxRkxPbT%UUF^zo?$Q%$QL52$|f)d_T2_Uj`BVFAdTF3ZVK z1uiSOKk#@4k~eGJq4e?@{9*SCCU}0csRtb`-5q^AAR5pw7uBFfhX(htE7FL#2V(&K zroE$&4ixmugfuA8yO`0I0{E0^XuX;LXJeTyW7NEx}*xXaN zRXsaSVan@SqOSbo>n35wN_L0k>x^SVa&roGcH?mciW~iD+?EWif-JoX&MurS+8ckq zVls2eK9l4O=O-uNff4swPL)3y$_d=aA~!FFFKZ%`5Q&jc)-}!7KeN`fYJ14ol~kro zRs_giJYDXM(iM5m9s4{Q0VDuZKUSrcqLuf-={FQ#Vq)qEsKLn?(G=X8WBn6s#g}Nv z-+T4;-A{J>=+B?&C0-xJOe^51{M2^DDXWt+~!OQOCnk;~w92 zb7{hjIuV;+xOiTTVdbbV5?7Vg;?GD2I=b zR4mHBAWnnpKfd}l&f$VD%dX#&$S7$*$(>^OFt-C=%=Nnnj$eN(TNy)&q)xSzG7Cf6<$WX z{NR3NtaiIX+Q1~6we36OCJu5gh~)aYf3lzPM_)F(1JCvr!ioAE3THdf8GsqqJ3LNI z!MlQZ*~HZfHblIJL|q!We{sF8may(egpS4k!1c7KuQsL~SKIGlsa)huX$OnTnq-~Y z2c-3c%uv(N;=Q3R zKIw*M!<3=PggVXx&~ff$d8=<=h-?4stwy%#&E*N_EB@sG`>{+`*a`rRV}|B$2onZB z<0-r`>k92iP#;QShNO7nu_LvzL6KT{ggwDVP(yn5bb|0Df7<{?4tk2SKyjT!goWOP|H~Mj${OFKu5Z5z10;jORh9 zOU(OP6`qSeol=3cwEFI#93uwt$cWpu@}P;@(~-*D5)X>4KCg4ESO763XTl@a=Gb7W zb?O3&VxnT5DoQfLeW>YDPSD!j$m#NY_bP7Y)v3~kx)zc`OFmPg@q3&iE%ob>S*wp{ zf4brG7{x?#tMl_Q;qjO|8y7$d*jq(0^5e!pcPnSQghG%xC8DIg_G~e%>wMxTU!b6l z0f;EN_#g6q9_ckmh4Ikks{qAz8?BH+T0C1f&(IK_=zTFaWMT;^T_eK*%VSQk?lz7y zUGKO;OFRwalaH!T+!Fo_X!gK1d4mcn>iAiviW1zcUuh8KJ}NE22W~S3nF8ZQ8(PGo zQ)OA6u3IZ*E&@IgU<}@^v$mQjKS*c|e09VCsQw@liSiFWg4F7#VG00sGoh)Q5edmT zqON}Y4|BIcr5TrL0X0|I?*qT-%H_+d0%+TM_5V!UBxu?$SpIY0 zS~#}%zj<2?%zb_MRIqp?)uH!fuTRks!!ks^CsC0<;8r77%BVL`xW3R;C?QYG-P~SZ z)-;z;%!#|%*F#OVnd22`(Z}=R()O&$Y+ZcMEVhp`F=@${(Q2|>zWvFzF3(2rnllB8 zBuB#FQEH}65l;wz09fJF5_)kBm1%j(1**fN;EI7z?j+IB7b*{Vg&!!Qv+PH~i=81W z{%es&XP~%xQEjE)kyeuhG&A1SRvB9Z+5vMn(*O*abbBM-kbGm@id{xJ4^dOCgkxU5JTdpxbH z*e69ji2w5Zcas@&E+x&2_!ntoe6}R1Ni-p%_`)4DzUr@@i!}2HQhowTZ6@SpKWORY znyS5$HiWLCVq0(jP&fYJ?_i&az1$0U9w<)dHFlXao{lD;g1dZSFc>mzHdENg3zp+XV%YC&VTB{R(@!Ju=Z+ig0GX?U^ z>r@@FutK4=u9M0SY>!TSC2<@CccS6noLhsSNQvkh3Yk!_Nk!+LWSJPYGQBfAoBD4wZpAKf=KOlbG*ifq$ z;iV<9Rjdw^8m*<}rFtwfLv&sQuQN_tIZ`MP{5?&~S@A3UVKmk+$}VJ9IN^-`3{)e!btB zH{POk&m!~!Wk@D;em=!oyPtI+9S}Kr+0eUnEPl4!8^gI3MVOH_<+pld`w-VveZh1Y zvHIy)gC---JZUxcs`LH*7Y^k7#%|Th%#fCilJTLD{4;N91LQ6Yp};d+tl+muYI%Os zjYZObr)Qf)i9`vIn&Z$?bJIpV%Ln6D9V7j>C-?O`(F*=cV7u<@vov=!0Fys|s;|pK z@vqD!IUs2_^;206pi7db6#!74t};wT?^8_xjK~e2QT;WdK%;hpxdsm!wFLp?Tg?>H zpdBrPYXIDB@GMkIv@3Ih$F?+|)E8%?pS{F71s&V_u{=FVxvcO6~w^<`530e1O}Y zQ}F~A6KEw0Rff|$>@xTMBN>Y@(YguAYyawXr&jqxm1LEb?REO7!ueWMT)|y$8Vf{S z+iv~nn&QeEfeCt(*kdnlziwq1>5^t_etdtp>Z^!kDzujFkQ?|`&qdtjTzyGKZm&4j zC$=5?J_8`3D|B$CQ+k%61$1rFfLFAR!!N8`ZZCJJ*u;WQUGThn?^1%u>}bGn7ED5J z%{Q5`MT#$kLoR({Q;;yzHRd!O5|E^vyz|eW9AsYaUU-DIq|<0xR2-u-!{Gu?8QM@w z5t&9V{hGMH8ujp@sR5GRd@>3XKoko*ki7F<`sVi+SRisbL@u6mLPP~LCW68%5d?}; zsxTsY5X8}o2Y=9oX|*bB{r8Hp&?L>aWj*n9H!#iVd!tk0<=ZE0Ui>;>B`MSpa4tE zf6ug3eg$ApyQA(Ex_@MG1#10D$L*J!NPI&=!2AsNl zTR`sX=w&KNeCE&B$U5g){<_^$aNkRWYUjpb4Xr9D4p?YyZO86{YTn zGL5>EQm0>S$VC}V_kp@cEM#T$B3Bm`ufd~edpHSvzo)Fmdt=NK3x}AJAPhibDJC_~ z%n2^0pr5RGk6@AJY;kYPP4JJVnc0!vpTw27pAkojmU%sNnp7x!zp{<*--zw4@D_Mm z#DfojioRNS%jEn)+GgtW2}RTF@eHjWPYB-vI<{Ic%d5i#I$?!`N9M1^-1mSGi+!Ji z@x)Sf9Pr70f_9hT~K1o_f^+7`!uk?}@V>F}oq z;eX5Bk?>rt=8ljeJ%(sP z0~BX~6o<0)96K|9tQ80HAI8q3`bjd%aQAzePSKbRoEp-3UI_ByCntVGgB8ZMJQ2%* zKVca%RcE7&$DVjsZEX?s0XK)ml?7I(tI1$LUnEWX3th(N59#>*YKm_*b=WYB7CX{N zNk?eSfl{f_99PQme*4&W`=%;D$ZiAb2vn}*%cIjE<54@ZUj`XN)`3{7DMJLIbQoc> z)qeabRWn0J<%8RTP!f)747Yl>Mr6sJ|=HC?92z<^@VONfuBOAakp!hlL zM`x?laEdW<6tU7AQ0^zO3CKrn1I6F%T^Lvo6dUu1lb1EYpRzjl;T!fzVuS0D=oN3M31`{ znQ7kibQ%+u{6do;zLvJpZ9#(WVm|ch7aP`XC^P5uVzcLJf0V34gc^3Qki8f6daQLx z=+x6M^&ae2%U|dYkAzY9Pnwx}&muNEfdo1SdeH7&;NF^d1T{61Mb8hT1ilXuIO#;U z`zPEv&zTZW<$~Q)GLoo}d0tVXYK(hL7Zo+a9SlW8AJ@bj13`UpaiebgEoaV z7zo5$-iTcQY3$@FbNuBe%5kmRk^X%{YMs5@BQl*npJ<GJW$;0o!HA!!cq6t5DhFtaU54+n37lA62<>)3NqG2zX+rfl( zUaZK6_(=s9K;7R1{=1XB08u;A!Nc`JlP8C15V?y{!=#^h0}`3*l#`gM0Tuixyo|FY z#0wYH&G`Q({I56>^Lr9vj+-@cx=gXrHL- z_u*F+B7v*CPiL!h2xS8b@;T^4EaoWzhW>aYzR7M;AOw26%+APcAoVC>0+q(QPDU^K z(%@b2wVsGX&Q#Ci1Wv7?=ZZt_(I)C5+BHu<64DCD96cdO(xg@tw?k3tSnA|hWh#G4 z6)39Kk!cnUc%A{V$W7B|W}2L09*uU)>(M8ldRzpi1f78(D$U*9(Vt?4lAB16{uzaA zEoKPMW5&B~iH{;+d0)`VHNRu(Skn@hZ7BDDF*Nvh7I>7ZAnRtsE#4WXRqKjLA&qXO+(kbb(e`qQ+ZrzN*6u6fPogu7!e7}NTgo1c}8^36oyI6{+teQxB}v+ySe5!N|k?z#2JXA_1%Ns&eHgUIoNw$e?g zIcct6^hCd8Tuu6!T-{47ZUCAeN^GE^c{1Cr6*e!BAOL5T%?7YMRp?tdYvl`C7{^F1 zNnTbs&(9&NtB|*UX0iY9M*hg5Q))Wzwfhn;sF1++W=hh3FQGLpfJiOw2Us0G1^+Cu zOSCw?InNyd=|Zem;?f}y+sRXB>gTzaOSQD|FrCc9l>;Hci{bd{+m*)k!D(h*N=M%9 zYJ5P42dRp4G+lT=oyJ#D>>+Om=i{9rR0E|+H0L8PR&0m4A(MK@-HW4CB)@J87iaxd zG)CV~9HsykYFvXbfuI<6{c6~ZyJ=9H8!l4vj*;7QFNc|PUS{lC5%i-!D1@}Vspr^k znl+Eta2F$d9>Iv&U`ldIFLH~n69ki$8Kj%iJpu675|?2Opke7@-p^mrMIlFMxDK3& s_p}2b97h~LXp$p*kyEsl6xlh+y}@*CD)v+z^nXgh6g3s9(V`}LAH5FI%_z~L*XW~n5u#2+?_qRNLUf`NBt#dTD5IAQ z(ftqK@9*CG-urr9QOQRRHo{EEksz5 zucOOcIf&jipjL2te!;U|7AXfSCDcTbqo&JMib6w?3+18TRQhLI49Y3+AO88hoMvW? zX4Snkr;R=xx1W6U>vU_zSN!rF4h!+yaNajQ;)rd9>ZN7^#S03Cm(GQ2pXz%{sKm6q z^Gd{x_ib`+&autE82ouOK{!D6yMi$6L-%>eFf$zqy!i{+VZ3EcpUsD75>6fxg9S8) zb8f?uxaoc zro$sa{#*U;JI!(xje;@4A3u#0%x4=r8g9Fxv~7k>8KGBQ30%yGjCR*1@ zU)fqs4UGe6UNW#!~<>kNA~Vz2>-nzV!J!}QfuK^D%ATxOQe=2l$ZjxM)d(8Rq#K-19* zW=8Mr_}a-Gq`$ia<{-hSuck>a=j>)hFT};e#lt9xOHWTP?q+EX z(w0~Jw>$8k1fwks<^tm8_VV)L^5W-mcC+E;6%`fb=HcV!&J8!GkFXinVf$0J6A;~MqFMfCZ|2gyDcl;kc_5ZskzlhNP?D;>= z{O_JR?pAJc&W^w>VUqv7GXHk|pC|wAD9(L5^8Yat|4j4UQDB}WamBg+Yt1Ba!;bnU z(9lG+l;vfh-srno4>HMm&i>}4{W5k+rY3tYm#BiyEzg6?rp03$W_!)@755!(e6jOW z`8*#)%cTsIXhb-^jf99iMlse_S_XroSnC}PBmG|nMsNe=w8O&rgeHG$tMP<{1b=f4 z|L~u)9Z#?MVThpdgwDhwfq>(Tfa>$t=yhm@!JO}FDR8XI*FT8>?RE-NJS99Aa65j; zUE8t|tAXj;(f#(V8m#GiuhI8#8t|)bTOrTu(Y!EUE}-cL+zDx?MX&!?+#1DF9Q`0Q zTG=oltP1GABy=mb3afhVo+!)J@YPxkv^)@xL)10(Ynp2_doilaYY$$$2B&+!A$HB1 zP2g<2N5wjJZ`^h~U3S8L?DPKD3ZjW4d*S>}3+$DlLFbkR{uLbH;iYE`+oy@cj_gN5 zl7fonRcw5;SAW*$5M2+^9ed?zRuqZU8fI!ZVynG3>ndP|R@q?ezB=^P2E{8H-`8~7 zRYYAC)kq}usLKw>;etQ@x7Q8+n{Un43v;jN>_b+yMnO1S&V8 zjazZkEkk*WOr_dXZZ=ss3_be!S#bZzJZA7h37142II(@94EZk z>gMy;vCoR1d=$Z@6M4}S?91-;0$$u#c#c7T+enA{I78M00u>^J0~E`O@bl%UlQpBu z_Pt_8KTYjIVV0FUm_+Rk;(guI@xYRNQ^I?cgQH4S-@K_jbG$ruWNnGNF;R&`26BL| z>1M7*VD^9iutPUKIl*9kF9ynHQw@y&Vv7!NakA&NzWQDe@N6Nme|=3DQB(0)O)&jS z!W3AD6S2E5A>(2AN{O@biWbYgRQc}^(=`Wt@vDIL`bhQSRbtQ(JS^x*QYJ{fIps2q z!0A*eyrWIst3~NT@2eVq^TYf(P%QFv&)IhIsEul@J5e)(`leF#_wpbhc zKY!$b^w3uyzPET9qjmK+N`e`+Mf|?_1#;U`B*G{R*!r3rdyy~gjL2UT-0-8~^|UO@ zOs3guHN$J6@S)R@EOYx}hF$%GR2UeH!7l2N==uG|nM1r9&XY8z4P)u^ET^!0l6aj< zbvBNwo<%M-5%xf*`k-Y#Hf}0^y00DcV&IzmT3_vthY~l7gG6h$AlHq0qh99ch3eZX z@{QqOvCx_dS~Z1;zdy5wP4f9GAF&_HU|W4yNdV`2RT*$HewGZTmV8?21o461qlQYF%Fe|VYA7WvwH7Tf zo9-9dK33mZRPVvUwi0!9B2N^ABDf|TJwhJ&1hKZ33W_|NA#Puu9!gq?+<3ehK`hyw z$a0By;#8PtanWOb__EX!t}ADJI1-Z(rMSxzxKa^;+}-a^ke& zP?qd5EEp&I^N$n(L26Q$+vFo>592%B-m2@`xm=^o)V#JH?{{di#gsI=bs3%aDjYC% zpDnG!d-N`4c&Pe(;u>DEax4ZM2<)0%-muYd6*M_=C-m!7PKmp{9*;VjeZ_vGzBenY zz{<$9GOvNQqju55{ zDV-u^Q40Sy^+JeS(@s^EZ>S6_n&@Ze8gbvV`rRyWLAk-Jn|H`}c$V09^Ah8yc`{G$ zy<@NN8hE-vQK@SB_0Gw4-gA{8rulWwyi7QHwx`$Ql=)KGkV%9^aUivcofzTweR<#T zP?$r!Nn^eC0&znu+|PM5cTT8*wrzu-vW@fOVvdPI7CaI(8Ayc3=oE%NqNUHw-Nrs) z=Rq7GwlM|Mo(tx!4;td`{wAd?^axIXwdnkw!QovTfd;i8Ro&5} zgw>V(^|mYZ@Zpk|a1efwvLty4X8Jq3T#T~yl0hV~ScK|f9{Smx4N>7Gwq6v3%%rAp zOS--rYH;nA0P-{j)-vD;BU)ASY`58b6a)W;;UAvmf`W}nZ$FEACT>!%!W0A87O>b_gg zIYzb%k4}DJ6&0-MMMau_+fI*Lf7(hC-gkmq@2;y)U%`5>Oo0a;v?Beh(W@+oaYM@g zx|(L*0u`sQqC0g%*GT!q&V1wNi4S~WmD*V3odN1%u^J=zBD=t9ik%?RSYF@X>89=a z`Ta`<11a}MXMlA8K5)`z&Tx4tvJQ$wfWBT-wiEueqBXU;Y`XS|w7E zpN7<|Di~K*J<7G7Dvr*c_GE}bNNS3 zWA7mqnsaBAS;h2bO}#2v%$A&w;2Qqdn1~3xojP_=pYL+tq8@w*IEnV9No~y@YM}l`OP5FE_I*!A=F>88rYnF*`Qm1-v_K0d&ho$@_`A&|4Dh= zaVq+Nt9wWRLdt0w$#npu-(Q`7Hn&rU1Rw-K6Iq+ z>tQeT=2FKS*yZV=ceDX>=ijY~XevQ_dB2mL5|TARaz3LnDy*m%sxqSkm8cb6vT54I ztBJoA3{vS**QdY7D~y|M$8EBG!YQ~-Xj)I)-3B@9HM_#`H-FjW#EP8{2cGcTPkwqF zaAvhS|E_^l$u&&3++d_}BU^=5%rh}dhvl;PGW{WKN4lT`GQ;`nVMZ9QiNfspq@i;G z0t_D@SkSK$ksv*j=uS)GJ>?EMQvtawp#>aI==HkvAH5#Q!-{>xD|@ZWW7=>NXoF)R ze)d*O5b;xuQoGnn;&D0Z=L3$%SGod`2LF9$GzA?eSjr>KxC0~^3mghaIY~f=^KXmQBn)41@NJi z&9@hIl+x7EnL0V4YFnBjk+kRqjA|B@ zIwziYAC^eW$avP(dcIY5a1w_j_L?)m^5baVNKos|_V6esGw{$5J?7s%WQ zM%A2HYGD!(J9E?J5?rgVi%?1xI8aFP+0p&_@=U;C@;P~u3;)?c7h#D;j-)najH>37 zgqF*r!7mO|Txa=E0|NstHDjZMz)0j0t5gC3itaRs8XFW#SEdIN7@Ht z6J6~g70X>_1!r@o@>7V`OR5aVXS?$y=|pw-;v9Rc7P)kvw@s zs@$rXB3eVQID|JDt~Za~NConEhoahqsI)twpJ7{9Mn~J$0`HAZGGn-PQ-X{$#wtXBkAKUg+hAOweg8J zv->iL^p=rVzMBPBExgE5lU7i`)$!DkaKVKmBMC~l_6!(5yf>B_d~^M`&5AWxgypIQ z7?h_B>wDD){z0joMN#YtfBAO^s92cJsQO)~uzRK(k z4Z3MIIYQA-&U>q*s95IBeK*UqtG|nbuqw&VUe5WSY*!z#PZn!+nLKqHAheQ4^nH84 zB(!P>g~|&r`tz%02yv$OCUY8EvSLJNNFCp==z?QvNS}5UZ>Bp`#GfmYdWg7=ahuR; z>c({LhW4n!w4KhK6kIC-I!|vfU2HaOS0s46 z6Z}|ZDz~gW&Y5lO?6F%A&|5~g^FRjsZPAO%+y!3asE}A?DTu_9@Dj?e;dCXswoWk+ z`JAD$^(3999kw_3QhnBUg!T~T_@s?g6^r}T*Gv|T?3j<)+a1}3%Gg#KX)+`?!XiN>Y*Nuo;cPw&wV{p&iNWOs4nS>VVy=jdb8CT#kL-Lu$ zB{8ZeK^BSrnN~tQvWy8Q8k{yq-AwevO4eo_GSZj9b`bBU@yY3{eYXWC6NLkVK2Dfv z_Z#X@zKp>cR4;Efq~iAV*6`Aus#0<+wQJzCZ>qV^p$N)RzO!~uVL>al~kN7v2R_mSSL5_hb zMsAVcNwd%}&5}%0DCPACr~QLzMTs7=Ko^`2Eiog4OlFD(Fh+E)(om3J`q%YdoIV0sDc@Pma7k}hLF?}Ai|MBCa0BS3wRpJ6u;=Es(6^<9=V7GAe!I0tis6k;+=rI*hr><|6D(@G{3cgW z6HdcAXweZn8f}iU)avia3a!A4{SHe97;L%&2H*;_A5WKHC!>^-U28g- zI8=IBaV4@NBC+*N6LS@)Zt<(WcvLsjBOMfg>A0GZjF0j0O6QxsQiScl%a})DK}7#9 z_*J6*g33cP_)eY~f@w>QrgftPA_(a!GX3dzCqhT?slNC!wj&?FMhamPyX;jSC>795)1#U##E|QsKFIRVT)Bdws%R2nTAu3O1ggm} z%~vWZTuEBWc+Je+c4X{F~P-$ax{^dGL|H{XU_%tLeymbl?fB#mJ%#-NBJfEc?>7251U^eDRV{pMKFphUa<6sA+yWS{jVQMq}m$rs!B9n-4dXk&$GyHM%}g^G&s zC!B~9%3i4WYBX4ldAdJ%lPM;Ko+I-}Z|c!6dbQ)Bdvm-t4&>yMTkD>2S~L^`45tib zTE$TjZ{3hs7fXGSth5P%?`*gunojcG4Cv9$??k$=(5eP<5MB^_gnD6rtVz$-Hk*S< zXEf~A%?|8_xkg8zvR>~_N?su&TB?}96JlGCR@S^{tTW+;tzuVCpcmk5_0Txdv2t(R zzI_7W*E$r!^YPjChU#`*j>*$6BV_0(=7SESUwvZ@v%HDhYxqsjsq*?o)k};wXmgbQ zk*8|mzIC}v!Rmaz`rr2{zqW?oyNPT?d!*0sm24UzxTa3S_Lb{Vekb z|h7zeen^VL2&5Hsa=Vxl%%!hF~dD1%!^vo~26N+c@w#fRNS%bf< z7&5sGWX+RQqt$&ke^Sd44aj??Gx;(}V(b{=d&FDAsmTtJOLNqojJxB3p`3U;0bc~i zz-6)tKLpNgIbLTgcMn6S`<&BN>5d&l)x-Zt%NoCL|IPLUtdG;3xv-9G8nX$U0bPv&KHn&d#Y23$Dp1wk)LOAU)B0IJan zWvPLXCN*y&kU=9+DYOu zaGGRkZeu!5CX1BAAMK4a^?$A?tsDFE4W%X5diTV(Poi!t3e@keGIq!K&Y7@@P`wcH=Fro(J*%AZ4!JF<-vAOQg94k-&v{JO+`Snxkll7tnHLLI5 zQb31_s5D7~g*tn!`WE;CGcj%f-4Vht@YdFS@hvpy)XgrD!LZ9-{`thKt&RK?H~zJv z&Tc5k}$tv4Xhx!ewQ^SbIBKqX@Xn;z1W4Rsv0+IuD z(s>lciG*XO%}+hUp><1$lUngwo~)Eja=BZf9+JdGB;_dnpr`XVM(rZSDg zx*)2d2&UPYh>_~2QTgyF_PkQYf|ZMbjKrljylgeqe4=}tlGE+zJPH{ z+6=leu#Ey|Cu+nq%nNUQI7g$#T484;rDCSWMgt%k$aKndV}%@N$ZC1jd0qsZE=QEO z{_e#L&KPiBY7Y_cJ}{-?vs4oB2Q*PjL=_(Qm_K|-Hg|G*P!LVpO(8!cO-XN0x61eE{u4{z&{whiFmSoyS{W* z=WOw>nzrV>7S#mp$3J4z`D=QjO%4quu79@ZL#Eke$7g*5m4RLRnkfI>K502C~y zBTOqmJ15W8uyn6+?7@_J;4f9Ww`$pbd!oKeA=s;lcuoVAU(Y#Ok0%WeKCD>s!!TKJ zpJzXnyt#Bwl1IJc#*jTo7P_WK%Kv0&(6X^wdVbZ1(lYMhzcp-XA)>nZI6A59&<@it z1*tft>VeK}S3=9f%202b&p-6%zYz(MA`w3^G|=QC2$Gmk7kkYbqe|tpA=T)eEBE0g zObuB9C%*(dcl2kLGzq<%g4>MeUKf^O`+|3GOu?B3l*l`g3bN4114GNmHQTX@dp zO=jC-twTyniN(k)N1*_<&}+7$h;TwWr&!r%dKHOT);HHz1!AMNi(gy)U!d)xhQ#(` zB#DhYCUnMu;EV4ti>s;^P3RF1Qoo^3w6K=XI91ZV*z9(`$bJC+L}j(*>v_p(ZkdVK zya()Jx1Mw*kF-h>pWfIw{dDOkas**DdE(ptg_z=vYgZrLXnO@a&Lz=4Kyf8aP0znQ zzAxPPJ5H=vuhL}1bXvU$wyB+_G0sb>0a(Ymfz`R~_uj~#q8}xd9)DD9{V#7)t08sH5XYW9G+h4VBVtc@UC%2&EeWu`PoGY3 z^+2S^q+1U}?zBT@nTmU*9dC^b-FV9X?Vc*#kBO{$B7cmdY}`Pa3?-$_2LElTm89>BfU;(JP-2nGC<7QZT8 z7I`hjougMO9#PB{s!Ko84s7yt zd@&{u?@9bAM=0?$0ehU_jXZOz(4^K&okv9$)$ApzbRN~!Z>Yz>gQPSe_t0=3ODV=^ z9vs0lQk4=48(46ePYT~0^SiIB8Z>)y;!_E*r6<0^EnvT;zuIhbY1H=`H=clCNFs?V z)Y$kJyD>fn80fiSUCh>Gxr&4@>RtH$&)hfuzbOcsyhqy8N#`sN?onud*Dq=xG$>UK zJpMyH3?(cn_DaRH^01#`@k`$#Y>SR0WgT#AiOMWhbLullR5wruPmICvv&4 z!QuQj33N*yj|6A1+Cf#$!M0C?oie+*(Hwc)R+S!)%a6)S>@2qB987Yd`e+0eD&Er0 zt^69yt?1Ybna>Wo_UWusNAmfdyuP>TzGrlU(#qerq)eQ6>U8{m$5+qlbG<`?#K!kD zM7dPb6MdCwo>1SqsJH=KqwtE>{HPBDfYJZZ9|DG;>hdM^o#e$1|8_xYZMvb)8ThJ8uUCb&iJteFM2E<%N9bbhSDMj zzt!ur{0ZKjZB?@?J?|Y+hwE64(t1F3A$}tTNvZ(&FevXwX}!V(mp#ZDQp;Bk!>0HM zJlF>jyVy#Era_nfFt6iMifklI6$njv&Rz}{0dW)>f-cqiz?-KB(XEEPMA<&mm(u+4 z8JZnYy*Ecdb2)Em&pDdpN@(>QKy&gHd(3d(#;8t*J62kUfMs{;!%aNudtJqZ3+zgz z>S6Q6pPEkl3-bX-Z)3MSgwpZ~WpbiAjEWN|8STj9{<;|57&w>Oj_4x`j+2e_K5;8{ z036O`)h*{>dISN@z|ktr_C;Zmw=3%`ObhJl^7Zqc8Af=Y6=Bg4RqA!wk^!JwPCXgR zEcv-|reqXsZf6BbQSYmH9D`!mnu*?ve>8NX1JZp3NiRI_DiY$IKwz332CNx6x9U{v zrxt(Wh`UOK-CFQ2i~2PHUM++?nACb@lcoO1=02x;wz3i-TXL&&{Tg~gWqyUDVA9Xg z6;D2Sj%8hxco`cE%qJF>TtodYw7^!i5oPZk04z219mJOx33x>T6SmmEQ8la%EzYf8 zNVYuc58}-~po%3+?UYaoM9CX=I_?rQUgV}Ym1n%Uia5DhQ$YLoO zw=;4tO*;J+TK%!j`$Dj;C)IX1I`hb=fx`j45U%^H^mpi8?M29{{zutd_tJ0nhG|U| zpO{QMDgnTv{ZX&vCpy^xfLp%he{K^ehaZ=)d2x((cOHgf;SF%ous*AVReOMvdZGia zuH!Cy|7aIlhR36{com>Jn5dDZqg_|>dRej1KDwYx!2MHl(t8p^-Q8GCHgt-g zrJAVM|EH<1Th^T?^!{A3xER0at!`xwY%v@Yim1HK z2m%t5cYip@Cy99i#vPKnJFW2E@)f$SWfdfpkCi+(vG3+q7PCEsuqyls!8VWpB@|N+%(&01r~?sM*T+ zo`3x3O1>SXRR}k4wb@^2t#(aQN>>k5rBe$!XXBt?J0O(0d>?pR_DcDjqZn}t$wR#B zq+O3Mjh5X&nt;i^9i7{|Jq(O!=FO-o(!tFz`jUg`gylQUYin7PQUFLwaM=%;;dH1ca9RrW$NCL#UZoh-J zN6fy0h5bW_ya^^#&^uqzhnPkQOZZl|uyd7o(FDyMcQH^pqpO~I$Gi_deMQ=6eO^4| zdk0;|!0f`pDFjc%A6+G}&9Kn$kk{Fbv7D)45%H_7kPTBkd7d|r4_{&8jl>zCgD)A9 z5Q*EBx?2GTh3hoFsHbJB@tRdrdYZ$it?Y z7?w@*^wNt{l<-cm3gOAMr5Qg4!Yf*yap&t9Z>CDlWGlJE%U*%DR4rkDPhz}45AG0_ zn#`faC;1n*B8nWcf~$E0zPOdtOg72T=6x+k_>(G-xt8yC`8q~zDEihW_xlc^Moi?V zVEG?tKeOtT1LJIPsW1ZC^al4#rc zSuA!d?AUbl<=NSUaZhD2BBA^l$3EW~5Jbm_e((i^9IZp~?AHQNT;yv1Y<&G1m1aAe zUFK#)Pt@BDSO7VZ8@hy(Zoh`FwwyFrC#F~oi$ME)oBX$8P8k&_zG31P+=lK^ zss_3J00rROf~yn9KS+fK9T?Y~)K4L|0*WZ6@9DJPV>rCO#6)ha{yL*3=wWl-VDRNZ zso(ZwIU+hIN$Wl&`=jQ`zp2_&eUTF#n=;d7Zy-S%*1A*jzSMdf=GURr6 zhtdjl(!Q9lJ=|CBp>qZt_Qs$ z;Zz78n7SJIc^J_M!&x6YTp38^wRqZWc^l~2rm>voMy3}dOa>nING)Ln%bK(WN|<35 z>OUJ;+(f1f*^P1&(`F9f2t)3Gd{fI(_9jZJ?L?r~I6)#oZ9`x!=|Nz~J|W~3%DZ-1 z$o&>;?gvN5kBj2dL&DjvGl5u(V)6C=EEGe{LT`UVwWB9khcpjeEu3MFVFCD z>1j%@xH)gDGnaiubUwf9j`B5mnTdmF8L;9r}41616h@4r81qcxPfohFS#Z`}O z>{WI)UWeql?rXVd>J3n{I5pLK73Br_%Z2!qSuM3MAjl{%f4UqSbvEkU!;=?TCfXa# zv|Y55;(aUMXHHEUCAFxI-a+u8nz>VdwBWH5SK-I>iQGINAm%&K=3Z>jdPqDSo6Jr+ zehFdnaR{WRmpv$a285%gJKTQ(ss?#__#4)NMX7|zyVH%*(q~|dO5U85%zAB2MFZr( zTRJy0UlgfI002p^=3g&K3i$sEf$0m2zBuOKNuGVFz)Nr3`xXzCg!xLYSPg!`)ct(; zc0pAxv~RJq!6EKFG_9%OA>ZCwsAP5E3;b*`aqJd;1MIZeIKt&BFjp1ghVOhemxXAZ zvpwuT(7sC0hixJ}9bQ&URTw{O%ML3~_OzR>%FTS@mS;87)nr`sVF{_Yfq91&wJ9A$ ze@}30(f$h&kF_>_$pTcKhER(@ZUtzCc^ z=a{^dJSMFmG5n7bso`I!$oFG|o+ndVMPgnE_o2*}jbtz8Qq@kvOm1UFPzKsSatUfg zerI#i)8j-Hlm$uADr|@r^DVZa#w9>L6~v7`yXvf;(p-}2Lr~q<-TC<8NjbaMiZpIm zDD<@5@F)XK;buZmZULNP`85O_9{vb?{Q{ULV#7J#?eZgL z#kbm#FTeQ%q-ae*rvGgOmxMY0#AlVb|2haY0=And)qw%!90S6dq^ij?_0HUjOa}?- zEtp^_pQj{6rNq3XBem7Pt((fzB&mY%EY59dge-3TZfz>SIcYuf5uD+9&Fa4XodNA( zM|m~w9II-obOZryUIblWDwVL4Dl;>)rN$ptsj~<39#h6gYO6pR32x%OjOD-=2N0(o z0@0K4n+Suk#A6&hOb6Rh4fg%;KarEW?AvzDo!hAQ7{3L+Byanr7J=;;SjN9Q?* zn78rxm`t?I9~0UMm(DhFSYKygl(?Pj{2H%!ew8lfRq=37eiRdd6-({+xlE^wWw_j~ zd4x6#CywJQ1b}~nV@6px2;2|rt0@5I^w0!|(djd09V%N{;@7goypkaWNTNBm`deyA z%Pz| zJgOxUYjlvzWf}WuLYC(UbK#`~ar=+R`?y{S4I{K_u31NrHM4UhItLru`*k_&Pu=o^ zup`5ICu5X#@n}0SY)FNxQ67|A6G8up#NUD{uPmY`nv&FWQko}K)2F#7ftcpn$1aD1 zP+o6UCn4~a{~^{gB;o4fZ&79@P19Cs*;BAR05aTY@i&pEH#r_KcnA@Q@ZFZtYR_g+ zl&8JNZ+B-viHnESZ#`r&p+O-4&g2K7%dVIy;Q8*6=Mzqk#bxe^*rotuPH}45WNA8? zad;r?`n_3hZlwg)uFyG_^Wo}(>65qrplsI0`uf1861id$<w6!en_3AxtJ$ z)qedRb>AMfNgyeyqNnf#NPCYLs!@wh!PU{S2)|k_7sMS<%Wk}3r4!fk2MkP?oMsrX zI$MfzrZI4BA+gf#d<1g5`z_h&H4YXoEU8s@jacEhrsfEYx+mBHPy2@e6kuZQ{thHXHp%d)HP6kjq z+pKS294+GE(|S;+IlqvuG*9g2+pKQX$h&!o8$io#AZG;Ab&iH-%3!~H-XQ~+C)E*N z&66Xv^DB*Kzek=heKu}ZXR@AWd~`{g>&|kU2Z!Ae4UfN*9Csp(v9tdmFh~GrRuaHc z`UK=SjJ`_nK*v>8q1{ zsFBl$070JGS{*5wK6LC<9-+#b%NHPUa-lNwxDLM3LthB@6}BrR7xlG(?^+hT?*?S(90 z9<7CfQ>t{vN`L~l&Lvc+hD}{z6VR{)#XLG@vyMOV0x=RSlmYND{8Alb6vfCw1&rTM zUJd7(fZmZvxYS1A!)~{H-?3p(;BOsR>mpE^M?!Ii8q-Kw4q_)=mVm211;z}Ws2T*^ z3YeLO&^E?ytWcr+t=Cf)^-Q+PR&zc8l|{`xR-5)*QPhh~`50AgjMy8qsQ1(tb-4fQ=c1dhCR5BCI){KF2aq zZCR18gf%lXjOa{^z4JZ95y2ppsrC7@uIO8E%co>ss=|g;i{Pav%a$pyQZI-E8!ca2 z7_hVG44wc6bDP-EdXXqu8mOy2uDuq4$O?-(I1TKZ{%o;^(+bqv2>=-DtbmW4&~;}; z(WK|wqB8W%(o%oV(CtyMq;$;aCE_IPV zpZ&|}lm61CR7ttZ4s+4{A4)6Vsx%^o<#y0hi6mY&=e9o7r3&*$lD?F}RK=2oQf@{6 zFj$3drA4$wJS?Chi1*sMun7ke5xBFWOZY;g2=QuO?RW`{T~u9gJ2zw127DEo|E2~9 zfrl!~S@cdb<)37@dJfn*s*IG2`0ll$VokhNsI5~GKV)`OB{|XxV&zOfr{6@FM-`VUZT6J2 zHTe4(^_2gN1B~8v0>6O&EuW+s387V0JWX}28kRw0@~*_&n*5?Y@QWQ5AOYQfJ`l&IM-BvK`}J_C~g=uj2v z@80|~!g?89&b%B@8iyDm`(kvv4H@!5q5iXjbTM}{t2c-nW8|R zk#$i~Ps%2LqAe)McO%OSXOSqFo&bbN>Y>{++zXhcU@bri7s6NWn`mm%8{$-TzW|<| z705=Di2mw%L06mb`cRk0@<+Ph(K~0-V66P&Y8QD{m%sB~gIO+<#@5a)hxcP2^DC-9 z#(%^UJg$=EH2yM8EBV^uytrcJ?!f_S^icqr1GVn2wxG6_?u1lOeS+p^3N#iX#dDmZ zuN?Q0PYR=MYfR2~nRk}n1dEp<-~(uv8U}#EOorcaNR%wYY>Ob#$N4JPwk1Ht&`(>5VUxYI~5NnPQea!8% zyGEBy?|ZyOjvuv~FYTw1#>c#s9M1}b83DDxW@Y`mR6ciHi(3sSlD#bl02C!qIX&?A zasj`X_fTbE;BR&n(Ikbl%{YCs`%N@-2)(REuIJ4;bwCmyGDS8nn7x9`E2 zs0cBcR1kqQcwb%;(U6V|!wGK7Ab8Hwb}XCB&PLFE&SGusbN)#UQLy^0G)lwFEa|fhlYfGrk@4o%cnCGy{3mwVn!kOBv)acL?>ByAu)YfZlX1Uq&SSQ_U0KA_NQ*NLeb61GsT?4#!xUkcmdzT;lzh~QTuwlD? V5bMF<+1r<7l@-+GD`ZVW{~rQdd)NQ~ diff --git a/plane/code/images/started.png b/plane/code/images/started.png deleted file mode 100644 index d6a0a0baaee767c3e8d852926e1a2ec788b90f60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21489 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 diff --git a/plane/code/images/upgrade.png b/plane/code/images/upgrade.png deleted file mode 100644 index b78fbbb6093b60a8855f863fcddc329b8472292b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55240 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 J!m~C3{}1cA2Fd^c diff --git a/plane/code/migration-0.13-0.14.sh b/plane/code/migration-0.13-0.14.sh deleted file mode 100755 index d03f877..0000000 --- a/plane/code/migration-0.13-0.14.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/bin/bash - -echo ' -****************************************************************** - -This script is solely for the migration purpose only. -This is a 1 time migration of volume data from v0.13.2 => v0.14.x - -Assumption: -1. Postgres data volume name ends with _pgdata -2. Minio data volume name ends with _uploads -3. Redis data volume name ends with _redisdata - -Any changes to this script can break the migration. - -Before you proceed, make sure you run the below command -to know the docker volumes - -docker volume ls -q | grep -i "_pgdata" -docker volume ls -q | grep -i "_uploads" -docker volume ls -q | grep -i "_redisdata" - -******************************************************* -' - -DOWNLOAD_FOL=./download -rm -rf ${DOWNLOAD_FOL} -mkdir -p ${DOWNLOAD_FOL} - -function volumeExists { - if [ "$(docker volume ls -f name=$1 | awk '{print $NF}' | grep -E '^'$1'$')" ]; then - return 0 - else - return 1 - fi -} - -function readPrefixes(){ - echo '' - echo 'Given below list of REDIS volumes, identify the prefix of source and destination volumes leaving "_redisdata" ' - echo '---------------------' - docker volume ls -q | grep -i "_redisdata" - echo '' - - read -p "Provide the Source Volume Prefix : " SRC_VOL_PREFIX - until [ "$SRC_VOL_PREFIX" ]; do - read -p "Provide the Source Volume Prefix : " SRC_VOL_PREFIX - done - - read -p "Provide the Destination Volume Prefix : " DEST_VOL_PREFIX - until [ "$DEST_VOL_PREFIX" ]; do - read -p "Provide the Source Volume Prefix : " DEST_VOL_PREFIX - done - - echo '' - echo 'Prefix Provided ' - echo " Source : ${SRC_VOL_PREFIX}" - echo " Destination : ${DEST_VOL_PREFIX}" - echo '---------------------------------------' -} - -function migrate(){ - - SRC_VOLUME=${SRC_VOL_PREFIX}_${VOL_NAME_SUFFIX} - DEST_VOLUME=${DEST_VOL_PREFIX}_${VOL_NAME_SUFFIX} - - if volumeExists $SRC_VOLUME; then - if volumeExists $DEST_VOLUME; then - GOOD_TO_GO=1 - else - echo "Destination Volume '$DEST_VOLUME' does not exist" - echo '' - fi - else - echo "Source Volume '$SRC_VOLUME' does not exist" - echo '' - fi - - if [ $GOOD_TO_GO = 1 ]; then - - echo "MIGRATING ${VOL_NAME_SUFFIX} FROM ${SRC_VOLUME} => ${DEST_VOLUME}" - - TEMP_CONTAINER=$(docker run -d -v $SRC_VOLUME:$CONTAINER_VOL_FOLDER busybox true) - docker cp -q $TEMP_CONTAINER:$CONTAINER_VOL_FOLDER ${DOWNLOAD_FOL}/${VOL_NAME_SUFFIX} - docker rm $TEMP_CONTAINER &> /dev/null - - TEMP_CONTAINER=$(docker run -d -v $DEST_VOLUME:$CONTAINER_VOL_FOLDER busybox true) - if [ "$VOL_NAME_SUFFIX" = "pgdata" ]; then - docker cp -q ${DOWNLOAD_FOL}/${VOL_NAME_SUFFIX} $TEMP_CONTAINER:$CONTAINER_VOL_FOLDER/_temp - docker run --rm -v $DEST_VOLUME:$CONTAINER_VOL_FOLDER \ - -e DATA_FOLDER="${CONTAINER_VOL_FOLDER}" \ - busybox /bin/sh -c 'cp -Rf $DATA_FOLDER/_temp/* $DATA_FOLDER ' - else - docker cp -q ${DOWNLOAD_FOL}/${VOL_NAME_SUFFIX} $TEMP_CONTAINER:$CONTAINER_VOL_FOLDER - fi - docker rm $TEMP_CONTAINER &> /dev/null - - echo '' - fi -} - -readPrefixes - -# MIGRATE DB -CONTAINER_VOL_FOLDER=/var/lib/postgresql/data -VOL_NAME_SUFFIX=pgdata -migrate - -# MIGRATE REDIS -CONTAINER_VOL_FOLDER=/data -VOL_NAME_SUFFIX=redisdata -migrate - -# MIGRATE MINIO -CONTAINER_VOL_FOLDER=/export -VOL_NAME_SUFFIX=uploads -migrate - diff --git a/plane/code/restore-airgapped.sh b/plane/code/restore-airgapped.sh deleted file mode 100755 index 9da02fd..0000000 --- a/plane/code/restore-airgapped.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/bin/bash -+set -euo pipefail - -function print_header() { -clear - -cat <<"EOF" --------------------------------------------- - ____ _ ///////// -| _ \| | __ _ _ __ ___ ///////// -| |_) | |/ _` | '_ \ / _ \ ///// ///// -| __/| | (_| | | | | __/ ///// ///// -|_| |_|\__,_|_| |_|\___| //// - //// --------------------------------------------- -Project management tool from the future --------------------------------------------- -EOF -} - -function restoreData() { - - echo "" - echo "****************************************************" - echo "We are about to restore your data from the backup files." - echo "****************************************************" - echo "" - - # set the backup folder path - BACKUP_FOLDER=${1} - - if [ -z "$BACKUP_FOLDER" ]; then - BACKUP_FOLDER="$PWD/backup" - read -p "Enter the backup folder path [$BACKUP_FOLDER]: " BACKUP_FOLDER - if [ -z "$BACKUP_FOLDER" ]; then - BACKUP_FOLDER="$PWD/backup" - fi - fi - - # check if the backup folder exists - if [ ! -d "$BACKUP_FOLDER" ]; then - echo "Error: Backup folder not found at $BACKUP_FOLDER" - exit 1 - fi - - # check if there are any .tar.gz files in the backup folder - if ! ls "$BACKUP_FOLDER"/*.tar.gz 1> /dev/null 2>&1; then - echo "Error: Backup folder does not contain .tar.gz files" - exit 1 - fi - - echo "" - echo "Using backup folder: $BACKUP_FOLDER" - echo "" - - # ask for current install path - AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped" - read -p "Enter the airgapped instance install path [$AIRGAPPED_INSTALL_PATH]: " AIRGAPPED_INSTALL_PATH - if [ -z "$AIRGAPPED_INSTALL_PATH" ]; then - AIRGAPPED_INSTALL_PATH="$HOME/planeairgapped" - fi - - # check if the airgapped instance install path exists - if [ ! -d "$AIRGAPPED_INSTALL_PATH" ]; then - echo "Error: Airgapped instance install path not found at $AIRGAPPED_INSTALL_PATH" - exit 1 - fi - - echo "" - echo "Using airgapped instance install path: $AIRGAPPED_INSTALL_PATH" - echo "" - - # check if the docker-compose.yaml exists - if [ ! -f "$AIRGAPPED_INSTALL_PATH/docker-compose.yml" ]; then - echo "Error: docker-compose.yml not found at $AIRGAPPED_INSTALL_PATH/docker-compose.yml" - exit 1 - fi - - local dockerServiceStatus - if command -v jq &> /dev/null; then - dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped --format=json | jq -r .[0].Status) - else - dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-airgapped | grep -o "running" | head -n 1) - fi - - if [[ $dockerServiceStatus == "running" ]]; then - echo "Plane Airgapped is running. Please STOP the Plane Airgapped before restoring data." - exit 1 - fi - - CURRENT_USER_ID=$(id -u) - CURRENT_GROUP_ID=$(id -g) - - # if the data folder not exists, create it - if [ ! -d "$AIRGAPPED_INSTALL_PATH/data" ]; then - mkdir -p "$AIRGAPPED_INSTALL_PATH/data" - chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data" - fi - - for BACKUP_FILE in "$BACKUP_FOLDER/*.tar.gz"; do - if [ -e "$BACKUP_FILE" ]; then - - # get the basefilename without the extension - BASE_FILE_NAME=$(basename "$BACKUP_FILE" ".tar.gz") - - # extract the restoreFile to the airgapped instance install path - echo "Restoring $BASE_FILE_NAME" - rm -rf "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" || true - - tar -xvzf "$BACKUP_FILE" -C "$AIRGAPPED_INSTALL_PATH/data/" - if [ $? -ne 0 ]; then - echo "Error: Failed to extract $BACKUP_FILE" - exit 1 - fi - chown -R $CURRENT_USER_ID:$CURRENT_GROUP_ID "$AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" - if [ $? -ne 0 ]; then - echo "Error: Failed to change ownership of $AIRGAPPED_INSTALL_PATH/data/$BASE_FILE_NAME" - exit 1 - fi - else - echo "No .tar.gz files found in the current directory." - echo "" - echo "Please provide the path to the backup file." - echo "" - echo "Usage: $0 /path/to/backup" - exit 1 - fi - done - - echo "" - echo "Restore completed successfully." - echo "" -} - -# if docker-compose is installed -if command -v docker-compose &> /dev/null -then - COMPOSE_CMD="docker-compose" -else - COMPOSE_CMD="docker compose" -fi - -print_header -restoreData "$@" diff --git a/plane/code/restore.sh b/plane/code/restore.sh deleted file mode 100755 index 23b8de6..0000000 --- a/plane/code/restore.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash - -function print_header() { -clear - -cat <<"EOF" --------------------------------------------- - ____ _ ///////// -| _ \| | __ _ _ __ ___ ///////// -| |_) | |/ _` | '_ \ / _ \ ///// ///// -| __/| | (_| | | | | __/ ///// ///// -|_| |_|\__,_|_| |_|\___| //// - //// --------------------------------------------- -Project management tool from the future --------------------------------------------- -EOF -} - -function restoreSingleVolume() { - selectedVolume=$1 - backupFolder=$2 - restoreFile=$3 - - docker volume rm "$selectedVolume" > /dev/null 2>&1 - - if [ $? -ne 0 ]; then - echo "Error: Failed to remove volume $selectedVolume" - echo "" - return 1 - fi - - docker volume create "$selectedVolume" > /dev/null 2>&1 - if [ $? -ne 0 ]; then - echo "Error: Failed to create volume $selectedVolume" - echo "" - return 1 - fi - - docker run --rm \ - -e TAR_NAME="$restoreFile" \ - -v "$selectedVolume":"/vol" \ - -v "$backupFolder":/backup \ - busybox sh -c 'mkdir -p /restore && tar -xzf "/backup/${TAR_NAME}.tar.gz" -C /restore && mv /restore/${TAR_NAME}/* /vol' - - if [ $? -ne 0 ]; then - echo "Error: Failed to restore volume ${selectedVolume} from ${restoreFile}.tar.gz" - echo "" - return 1 - fi - echo ".....Successfully restored volume $selectedVolume from ${restoreFile}.tar.gz" - echo "" -} - -function restoreData() { - print_header - local BACKUP_FOLDER=${1:-$PWD} - - local dockerServiceStatus - dockerServiceStatus=$($COMPOSE_CMD ls --filter name=plane-app --format=json | jq -r .[0].Status) - local dockerServicePrefix - dockerServicePrefix="running" - - if [[ $dockerServiceStatus == $dockerServicePrefix* ]]; then - echo "Plane App is running. Please STOP the Plane App before restoring data." - exit 1 - fi - - local volume_suffix - volume_suffix="_pgdata|_redisdata|_uploads|_rabbitmq_data" - local volumes - volumes=$(docker volume ls -f "name=plane-app" --format "{{.Name}}" | grep -E "$volume_suffix") - # Check if there are any matching volumes - if [ -z "$volumes" ]; then - echo ".....No volumes found starting with 'plane-app'" - exit 1 - fi - - - for BACKUP_FILE in $BACKUP_FOLDER/*.tar.gz; do - if [ -e "$BACKUP_FILE" ]; then - - local restoreFileName - restoreFileName=$(basename "$BACKUP_FILE") - restoreFileName="${restoreFileName%.tar.gz}" - - local restoreVolName - restoreVolName="plane-app_${restoreFileName}" - echo "Found $BACKUP_FILE" - - local docVol - docVol=$(docker volume ls -f "name=$restoreVolName" --format "{{.Name}}" | grep -E "$volume_suffix") - - if [ -z "$docVol" ]; then - echo "Skipping: No volume found with name $restoreVolName" - else - echo ".....Restoring $docVol" - restoreSingleVolume "$docVol" "$BACKUP_FOLDER" "$restoreFileName" - fi - else - echo "No .tar.gz files found in the current directory." - echo "" - echo "Please provide the path to the backup file." - echo "" - echo "Usage: ./restore.sh /path/to/backup" - exit 1 - fi - done - - echo "" - echo "Restore completed successfully." - echo "" -} - -# if docker-compose is installed -if command -v docker-compose &> /dev/null -then - COMPOSE_CMD="docker-compose" -else - COMPOSE_CMD="docker compose" -fi - -restoreData "$@" \ No newline at end of file diff --git a/plane/code/swarm.sh b/plane/code/swarm.sh deleted file mode 100755 index c58f05e..0000000 --- a/plane/code/swarm.sh +++ /dev/null @@ -1,612 +0,0 @@ -#!/bin/bash - -BRANCH=${BRANCH:-master} -SERVICE_FOLDER=plane-app -SCRIPT_DIR=$PWD -PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER -export APP_RELEASE="stable" -export DOCKERHUB_USER=artifacts.plane.so/makeplane - -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/deploy/selfhost" - -OS_NAME=$(uname) - -# Create necessary directories -mkdir -p $PLANE_INSTALL_DIR/archive - -DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yml -DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env - -function print_header() { -clear - -cat <<"EOF" --------------------------------------------- - ____ _ ///////// -| _ \| | __ _ _ __ ___ ///////// -| |_) | |/ _` | '_ \ / _ \ ///// ///// -| __/| | (_| | | | | __/ ///// ///// -|_| |_|\__,_|_| |_|\___| //// - //// --------------------------------------------- -Project management tool from the future --------------------------------------------- -EOF -} - -function checkLatestRelease(){ - echo "Checking for the latest release..." >&2 - local latest_release=$(curl -s 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 to read stack name from env file -function readStackName() { - if [ -f "$DOCKER_ENV_PATH" ]; then - local saved_stack_name=$(grep "^STACK_NAME=" "$DOCKER_ENV_PATH" | cut -d'=' -f2) - if [ -n "$saved_stack_name" ]; then - stack_name=$saved_stack_name - return 1 - fi - fi - return 0 -} - -# Function to get stack name (either from env or user input) -function getStackName() { - read -p "Enter stack name [plane]: " input_stack_name - if [ -z "$input_stack_name" ]; then - input_stack_name="plane" - fi - stack_name=$input_stack_name - updateEnvFile "STACK_NAME" "$stack_name" "$DOCKER_ENV_PATH" - echo "Using stack name: $stack_name" -} - -function syncEnvFile(){ - echo "Syncing environment variables..." >&2 - if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then - # READ keys of plane.env and update the values from plane.env.bak - while IFS= read -r line - do - # ignore if 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" - - value=$(getEnvValue "STACK_NAME" "$PLANE_INSTALL_DIR/plane.env.bak") - if [ -n "$value" ]; then - updateEnvFile "STACK_NAME" "$value" "$DOCKER_ENV_PATH" - fi - fi - echo "Environment variables synced successfully" >&2 - rm -f $PLANE_INSTALL_DIR/plane.env.bak -} - -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 - sed -i "s/^$key=.*/$key=$value/g" "$file" - fi - fi - else - echo "File not found: $file" - exit 1 - fi -} - -function download() { - cd $SCRIPT_DIR || exit 1 - TS=$(date +%s) - if [ -f "$PLANE_INSTALL_DIR/docker-compose.yml" ] - then - mv $PLANE_INSTALL_DIR/docker-compose.yml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yml - fi - - echo $RELEASE_DOWNLOAD_URL - echo $FALLBACK_DOWNLOAD_URL - echo $APP_RELEASE - - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -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.yml - else - # Fallback to download from the raw github url - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -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.yml - 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.yml $PLANE_INSTALL_DIR/docker-compose.yml - exit 1 - fi - fi - - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -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 -H 'Cache-Control: no-cache, no-store' -s -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.yml $PLANE_INSTALL_DIR/docker-compose.yml - 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 - - updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH" - -} -function deployStack() { - # Check if docker compose file and env file exist - if [ ! -f "$DOCKER_FILE_PATH" ] || [ ! -f "$DOCKER_ENV_PATH" ]; then - echo "Configuration files not found" - echo "Downloading it now......" - APP_RELEASE=$(checkLatestRelease) - download - fi - if [ -z "$stack_name" ]; then - getStackName - fi - echo "Starting ${stack_name} stack..." - - # Pull envs - if [ -f "$DOCKER_ENV_PATH" ]; then - set -o allexport; source $DOCKER_ENV_PATH; set +o allexport; - else - echo "Environment file not found: $DOCKER_ENV_PATH" - exit 1 - fi - - # Deploy the stack - docker stack deploy -c $DOCKER_FILE_PATH $stack_name - - echo "Waiting for services to be deployed..." - sleep 10 - - # Check migrator service - local migrator_service=$(docker service ls --filter name=${stack_name}_migrator -q) - if [ -n "$migrator_service" ]; then - echo ">> Waiting for Data Migration to finish" - while docker service ls --filter name=${stack_name}_migrator | grep -q "running"; do - echo -n "." - sleep 1 - done - echo "" - - # Get the most recent container for the migrator service - local migrator_container=$(docker ps -a --filter name=${stack_name}_migrator --latest -q) - - if [ -n "$migrator_container" ]; then - # Get the exit code of the container - local exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container) - - if [ "$exit_code" != "0" ]; then - echo "Server failed to start ❌" - echo "Migration failed with exit code: $exit_code" - echo "Please check the logs for the 'migrator' service and resolve the issue(s)." - echo "Stop the services by running the command: ./swarm.sh stop" - exit 1 - else - echo " Data Migration completed successfully ✅" - fi - else - echo "Warning: Could not find migrator container to check exit status" - fi - fi - - # Check API service - local api_service=$(docker service ls --filter name=${stack_name}_api -q) - while docker service ls --filter name=${stack_name}_api | grep -q "running"; do - local running_container=$(docker ps --filter "name=${stack_name}_api" --filter "status=running" -q) - if [ -n "$running_container" ]; then - if docker container logs $running_container 2>/dev/null | grep -q "Application Startup Complete"; then - break - fi - fi - sleep 2 - done - - if [ -z "$api_service" ]; then - echo "Plane Server failed to start ❌" - echo "Please check the logs for the 'api' service and resolve the issue(s)." - echo "Stop the services by running the command: ./swarm.sh stop" - exit 1 - fi - echo " Plane Server started successfully ✅" - echo "" - echo " You can access the application at $WEB_URL" - echo "" -} - -function removeStack() { - if [ -z "$stack_name" ]; then - echo "Stack name not found" - exit 1 - fi - echo "Removing ${stack_name} stack..." - docker stack rm "$stack_name" - echo "Waiting for services to be removed..." - while docker stack ls | grep -q "$stack_name"; do - sleep 1 - done - sleep 20 - echo "Services stopped successfully ✅" -} - -function viewStatus() { - echo "Checking status of ${stack_name} stack..." - if [ -z "$stack_name" ]; then - echo "Stack name not found" - exit 1 - fi - docker stack ps "$stack_name" -} - -function redeployStack() { - removeStack - echo "ReDeploying ${stack_name} stack..." - deployStack -} - -function upgrade() { - - echo "Checking status of ${stack_name} stack..." - if [ -z "$stack_name" ]; then - echo "Stack name not found" - exit 1 - fi - - 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 - - # check if stack exists - echo "Upgrading ${stack_name} stack..." - - # check env file and take backup - if [ -f "$DOCKER_ENV_PATH" ]; then - cp "$DOCKER_ENV_PATH" "${DOCKER_ENV_PATH}.bak" - fi - - download - redeployStack -} - -function viewSpecificLogs() { - local service=$1 - - # Input validation - if [ -z "$service" ]; then - echo "Error: Please specify a service name" - return 1 - fi - - # Main loop for service logs - while true; do - # Get all running containers for the service - local running_containers=$(docker ps --filter "name=${stack_name}_${service}" --filter "status=running" -q) - - # If no running containers found, try service logs - if [ -z "$running_containers" ]; then - echo "No running containers found for ${stack_name}_${service}, checking service logs..." - if docker service inspect ${stack_name}_${service} >/dev/null 2>&1; then - echo "Press Ctrl+C or 'q' to exit logs" - docker service logs ${stack_name}_${service} -f - break - else - echo "Error: No running containers or services found for ${stack_name}_${service}" - return 1 - fi - return - fi - - # If multiple containers are running, let user choose - if [ $(echo "$running_containers" | grep -v '^$' | wc -l) -gt 1 ]; then - clear - echo "Multiple containers found for ${stack_name}_${service}:" - local i=1 - # Use regular arrays instead of associative arrays - container_ids=() - container_names=() - - while read -r container_id; do - if [ -n "$container_id" ]; then - local container_name=$(docker inspect --format '{{.Name}}' "$container_id" | sed 's/\///') - container_ids[$i]=$container_id - container_names[$i]=$container_name - echo "[$i] ${container_names[$i]} (${container_ids[$i]})" - i=$((i+1)) - fi - done <<< "$running_containers" - - echo -e "\nPlease select a container number:" - read -r selection - - if [[ "$selection" =~ ^[0-9]+$ ]] && [ -n "${container_ids[$selection]}" ]; then - local selected_container=${container_ids[$selection]} - clear - echo "Showing logs for container: ${container_names[$selection]}" - echo "Press Ctrl+C or 'q' to return to container selection" - - # Start watching logs in the background - docker container logs -f "$selected_container" & - local log_pid=$! - - while true; do - read -r -n 1 input - if [[ $input == "q" ]]; then - kill $log_pid 2>/dev/null - wait $log_pid 2>/dev/null - break - fi - done - clear - else - echo "Error: Invalid selection" - sleep 2 - fi - else - # Single container case - local container_name=$(docker inspect --format '{{.Name}}' "$running_containers" | sed 's/\///') - echo "Showing logs for container: $container_name" - echo "Press Ctrl+C or 'q' to exit logs" - docker container logs -f "$running_containers" & - local log_pid=$! - - while true; do - read -r -n 1 input - if [[ $input == "q" ]]; then - kill $log_pid 2>/dev/null - wait $log_pid 2>/dev/null - break - fi - done - break - fi - done -} - -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 askForAction() { - # Rest of askForAction remains the same but use $stack_name instead of $STACK_NAME - local DEFAULT_ACTION=$1 - - if [ -z "$DEFAULT_ACTION" ]; then - echo - echo "Select an Action you want to perform:" - echo " 1) Deploy Stack" - echo " 2) Remove Stack" - echo " 3) View Stack Status" - echo " 4) Redeploy Stack" - echo " 5) Upgrade" - echo " 6) View Logs" - echo " 7) Exit" - echo - read -p "Action [3]: " ACTION - until [[ -z "$ACTION" || "$ACTION" =~ ^[1-6]$ ]]; do - echo "$ACTION: invalid selection." - read -p "Action [3]: " ACTION - done - - if [ -z "$ACTION" ]; then - ACTION=3 - fi - echo - fi - - if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "deploy" ]; then - deployStack - elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "remove" ]; then - removeStack - elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "status" ]; then - viewStatus - elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "redeploy" ]; then - redeployStack - elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ]; then - upgrade - elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ]; then - viewLogs "$@" - elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "exit" ]; then - exit 0 - else - echo "INVALID ACTION SUPPLIED" - fi -} - -# Initialize stack name at script start - -if [ -z "$stack_name" ]; then - readStackName -fi - -# Sync environment variables -if [ -f "$DOCKER_ENV_PATH" ]; then - DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH") - APP_RELEASE=$(getEnvValue "APP_RELEASE" "$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 -fi - - -# Main execution -print_header -askForAction "$@" diff --git a/plane/code/install.sh b/plane/setup.sh similarity index 95% rename from plane/code/install.sh rename to plane/setup.sh index 9f0065f..3460d35 100755 --- a/plane/code/install.sh +++ b/plane/setup.sh @@ -2,14 +2,14 @@ BRANCH=${BRANCH:-master} SCRIPT_DIR=$PWD -SERVICE_FOLDER=plane-app +SERVICE_FOLDER=plane-code 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/deploy/selfhost" +export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deployments/cli/community" CPU_ARCH=$(uname -m) OS_NAME=$(uname) @@ -57,7 +57,7 @@ function spinner() { function checkLatestRelease(){ echo "Checking for the latest release..." >&2 - local latest_release=$(curl -s https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g') + local latest_release=$(curl -fsSL 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 @@ -196,7 +196,7 @@ function buildYourOwnImage(){ 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/deploy/selfhost/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml" + cp "$PLANE_TEMP_CODE_DIR/deployments/cli/community/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml" cd "$PLANE_TEMP_CODE_DIR" || exit @@ -247,7 +247,7 @@ function download() { mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml fi - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)") + RESPONSE=$(curl -fsSL -H 'Cache-Control: no-cache, no-store' -s -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://') @@ -255,7 +255,7 @@ function download() { echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml else # Fallback to download from the raw github url - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)") + RESPONSE=$(curl -fsSL -H 'Cache-Control: no-cache, no-store' -s -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://') @@ -269,7 +269,7 @@ function download() { fi fi - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)") + RESPONSE=$(curl -fsSL -H 'Cache-Control: no-cache, no-store' -s -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://') @@ -277,7 +277,7 @@ function download() { echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env else # Fallback to download from the raw github url - RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)") + RESPONSE=$(curl -fsSL -H 'Cache-Control: no-cache, no-store' -s -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://') diff --git a/plane/update.js b/plane/update.js index a5ef43f..51317d8 100644 --- a/plane/update.js +++ b/plane/update.js @@ -1,19 +1,10 @@ import utils from "../utils.js"; -console.log( - "Plan file structure has changed. This script needs to be reworked." -); - -process.exit(); - -await utils.cloneOrPullRepo({ - repo: "https://github.com/makeplane/plane.git", - path: "./repo", - branch: "preview", -}); - -await utils.copyDir("./repo/deploy/selfhost", "./code"); -await utils.renameFile("./code/variables.env", "./code/.env.example"); +await utils.removeDir("./code"); +await utils.renameDir("./plane-code", "./code"); +await utils.removeDir("./code/archive"); +await utils.renameFile("./code/plane.env", "./code/.env.example"); +await utils.renameFile("./code/docker-compose.yaml", "./code/docker-compose.yml"); await utils.removeContainerNames("./code/docker-compose.yml"); await utils.removePorts("./code/docker-compose.yml"); diff --git a/twenty/README.md b/twenty/README.md deleted file mode 100644 index 1fa12fb..0000000 --- a/twenty/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Twenty - -- copied from https://github.com/twentyhq/twenty -- removed `ports` diff --git a/twenty/code/.env.example b/twenty/code/.env.example deleted file mode 100644 index d538814..0000000 --- a/twenty/code/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -TAG=latest - -#PG_DATABASE_USER=postgres -#PG_DATABASE_PASSWORD=replace_me_with_a_strong_password_without_special_characters -#PG_DATABASE_HOST=db -#PG_DATABASE_PORT=5432 -REDIS_URL=redis://redis:6379 - -SERVER_URL=https://$(PRIMARY_DOMAIN) - -# Use openssl rand -base64 32 for each secret -APP_SECRET=replace_me_with_a_random_string - -STORAGE_TYPE=local - -# STORAGE_S3_REGION=eu-west3 -# STORAGE_S3_NAME=my-bucket -# STORAGE_S3_ENDPOINT= diff --git a/twenty/code/Makefile b/twenty/code/Makefile deleted file mode 100644 index bdda023..0000000 --- a/twenty/code/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -# Makefile for building Twenty CRM docker images. -# Set the tag and/or target build platform using make command-line variables assignments. -# -# Optional make variables: -# PLATFORM - defaults to 'linux/amd64' -# TAG - defaults to 'latest' -# -# Example: make -# Example: make PLATFORM=linux/aarch64 TAG=my-tag - -PLATFORM ?= linux/amd64 -TAG ?= latest - -prod-build: - @cd ../.. && docker build -f ./packages/twenty-docker/twenty/Dockerfile --platform $(PLATFORM) --tag twenty:$(TAG) . && cd - - -prod-run: - @docker run -d -p 3000:3000 --name twenty twenty:$(TAG) - -prod-postgres-run: - @docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres:$(TAG) - -prod-website-build: - @cd ../.. && docker build -f ./packages/twenty-docker/twenty-website/Dockerfile --platform $(PLATFORM) --tag twenty-website:$(TAG) . && cd - - -prod-website-run: - @docker run -d -p 3000:3000 --name twenty-website twenty-website:$(TAG) diff --git a/twenty/code/docker-compose.yml b/twenty/code/docker-compose.yml deleted file mode 100644 index 55dfc6d..0000000 --- a/twenty/code/docker-compose.yml +++ /dev/null @@ -1,126 +0,0 @@ -name: twenty - -services: - server: - image: twentycrm/twenty:${TAG:-latest} - volumes: - - server-local-data:/app/packages/twenty-server/.local-storage - environment: - NODE_PORT: 3000 - PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default - SERVER_URL: ${SERVER_URL} - REDIS_URL: ${REDIS_URL:-redis://redis:6379} - DISABLE_DB_MIGRATIONS: ${DISABLE_DB_MIGRATIONS} - DISABLE_CRON_JOBS_REGISTRATION: ${DISABLE_CRON_JOBS_REGISTRATION} - - STORAGE_TYPE: ${STORAGE_TYPE} - STORAGE_S3_REGION: ${STORAGE_S3_REGION} - STORAGE_S3_NAME: ${STORAGE_S3_NAME} - STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} - - APP_SECRET: ${APP_SECRET:-replace_me_with_a_random_string} - # MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED} - # CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED} - # AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID} - # AUTH_GOOGLE_CLIENT_SECRET: ${AUTH_GOOGLE_CLIENT_SECRET} - # AUTH_GOOGLE_CALLBACK_URL: ${AUTH_GOOGLE_CALLBACK_URL} - # AUTH_GOOGLE_APIS_CALLBACK_URL: ${AUTH_GOOGLE_APIS_CALLBACK_URL} - - # CALENDAR_PROVIDER_MICROSOFT_ENABLED: ${CALENDAR_PROVIDER_MICROSOFT_ENABLED} - # MESSAGING_PROVIDER_MICROSOFT_ENABLED: ${MESSAGING_PROVIDER_MICROSOFT_ENABLED} - # AUTH_MICROSOFT_ENABLED: ${AUTH_MICROSOFT_ENABLED} - # AUTH_MICROSOFT_CLIENT_ID: ${AUTH_MICROSOFT_CLIENT_ID} - # AUTH_MICROSOFT_CLIENT_SECRET: ${AUTH_MICROSOFT_CLIENT_SECRET} - # AUTH_MICROSOFT_CALLBACK_URL: ${AUTH_MICROSOFT_CALLBACK_URL} - # AUTH_MICROSOFT_APIS_CALLBACK_URL: ${AUTH_MICROSOFT_APIS_CALLBACK_URL} - - # EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com} - # EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"} - # EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com} - # EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp} - # EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com} - # EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465} - # EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-} - # EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-} - - depends_on: - db: - condition: service_healthy - healthcheck: - test: curl --fail http://localhost:3000/healthz - interval: 5s - timeout: 5s - retries: 20 - restart: always - - worker: - image: twentycrm/twenty:${TAG:-latest} - volumes: - - server-local-data:/app/packages/twenty-server/.local-storage - command: [ "yarn", "worker:prod" ] - environment: - PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default - SERVER_URL: ${SERVER_URL} - REDIS_URL: ${REDIS_URL:-redis://redis:6379} - DISABLE_DB_MIGRATIONS: "true" # it already runs on the server - DISABLE_CRON_JOBS_REGISTRATION: "true" # it already runs on the server - - STORAGE_TYPE: ${STORAGE_TYPE} - STORAGE_S3_REGION: ${STORAGE_S3_REGION} - STORAGE_S3_NAME: ${STORAGE_S3_NAME} - STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} - - APP_SECRET: ${APP_SECRET:-replace_me_with_a_random_string} - # MESSAGING_PROVIDER_GMAIL_ENABLED: ${MESSAGING_PROVIDER_GMAIL_ENABLED} - # CALENDAR_PROVIDER_GOOGLE_ENABLED: ${CALENDAR_PROVIDER_GOOGLE_ENABLED} - # AUTH_GOOGLE_CLIENT_ID: ${AUTH_GOOGLE_CLIENT_ID} - # AUTH_GOOGLE_CLIENT_SECRET: ${AUTH_GOOGLE_CLIENT_SECRET} - # AUTH_GOOGLE_CALLBACK_URL: ${AUTH_GOOGLE_CALLBACK_URL} - # AUTH_GOOGLE_APIS_CALLBACK_URL: ${AUTH_GOOGLE_APIS_CALLBACK_URL} - - # CALENDAR_PROVIDER_MICROSOFT_ENABLED: ${CALENDAR_PROVIDER_MICROSOFT_ENABLED} - # MESSAGING_PROVIDER_MICROSOFT_ENABLED: ${MESSAGING_PROVIDER_MICROSOFT_ENABLED} - # AUTH_MICROSOFT_ENABLED: ${AUTH_MICROSOFT_ENABLED} - # AUTH_MICROSOFT_CLIENT_ID: ${AUTH_MICROSOFT_CLIENT_ID} - # AUTH_MICROSOFT_CLIENT_SECRET: ${AUTH_MICROSOFT_CLIENT_SECRET} - # AUTH_MICROSOFT_CALLBACK_URL: ${AUTH_MICROSOFT_CALLBACK_URL} - # AUTH_MICROSOFT_APIS_CALLBACK_URL: ${AUTH_MICROSOFT_APIS_CALLBACK_URL} - - # EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com} - # EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"} - # EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com} - # EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp} - # EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com} - # EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465} - # EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-} - # EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-} - - depends_on: - db: - condition: service_healthy - server: - condition: service_healthy - restart: always - - db: - image: postgres:16 - volumes: - - db-data:/var/lib/postgresql/data - environment: - POSTGRES_USER: ${PG_DATABASE_USER:-postgres} - POSTGRES_PASSWORD: ${PG_DATABASE_PASSWORD:-postgres} - healthcheck: - test: pg_isready -U ${PG_DATABASE_USER:-postgres} -h localhost -d postgres - interval: 5s - timeout: 5s - retries: 10 - restart: always - - redis: - image: redis - restart: always - command: [ "--maxmemory-policy", "noeviction" ] - -volumes: - db-data: - server-local-data: diff --git a/twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml b/twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml deleted file mode 100644 index bb4279e..0000000 --- a/twenty/code/grafana/provisioning/datasources/clickhouse-datasource.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: 1 - -datasources: - - name: ClickHouse - type: grafana-clickhouse-datasource - uid: clickhouse_datasource - jsonData: - server: twenty_clickhouse - defaultDatabase: twenty_dev - port: 9000 - protocol: native - tlsSkipVerify: true - username: default - secureJsonData: - password: devPassword \ No newline at end of file diff --git a/twenty/code/k8s/README.md b/twenty/code/k8s/README.md deleted file mode 100644 index d538d21..0000000 --- a/twenty/code/k8s/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# README -DISCLAIMER: The k8s and podman deployments are not maintained by the core team. -These files are provided and maintained by the community. Twenty core team -maintains support for docker deployment. - -## Overview - -This repository contains Kubernetes manifests and Terraform files to help you deploy and manage the TwentyCRM application. The files are located in the `packages/twenty-docker/k8s` directory. - -## Prerequisites - -Before using these files, ensure you have the following installed and configured on your system: - -- Kubernetes cluster (e.g., Minikube, EKS, GKE) -- kubectl -- Terraform -- Docker - -## Setup Instructions - -### Step 1: Clone the Repository - -Clone the repository to your local machine: - -``` bash -git clone https://github.com/twentyhq/twenty.git -cd twentycrm/packages/twenty-docker/k8s -``` - -### Step 2: Customize the Manifests and Terraform Files - -**Important:** These files require customization for your specific implementation. Update the placeholders and configurations according to your environment and requirements. - -### Step 3: Deploy with Terraform - -1. Navigate to the Terraform directory: - - ```bash - cd terraform - ``` - -2. Initialize Terraform: - - ```bash - terraform init - ``` - -3. Plan the deployment: - - ```bash - terraform plan - ``` - -4. Apply the deployment: - - ```bash - terraform apply - ``` - -## OR - -### Step 3: Deploy with Kubernetes Manifests - -1. Navigate to the Kubernetes manifests directory: - - ```bash - cd ../k8s - ``` - -2. Create Server Secret - - ``` bash - kubectl create secret generic -n twentycrm tokens --from-literal accessToken=changeme --from-literal loginToken="changeme" --from-literal refreshToken="changeme" --from-literal fileToken="changeme" - ``` - -3. Apply the manifests: - - ```bash - kubectl apply -f . - ``` - -## Customization - -### Kubernetes Manifests - -- **Namespace:** Update the `namespace` in the manifests as needed. -- **Resource Limits:** Adjust the resource limits and requests according to your application's requirements. -- **Environment Variables:** Configure server tokens in the `Secret` command above. - -### Terraform Files - -- **Variables:** Update the variables in the `variables.tf` file to match your environment. -- **Locals:** Update the locals in the `main.tf` file to match your environment. -- **Providers:** Ensure the provider configurations (e.g., AWS, GCP) are correct for your setup. -- **Resources:** Modify the resource definitions as needed to fit your infrastructure. - -## Troubleshooting - -### Common Issues - -- **Connectivity:** Ensure your Kubernetes cluster is accessible and configured correctly. -- **Permissions:** Verify that you have the necessary permissions to deploy resources in your cloud provider. -- **Resource Limits:** Adjust resource limits if you encounter issues related to insufficient resources. - -### Logs and Debugging - -- Use `kubectl logs` to check the logs of your Kubernetes pods. -- Use `terraform show` and `terraform state` to inspect your Terraform state and configurations. - -## Conclusion - -This setup provides a basic structure for deploying the TwentyCRM application using Kubernetes and Terraform. Ensure you thoroughly customize the manifests and Terraform files to suit your specific needs. For any issues or questions, please refer to the official documentation of Kubernetes and Terraform or seek support from your cloud provider. - ---- - -Feel free to contribute and improve this repository by submitting pull requests or opening issues. Happy deploying! diff --git a/twenty/code/k8s/manifests/deployment-db.yaml b/twenty/code/k8s/manifests/deployment-db.yaml deleted file mode 100644 index c797972..0000000 --- a/twenty/code/k8s/manifests/deployment-db.yaml +++ /dev/null @@ -1,58 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: twentycrm-db - name: twentycrm-db - namespace: twentycrm -spec: - progressDeadlineSeconds: 600 - replicas: 1 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - selector: - matchLabels: - app: twentycrm-db - template: - metadata: - labels: - app: twentycrm-db - spec: - volumes: - - name: twentycrm-db-data - persistentVolumeClaim: - claimName: twentycrm-db-pvc - containers: - - name: twentycrm - image: twentycrm/twenty-postgres-spilo:latest - imagePullPolicy: Always - env: - - name: PGUSER_SUPERUSER - value: "postgres" - - name: PGPASSWORD_SUPERUSER - value: "postgres" - - name: SPILO_PROVIDER - value: "local" - - name: ALLOW_NOSSL - value: "true" - ports: - - containerPort: 5432 - name: tcp - protocol: TCP - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "1024Mi" - cpu: "1000m" - stdin: true - tty: true - volumeMounts: - - mountPath: /home/postgres/pgdata - name: twentycrm-db-data - dnsPolicy: ClusterFirst - restartPolicy: Always diff --git a/twenty/code/k8s/manifests/deployment-redis.yaml b/twenty/code/k8s/manifests/deployment-redis.yaml deleted file mode 100644 index bd7aaa3..0000000 --- a/twenty/code/k8s/manifests/deployment-redis.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: twentycrm-redis - name: twentycrm-redis - namespace: twentycrm -spec: - progressDeadlineSeconds: 600 - replicas: 1 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - selector: - matchLabels: - app: twentycrm-redis - template: - metadata: - labels: - app: twentycrm-redis - spec: - containers: - - name: redis - image: redis/redis-stack-server:latest - imagePullPolicy: Always - args: ["--maxmemory-policy", "noeviction"] - env: - - name: PORT - value: "6379" - ports: - - containerPort: 6379 - name: redis - protocol: TCP - resources: - requests: - memory: "1024Mi" - cpu: "250m" - limits: - memory: "2048Mi" - cpu: "500m" - - dnsPolicy: ClusterFirst - restartPolicy: Always diff --git a/twenty/code/k8s/manifests/deployment-server.yaml b/twenty/code/k8s/manifests/deployment-server.yaml deleted file mode 100644 index 9d21993..0000000 --- a/twenty/code/k8s/manifests/deployment-server.yaml +++ /dev/null @@ -1,76 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: twentycrm-server - name: twentycrm-server - namespace: twentycrm -spec: - progressDeadlineSeconds: 600 - replicas: 1 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - selector: - matchLabels: - app: twentycrm-server - template: - metadata: - labels: - app: twentycrm-server - spec: - volumes: - - name: twentycrm-server-data - persistentVolumeClaim: - claimName: twentycrm-server-pvc - - name: twentycrm-docker-data - persistentVolumeClaim: - claimName: twentycrm-docker-data-pvc - containers: - - name: twentycrm - image: twentycrm/twenty:latest - imagePullPolicy: Always - env: - - name: NODE_PORT - value: 3000 - - name: SERVER_URL - value: "https://crm.example.com:443" - - name: "PG_DATABASE_URL" - value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default" - - name: "REDIS_URL" - value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379" - - name: SIGN_IN_PREFILLED - value: "false" - - name: STORAGE_TYPE - value: "local" - - name: "ACCESS_TOKEN_EXPIRES_IN" - value: "7d" - - name: "LOGIN_TOKEN_EXPIRES_IN" - value: "1h" - - name: APP_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: accessToken - ports: - - containerPort: 3000 - name: http-tcp - protocol: TCP - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "1024Mi" - cpu: "1000m" - stdin: true - tty: true - volumeMounts: - - mountPath: /app/docker-data - name: twentycrm-docker-data - - mountPath: /app/packages/twenty-server/.local-storage - name: twentycrm-server-data - dnsPolicy: ClusterFirst - restartPolicy: Always diff --git a/twenty/code/k8s/manifests/deployment-worker.yaml b/twenty/code/k8s/manifests/deployment-worker.yaml deleted file mode 100644 index fbee277..0000000 --- a/twenty/code/k8s/manifests/deployment-worker.yaml +++ /dev/null @@ -1,57 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: twentycrm-worker - name: twentycrm-worker - namespace: twentycrm -spec: - progressDeadlineSeconds: 600 - replicas: 1 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - selector: - matchLabels: - app: twentycrm-worker - template: - metadata: - labels: - app: twentycrm-worker - spec: - containers: - - name: twentycrm - image: twentycrm/twenty:latest - imagePullPolicy: Always - env: - - name: SERVER_URL - value: "https://crm.example.com:443" - - name: PG_DATABASE_URL - value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default" - - name: DISABLE_DB_MIGRATIONS - value: "false" # it already runs on the server - - name: STORAGE_TYPE - value: "local" - - name: "REDIS_URL" - value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379" - - name: APP_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: accessToken - command: - - yarn - - worker:prod - resources: - requests: - memory: "1024Mi" - cpu: "250m" - limits: - memory: "2048Mi" - cpu: "1000m" - stdin: true - tty: true - dnsPolicy: ClusterFirst - restartPolicy: Always diff --git a/twenty/code/k8s/manifests/ingress.yaml b/twenty/code/k8s/manifests/ingress.yaml deleted file mode 100644 index 0bbae11..0000000 --- a/twenty/code/k8s/manifests/ingress.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: twentycrm - namespace: twentycrm - annotations: - nginx.ingress.kubernetes.io/configuration-snippet: | - more_set_headers "X-Forwarded-For $http_x_forwarded_for"; - nginx.ingress.kubernetes.io/force-ssl-redirect: "false" - kubernetes.io/ingress.class: "nginx" - nginx.ingress.kubernetes.io/backend-protocol: "HTTP" -spec: - ingressClassName: nginx - rules: - - host: crm.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: twentycrm-server - port: - name: http-tcp diff --git a/twenty/code/k8s/manifests/pv-db.yaml b/twenty/code/k8s/manifests/pv-db.yaml deleted file mode 100644 index 9caa4ca..0000000 --- a/twenty/code/k8s/manifests/pv-db.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: twentycrm-db-pv -spec: - storageClassName: default - capacity: - storage: 10Gi - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain diff --git a/twenty/code/k8s/manifests/pv-docker-data.yaml b/twenty/code/k8s/manifests/pv-docker-data.yaml deleted file mode 100644 index 95fc52a..0000000 --- a/twenty/code/k8s/manifests/pv-docker-data.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: twentycrm-docker-data-pv -spec: - storageClassName: default - capacity: - storage: 100Mi - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain diff --git a/twenty/code/k8s/manifests/pv-server.yaml b/twenty/code/k8s/manifests/pv-server.yaml deleted file mode 100644 index 721de7d..0000000 --- a/twenty/code/k8s/manifests/pv-server.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: twentycrm-server-pv - namespace: twentycrm -spec: - storageClassName: default - capacity: - storage: 10Gi - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain diff --git a/twenty/code/k8s/manifests/pvc-db.yaml b/twenty/code/k8s/manifests/pvc-db.yaml deleted file mode 100644 index 146596e..0000000 --- a/twenty/code/k8s/manifests/pvc-db.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: twentycrm-db-pvc - namespace: twentycrm -spec: - storageClassName: default - volumeName: twentycrm-db-pv - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi diff --git a/twenty/code/k8s/manifests/pvc-docker-data.yaml b/twenty/code/k8s/manifests/pvc-docker-data.yaml deleted file mode 100644 index 12dd071..0000000 --- a/twenty/code/k8s/manifests/pvc-docker-data.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: twentycrm-docker-data-pvc - namespace: twentycrm -spec: - storageClassName: default - volumeName: twentycrm-docker-data-pv - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 100Mi diff --git a/twenty/code/k8s/manifests/pvc-server.yaml b/twenty/code/k8s/manifests/pvc-server.yaml deleted file mode 100644 index f265057..0000000 --- a/twenty/code/k8s/manifests/pvc-server.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: twentycrm-server-pvc - namespace: twentycrm -spec: - storageClassName: default - volumeName: twentycrm-server-pv - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi diff --git a/twenty/code/k8s/manifests/service-db.yaml b/twenty/code/k8s/manifests/service-db.yaml deleted file mode 100644 index 89dbd14..0000000 --- a/twenty/code/k8s/manifests/service-db.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: twentycrm-db - namespace: twentycrm -spec: - internalTrafficPolicy: Cluster - ports: - - port: 5432 - protocol: TCP - targetPort: 5432 - selector: - app: twentycrm-db - sessionAffinity: ClientIP - sessionAffinityConfig: - clientIP: - timeoutSeconds: 10800 - type: ClusterIP diff --git a/twenty/code/k8s/manifests/service-redis.yaml b/twenty/code/k8s/manifests/service-redis.yaml deleted file mode 100644 index 49f5088..0000000 --- a/twenty/code/k8s/manifests/service-redis.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: twentycrm-redis - namespace: twentycrm -spec: - internalTrafficPolicy: Cluster - ports: - - port: 6379 - protocol: TCP - targetPort: 6379 - selector: - app: twentycrm-redis - sessionAffinity: ClientIP - sessionAffinityConfig: - clientIP: - timeoutSeconds: 10800 - type: ClusterIP diff --git a/twenty/code/k8s/manifests/service-server.yaml b/twenty/code/k8s/manifests/service-server.yaml deleted file mode 100644 index b45b28f..0000000 --- a/twenty/code/k8s/manifests/service-server.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: twentycrm-server - namespace: twentycrm -spec: - internalTrafficPolicy: Cluster - ports: - - name: http-tcp - port: 3000 - protocol: TCP - targetPort: 3000 - selector: - app: twentycrm-server - sessionAffinity: ClientIP - sessionAffinityConfig: - clientIP: - timeoutSeconds: 10800 - type: ClusterIP diff --git a/twenty/code/k8s/terraform/.terraform-docs.yml b/twenty/code/k8s/terraform/.terraform-docs.yml deleted file mode 100644 index 792c543..0000000 --- a/twenty/code/k8s/terraform/.terraform-docs.yml +++ /dev/null @@ -1,48 +0,0 @@ -formatter: "markdown table" # this is required - -version: "" - -header-from: main.tf - -recursive: - enabled: false - path: modules - -output: - file: "README.md" - mode: inject - template: |- - - # TwentyCRM Terraform Docs - - This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure, and use visit their website. - - To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs -c `./.terraform-docs.yml .` - - To make configuration changes to how this doc is generated, see `./.terraform-docs.yml` - - {{ .Content }} - - -output-values: - enabled: false - from: "outputs.tf" - -sort: - enabled: true - by: required - -settings: - anchor: true - color: true - default: true - description: true - escape: true - hide-empty: true - html: true - indent: 2 - lockfile: true - read-comments: true - required: true - sensitive: true - type: true diff --git a/twenty/code/k8s/terraform/README.md b/twenty/code/k8s/terraform/README.md deleted file mode 100644 index 32facfd..0000000 --- a/twenty/code/k8s/terraform/README.md +++ /dev/null @@ -1,73 +0,0 @@ - -# TwentyCRM Terraform Docs - -This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure, and use visit their website. - -To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs -c `./.terraform-docs.yml .` - -To make configuration changes to how this doc is generated, see `./.terraform-docs.yml` - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.9.2 | -| [kubernetes](#requirement\_kubernetes) | >= 2.32.0 | -| [random](#requirement\_random) | >= 3.6.3 | - -## Providers - -| Name | Version | -|------|---------| -| [kubernetes](#provider\_kubernetes) | >= 2.32.0 | -| [random](#provider\_random) | >= 3.6.3 | - -## Resources - -| Name | Type | -|------|------| -| [kubernetes_deployment.twentycrm_db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | -| [kubernetes_deployment.twentycrm_redis](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | -| [kubernetes_deployment.twentycrm_server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | -| [kubernetes_deployment.twentycrm_worker](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | -| [kubernetes_ingress.twentycrm](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/ingress) | resource | -| [kubernetes_namespace.twentycrm](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | -| [kubernetes_persistent_volume.db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource | -| [kubernetes_persistent_volume.docker_data](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource | -| [kubernetes_persistent_volume.server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource | -| [kubernetes_persistent_volume_claim.db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource | -| [kubernetes_persistent_volume_claim.docker_data](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource | -| [kubernetes_persistent_volume_claim.server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource | -| [kubernetes_secret.twentycrm_tokens](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | -| [kubernetes_service.twentycrm_db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | -| [kubernetes_service.twentycrm_redis](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | -| [kubernetes_service.twentycrm_server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | -| [random_bytes.this](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/bytes) | resource | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [twentycrm\_app\_hostname](#input\_twentycrm\_app\_hostname) | The protocol, DNS fully qualified hostname, and port used to access TwentyCRM in your environment. Ex: https://crm.example.com:443 | `string` | n/a | yes | -| [twentycrm\_pgdb\_admin\_password](#input\_twentycrm\_pgdb\_admin\_password) | TwentyCRM password for postgres database. | `string` | n/a | yes | -| [twentycrm\_app\_name](#input\_twentycrm\_app\_name) | A friendly name prefix to use for every component deployed. | `string` | `"twentycrm"` | no | -| [twentycrm\_db\_image](#input\_twentycrm\_db\_image) | TwentyCRM image for database deployment. This defaults to latest. | `string` | `"twentycrm/twenty-postgres-spilo:latest"` | no | -| [twentycrm\_db\_pv\_capacity](#input\_twentycrm\_db\_pv\_capacity) | Storage capacity provisioned for database persistent volume. | `string` | `"10Gi"` | no | -| [twentycrm\_db\_pv\_path](#input\_twentycrm\_db\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no | -| [twentycrm\_db\_pvc\_requests](#input\_twentycrm\_db\_pvc\_requests) | Storage capacity reservation for database persistent volume claim. | `string` | `"10Gi"` | no | -| [twentycrm\_db\_replicas](#input\_twentycrm\_db\_replicas) | Number of replicas for the TwentyCRM database deployment. This defaults to 1. | `number` | `1` | no | -| [twentycrm\_docker\_data\_mount\_path](#input\_twentycrm\_docker\_data\_mount\_path) | TwentyCRM mount path for servers application data. Defaults to '/app/docker-data'. | `string` | `"/app/docker-data"` | no | -| [twentycrm\_docker\_data\_pv\_capacity](#input\_twentycrm\_docker\_data\_pv\_capacity) | Storage capacity provisioned for server persistent volume. | `string` | `"10Gi"` | no | -| [twentycrm\_docker\_data\_pv\_path](#input\_twentycrm\_docker\_data\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no | -| [twentycrm\_docker\_data\_pvc\_requests](#input\_twentycrm\_docker\_data\_pvc\_requests) | Storage capacity reservation for server persistent volume claim. | `string` | `"10Gi"` | no | -| [twentycrm\_namespace](#input\_twentycrm\_namespace) | Namespace for all TwentyCRM resources | `string` | `"twentycrm"` | no | -| [twentycrm\_redis\_image](#input\_twentycrm\_redis\_image) | TwentyCRM image for Redis deployment. This defaults to latest. | `string` | `"redis/redis-stack-server:latest"` | no | -| [twentycrm\_redis\_replicas](#input\_twentycrm\_redis\_replicas) | Number of replicas for the TwentyCRM Redis deployment. This defaults to 1. | `number` | `1` | no | -| [twentycrm\_server\_data\_mount\_path](#input\_twentycrm\_server\_data\_mount\_path) | TwentyCRM mount path for servers application data. Defaults to '/app/packages/twenty-server/.local-storage'. | `string` | `"/app/packages/twenty-server/.local-storage"` | no | -| [twentycrm\_server\_image](#input\_twentycrm\_server\_image) | TwentyCRM server image for the server deployment. This defaults to latest. This value is also used for the workers image. | `string` | `"twentycrm/twenty:latest"` | no | -| [twentycrm\_server\_pv\_capacity](#input\_twentycrm\_server\_pv\_capacity) | Storage capacity provisioned for server persistent volume. | `string` | `"10Gi"` | no | -| [twentycrm\_server\_pv\_path](#input\_twentycrm\_server\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no | -| [twentycrm\_server\_pvc\_requests](#input\_twentycrm\_server\_pvc\_requests) | Storage capacity reservation for server persistent volume claim. | `string` | `"10Gi"` | no | -| [twentycrm\_server\_replicas](#input\_twentycrm\_server\_replicas) | Number of replicas for the TwentyCRM server deployment. This defaults to 1. | `number` | `1` | no | -| [twentycrm\_worker\_replicas](#input\_twentycrm\_worker\_replicas) | Number of replicas for the TwentyCRM worker deployment. This defaults to 1. | `number` | `1` | no | - diff --git a/twenty/code/k8s/terraform/deployment-db.tf b/twenty/code/k8s/terraform/deployment-db.tf deleted file mode 100644 index 62c61a2..0000000 --- a/twenty/code/k8s/terraform/deployment-db.tf +++ /dev/null @@ -1,87 +0,0 @@ -resource "kubernetes_deployment" "twentycrm_db" { - metadata { - name = "${var.twentycrm_app_name}-db" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - labels = { - app = "${var.twentycrm_app_name}-db" - } - } - - spec { - replicas = var.twentycrm_db_replicas - selector { - match_labels = { - app = "${var.twentycrm_app_name}-db" - } - } - - strategy { - type = "RollingUpdate" - rolling_update { - max_surge = "1" - max_unavailable = "1" - } - } - - template { - metadata { - labels = { - app = "${var.twentycrm_app_name}-db" - } - } - - spec { - container { - image = var.twentycrm_db_image - name = var.twentycrm_app_name - stdin = true - tty = true - security_context { - allow_privilege_escalation = true - } - - env { - name = "POSTGRES_PASSWORD" - value = var.twentycrm_pgdb_admin_password - } - env { - name = "BITNAMI_DEBUG" - value = true - } - - port { - container_port = 5432 - protocol = "TCP" - } - - resources { - requests = { - cpu = "250m" - memory = "256Mi" - } - limits = { - cpu = "1000m" - memory = "1024Mi" - } - } - - volume_mount { - name = "db-data" - mount_path = "/bitnami/postgresql" - } - } - - volume { - name = "db-data" - - persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim.db.metadata.0.name - } - } - - dns_policy = "ClusterFirst" - restart_policy = "Always" - } - } - } -} diff --git a/twenty/code/k8s/terraform/deployment-redis.tf b/twenty/code/k8s/terraform/deployment-redis.tf deleted file mode 100644 index d867dac..0000000 --- a/twenty/code/k8s/terraform/deployment-redis.tf +++ /dev/null @@ -1,60 +0,0 @@ -resource "kubernetes_deployment" "twentycrm_redis" { - metadata { - name = "${var.twentycrm_app_name}-redis" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - - labels = { - app = "${var.twentycrm_app_name}-redis" - } - } - - spec { - replicas = var.twentycrm_redis_replicas - selector { - match_labels = { - app = "${var.twentycrm_app_name}-redis" - } - } - - strategy { - type = "RollingUpdate" - rolling_update { - max_surge = "1" - max_unavailable = "1" - } - } - - template { - metadata { - labels = { - app = "${var.twentycrm_app_name}-redis" - } - } - - spec { - container { - image = var.twentycrm_redis_image - name = "redis" - - port { - container_port = 6379 - protocol = "TCP" - } - - resources { - requests = { - cpu = "250m" - memory = "1024Mi" - } - limits = { - cpu = "500m" - memory = "2048Mi" - } - } - } - dns_policy = "ClusterFirst" - restart_policy = "Always" - } - } - } -} diff --git a/twenty/code/k8s/terraform/deployment-server.tf b/twenty/code/k8s/terraform/deployment-server.tf deleted file mode 100644 index a8bb43b..0000000 --- a/twenty/code/k8s/terraform/deployment-server.tf +++ /dev/null @@ -1,138 +0,0 @@ -resource "kubernetes_deployment" "twentycrm_server" { - metadata { - name = "${var.twentycrm_app_name}-server" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - labels = { - app = "${var.twentycrm_app_name}-server" - } - } - - spec { - replicas = var.twentycrm_server_replicas - selector { - match_labels = { - app = "${var.twentycrm_app_name}-server" - } - } - - strategy { - type = "RollingUpdate" - rolling_update { - max_surge = "1" - max_unavailable = "1" - } - } - - template { - metadata { - labels = { - app = "${var.twentycrm_app_name}-server" - } - } - - spec { - container { - image = var.twentycrm_server_image - name = var.twentycrm_app_name - stdin = true - tty = true - - env { - name = "NODE_PORT" - value = "3000" - } - - env { - name = "SERVER_URL" - value = var.twentycrm_app_hostname - } - - env { - name = "PG_DATABASE_URL" - value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" - } - env { - name = "REDIS_URL" - value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379" - } - env { - name = "DISABLE_DB_MIGRATIONS" - value = "false" - } - - env { - name = "STORAGE_TYPE" - value = "local" - } - env { - name = "ACCESS_TOKEN_EXPIRES_IN" - value = "7d" - } - env { - name = "LOGIN_TOKEN_EXPIRES_IN" - value = "1h" - } - env { - name = "APP_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "accessToken" - } - } - } - - port { - container_port = 3000 - protocol = "TCP" - } - - resources { - requests = { - cpu = "250m" - memory = "256Mi" - } - limits = { - cpu = "1000m" - memory = "1024Mi" - } - } - - volume_mount { - name = "server-data" - mount_path = var.twentycrm_server_data_mount_path - } - - volume_mount { - name = "docker-data" - mount_path = var.twentycrm_docker_data_mount_path - } - } - - volume { - name = "server-data" - - persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim.server.metadata.0.name - } - } - - volume { - name = "docker-data" - - persistent_volume_claim { - claim_name = kubernetes_persistent_volume_claim.docker_data.metadata.0.name - } - } - - dns_policy = "ClusterFirst" - restart_policy = "Always" - } - } - } - depends_on = [ - kubernetes_deployment.twentycrm_db, - kubernetes_deployment.twentycrm_redis, - kubernetes_secret.twentycrm_tokens - ] -} diff --git a/twenty/code/k8s/terraform/deployment-worker.tf b/twenty/code/k8s/terraform/deployment-worker.tf deleted file mode 100644 index 9e4da3e..0000000 --- a/twenty/code/k8s/terraform/deployment-worker.tf +++ /dev/null @@ -1,99 +0,0 @@ -resource "kubernetes_deployment" "twentycrm_worker" { - metadata { - name = "${var.twentycrm_app_name}-worker" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - labels = { - app = "${var.twentycrm_app_name}-worker" - } - } - - spec { - replicas = var.twentycrm_worker_replicas - selector { - match_labels = { - app = "${var.twentycrm_app_name}-worker" - } - } - - strategy { - type = "RollingUpdate" - rolling_update { - max_surge = "1" - max_unavailable = "1" - } - } - - template { - metadata { - labels = { - app = "${var.twentycrm_app_name}-worker" - } - } - - spec { - container { - image = var.twentycrm_server_image - name = var.twentycrm_app_name - stdin = true - tty = true - command = ["yarn", "worker:prod"] - - env { - name = "SERVER_URL" - value = var.twentycrm_app_hostname - } - - env { - name = "PG_DATABASE_URL" - value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" - } - - env { - name = "REDIS_URL" - value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379" - } - - env { - name = "DISABLE_DB_MIGRATIONS" - value = "true" #it already runs on the server - } - - env { - name = "STORAGE_TYPE" - value = "local" - } - - env { - name = "APP_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "accessToken" - } - } - } - - resources { - requests = { - cpu = "250m" - memory = "1024Mi" - } - limits = { - cpu = "1000m" - memory = "2048Mi" - } - } - } - - dns_policy = "ClusterFirst" - restart_policy = "Always" - } - } - } - depends_on = [ - kubernetes_deployment.twentycrm_db, - kubernetes_deployment.twentycrm_redis, - kubernetes_deployment.twentycrm_server, - kubernetes_secret.twentycrm_tokens, - ] -} diff --git a/twenty/code/k8s/terraform/ingress.tf b/twenty/code/k8s/terraform/ingress.tf deleted file mode 100644 index f8a2877..0000000 --- a/twenty/code/k8s/terraform/ingress.tf +++ /dev/null @@ -1,30 +0,0 @@ -resource "kubernetes_ingress" "twentycrm" { - wait_for_load_balancer = true - metadata { - name = "${var.twentycrm_app_name}-ingress" - namespace = kubernetes_namespace.twentycrm.metadata.0.name - annotations = { - "kubernetes.io/ingress.class" = "nginx" - "nginx.ingress.kubernetes.io/configuration-snippet" = </dev/null; then - echo -e "\t❌ Docker is not installed or not in PATH. Please install Docker first.\n\t\tSee https://docs.docker.com/get-docker/" - exit 1 -fi -# Check if docker compose plugin is installed -if ! docker compose version &>/dev/null; then - echo -e "\t❌ Docker Compose is not installed or not in PATH (n.b. docker-compose is deprecated)\n\t\tUpdate docker or install docker-compose-plugin\n\t\tOn Linux: sudo apt-get install docker-compose-plugin\n\t\tSee https://docs.docker.com/compose/install/" - exit 1 -fi -# Check if docker is started -if ! docker info &>/dev/null; then - echo -e "\t❌ Docker is not running.\n\t\tPlease start Docker Desktop, Docker or check documentation at https://docs.docker.com/config/daemon/start/" - exit 1 -fi -if ! command -v curl &>/dev/null; then - echo -e "\t❌ Curl is not installed or not in PATH.\n\t\tOn macOS: brew install curl\n\t\tOn Linux: sudo apt install curl" - exit 1 -fi - -# Check if docker compose version is >= 2 -if [ "$(docker compose version --short | cut -d' ' -f3 | cut -d'.' -f1)" -lt 2 ]; then - echo -e "\t❌ Docker Compose is outdated. Please update Docker Compose to version 2 or higher.\n\t\tSee https://docs.docker.com/compose/install/linux/" - exit 1 -fi -# Check if docker-compose is installed, if so issue a warning if version is < 2 -if command -v docker-compose &>/dev/null; then - if [ "$(docker-compose version --short | cut -d' ' -f3 | cut -d'.' -f1)" -lt 2 ]; then - echo -e "\n\t⚠️ 'docker-compose' is installed but outdated. Make sure to use 'docker compose' or to upgrade 'docker-compose' to version 2.\n\t\tSee https://docs.docker.com/compose/install/standalone/\n" - fi -fi - -# Catch errors -set -e -function on_exit { - # $? is the exit status of the last command executed - local exit_status=$? - if [ $exit_status -ne 0 ]; then - echo "❌ Something went wrong, exiting: $exit_status" - fi -} -trap on_exit EXIT - -# Use environment variables VERSION and BRANCH, with defaults if not set -version=${VERSION:-$(curl -s "https://hub.docker.com/v2/repositories/twentycrm/twenty/tags" | grep -o '"name":"[^"]*"' | grep -v 'latest' | cut -d'"' -f4 | sort -V | tail -n1)} -branch=${BRANCH:-$(curl -s https://api.github.com/repos/twentyhq/twenty/tags | grep '"name":' | head -n 1 | cut -d '"' -f 4)} - -echo "🚀 Using docker version $version and Github branch $branch" - -dir_name="twenty" -function ask_directory { - read -p "📁 Enter the directory name to setup the project (default: $dir_name): " answer - if [ -n "$answer" ]; then - dir_name=$answer - fi -} - -ask_directory - -while [ -d "$dir_name" ]; do - read -p "🚫 Directory '$dir_name' already exists. Do you want to overwrite it? (y/N) " answer - if [ "$answer" = "y" ]; then - break - else - ask_directory - fi -done - -# Create a directory named twenty -echo "📁 Creating directory '$dir_name'" -mkdir -p "$dir_name" && cd "$dir_name" || { echo "❌ Failed to create/access directory '$dir_name'"; exit 1; } - -# Copy twenty/packages/twenty-docker/docker-compose.yml in it -echo -e "\t• Copying docker-compose.yml" -curl -sLo docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/docker-compose.yml - -# Copy twenty/packages/twenty-docker/.env.example to .env -echo -e "\t• Setting up .env file" -curl -sLo .env https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/.env.example - -# Replace TAG=latest by TAG= -if [[ $(uname) == "Darwin" ]]; then - # Running on macOS - sed -i '' "s/TAG=latest/TAG=$version/g" .env -else - # Assuming Linux - sed -i'' "s/TAG=latest/TAG=$version/g" .env -fi - -# Generate random strings for secrets -echo "# === Randomly generated secret ===" >> .env -echo "APP_SECRET=$(openssl rand -base64 32)" >> .env - -echo "" >> .env -echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 32)" >> .env - -echo -e "\t• .env configuration completed" - -port=3000 -# Check if command nc is available -if command -v nc &> /dev/null; then - # Check if port 3000 is already in use, propose to change it - while nc -zv localhost $port &>/dev/null; do - read -p "🚫 Port $port is already in use. Do you want to use another port? (Y/n) " answer - if [ "$answer" = "n" ]; then - continue - fi - read -p "Enter a new port number: " new_port - if [[ $(uname) == "Darwin" ]]; then - sed -i '' "s/$port:$port/$new_port:$port/g" docker-compose.yml - sed -E -i '' "s|^SERVER_URL=http://localhost:[0-9]+|SERVER_URL=http://localhost:$new_port|g" .env - else - sed -i'' "s/$port:$port/$new_port:$port/g" docker-compose.yml - sed -E -i'' "s|^SERVER_URL=http://localhost:[0-9]+|SERVER_URL=http://localhost:$new_port|g" .env - fi - port=$new_port - done -fi - -# Ask user if they want to start the project -read -p "🚀 Do you want to start the project now? (Y/n) " answer -if [ "$answer" = "n" ]; then - echo "✅ Project setup completed. Run 'docker compose up -d' to start." - exit 0 -else - echo "🐳 Starting Docker containers..." - docker compose up -d - # Check if port is listening - echo "Waiting for server to be healthy, it might take a few minutes while we initialize the database..." - # Tail logs of the server until it's ready - docker compose logs -f server & - pid=$! - while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do - sleep 1 - done - kill $pid - echo "" - echo "✅ Server is up and running" -fi - -function ask_open_browser { - read -p "🌐 Do you want to open the project in your browser? (Y/n) " answer - if [ "$answer" = "n" ]; then - echo "✅ Setup completed. Access your project at http://localhost:$port" - exit 0 - fi -} - -# Ask user if they want to open the project -# Running on macOS -if [[ $(uname) == "Darwin" ]]; then - ask_open_browser - - open "http://localhost:$port" -# Assuming Linux -else - # xdg-open is not installed, we could be running in a non gui environment - if command -v xdg-open >/dev/null 2>&1; then - ask_open_browser - - xdg-open "http://localhost:$port" - else - echo "✅ Setup completed. Your project is available at http://localhost:$port" - fi -fi diff --git a/twenty/code/twenty-postgres-spilo/Dockerfile b/twenty/code/twenty-postgres-spilo/Dockerfile deleted file mode 100644 index 9a84c12..0000000 --- a/twenty/code/twenty-postgres-spilo/Dockerfile +++ /dev/null @@ -1,68 +0,0 @@ -ARG POSTGRES_VERSION=15 -ARG SPILO_VERSION=3.2-p1 -ARG WRAPPERS_VERSION=0.2.0 - -# Build the mysql_fdw extension -FROM debian:bookworm AS build-mysql_fdw -ARG POSTGRES_VERSION - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && \ - apt install -y \ - build-essential \ - git \ - postgresql-server-dev-${POSTGRES_VERSION} \ - default-libmysqlclient-dev && \ - rm -rf /var/lib/apt/lists/* - -# Install mysql_fdw -RUN git clone https://github.com/EnterpriseDB/mysql_fdw.git -WORKDIR /mysql_fdw -RUN make USE_PGXS=1 - - -# Build libssl for wrappers -FROM ubuntu:22.04 AS build-libssl - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && \ - apt install -y \ - build-essential \ - git && \ - rm -rf /var/lib/apt/lists/* - -WORKDIR /build -RUN git clone --branch OpenSSL_1_1_1-stable https://github.com/openssl/openssl.git -WORKDIR /build/openssl -RUN ./config && make && make install - - -# Extend the Spilo image with the mysql_fdw extensions -FROM ghcr.io/zalando/spilo-${POSTGRES_VERSION}:${SPILO_VERSION} -ARG POSTGRES_VERSION -ARG WRAPPERS_VERSION -ARG TARGETARCH - -# Install precompiled supabase wrappers extensions -RUN set -eux; \ - ARCH="$(dpkg --print-architecture)"; \ - case "${ARCH}" in \ - aarch64|arm64) TARGETARCH='arm64';; \ - amd64|x86_64) TARGETARCH='amd64';; \ - *) echo "Unsupported arch: ${ARCH}"; exit 1;; \ - esac; - -RUN apt update && apt install default-libmysqlclient-dev -y && rm -rf /var/lib/apt/lists/* - -RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_VERSION}/wrappers-v${WRAPPERS_VERSION}-pg${POSTGRES_VERSION}-${TARGETARCH}-linux-gnu.deb" -o wrappers.deb && \ - dpkg --install wrappers.deb && \ - rm wrappers.deb - -COPY --from=build-libssl /usr/local/lib/libssl* /usr/local/lib/libcrypto* /usr/lib/ -COPY --from=build-libssl /usr/local/lib/engines-1.1 /usr/lib/engines-1.1 - -# Copy mysql_fdw -COPY --from=build-mysql_fdw /mysql_fdw/mysql_fdw.so \ - /usr/lib/postgresql/${POSTGRES_VERSION}/lib/mysql_fdw.so -COPY --from=build-mysql_fdw /mysql_fdw/mysql_fdw*.sql /mysql_fdw/mysql_fdw.control /mysql_fdw/mysql_fdw_pushdown.config \ - /usr/share/postgresql/${POSTGRES_VERSION}/extension/ diff --git a/twenty/code/twenty-website/Dockerfile b/twenty/code/twenty-website/Dockerfile deleted file mode 100644 index 2ff7cb8..0000000 --- a/twenty/code/twenty-website/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM node:24-alpine AS twenty-website-build - - -WORKDIR /app - -COPY ./package.json . -COPY ./yarn.lock . -COPY ./.yarnrc.yml . -COPY ./.yarn/releases /app/.yarn/releases -COPY ./.yarn/patches /app/.yarn/patches -COPY ./tools/eslint-rules /app/tools/eslint-rules -COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/ -COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/ -COPY ./packages/twenty-website/package.json /app/packages/twenty-website/package.json - -RUN yarn - -ENV KEYSTATIC_GITHUB_CLIENT_ID="" -ENV KEYSTATIC_GITHUB_CLIENT_SECRET="" -ENV KEYSTATIC_SECRET="" -ENV NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG="" - -COPY ./packages/twenty-ui /app/packages/twenty-ui -COPY ./packages/twenty-website /app/packages/twenty-website -RUN npx nx build twenty-website - -FROM node:24-alpine AS twenty-website - -WORKDIR /app/packages/twenty-website - -COPY --from=twenty-website-build /app /app - -WORKDIR /app/packages/twenty-website - -LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty -LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the website." - -RUN chown -R 1000 /app - -# Use non root user with uid 1000 -USER 1000 - -CMD ["/bin/sh", "-c", "npx nx start"] \ No newline at end of file diff --git a/twenty/code/twenty/Dockerfile b/twenty/code/twenty/Dockerfile deleted file mode 100644 index 7818be7..0000000 --- a/twenty/code/twenty/Dockerfile +++ /dev/null @@ -1,82 +0,0 @@ -# Base image for common dependencies -FROM node:24-alpine AS common-deps - -WORKDIR /app - -# Copy only the necessary files for dependency resolution -COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/ -COPY ./.yarn/releases /app/.yarn/releases -COPY ./.yarn/patches /app/.yarn/patches - -COPY ./.prettierrc /app/ -COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/ -COPY ./packages/twenty-server/package.json /app/packages/twenty-server/ -COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches -COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/ -COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/ -COPY ./packages/twenty-front/package.json /app/packages/twenty-front/ - -# Install all dependencies -RUN yarn && yarn cache clean && npx nx reset - - -# Build the back -FROM common-deps AS twenty-server-build - -# Copy sourcecode after installing dependences to accelerate subsequents builds -COPY ./packages/twenty-emails /app/packages/twenty-emails -COPY ./packages/twenty-shared /app/packages/twenty-shared -COPY ./packages/twenty-server /app/packages/twenty-server - -RUN npx nx run twenty-server:build - -RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-server - -# Build the front -FROM common-deps AS twenty-front-build - -ARG REACT_APP_SERVER_BASE_URL - -COPY ./packages/twenty-front /app/packages/twenty-front -COPY ./packages/twenty-ui /app/packages/twenty-ui -COPY ./packages/twenty-shared /app/packages/twenty-shared -RUN npx nx build twenty-front - - -# Final stage: Run the application -FROM node:24-alpine AS twenty - -# Used to run healthcheck in docker -RUN apk add --no-cache curl jq - -RUN npm install -g tsx - -RUN apk add --no-cache postgresql-client - -COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh -RUN chmod +x /app/entrypoint.sh -WORKDIR /app/packages/twenty-server - -ARG REACT_APP_SERVER_BASE_URL -ENV REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL - -ARG APP_VERSION -ENV APP_VERSION=$APP_VERSION - -# Copy built applications from previous stages -COPY --chown=1000 --from=twenty-server-build /app /app -COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server -COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front - -# Set metadata and labels -LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty -LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the backend and frontend, ensuring it deploys faster and runs the same way regardless of the deployment environment." - -RUN mkdir -p /app/.local-storage /app/packages/twenty-server/.local-storage && \ - chown -R 1000:1000 /app - -# Use non root user with uid 1000 -USER 1000 - -CMD ["node", "dist/src/main"] -ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/twenty/code/twenty/entrypoint.sh b/twenty/code/twenty/entrypoint.sh deleted file mode 100755 index 0175a0a..0000000 --- a/twenty/code/twenty/entrypoint.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/sh -set -e - -setup_and_migrate_db() { - if [ "${DISABLE_DB_MIGRATIONS}" = "true" ]; then - echo "Database setup and migrations are disabled, skipping..." - return - fi - - echo "Running database setup and migrations..." - PGUSER=$(echo $PG_DATABASE_URL | awk -F '//' '{print $2}' | awk -F ':' '{print $1}') - PGPASS=$(echo $PG_DATABASE_URL | awk -F ':' '{print $3}' | awk -F '@' '{print $1}') - PGHOST=$(echo $PG_DATABASE_URL | awk -F '@' '{print $2}' | awk -F ':' '{print $1}') - PGPORT=$(echo $PG_DATABASE_URL | awk -F ':' '{print $4}' | awk -F '/' '{print $1}') - PGDATABASE=$(echo $PG_DATABASE_URL | awk -F '/' '{print $NF}' | cut -d'?' -f1) - - # Creating the database if it doesn't exist - db_count=$(PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -tAc "SELECT COUNT(*) FROM pg_database WHERE datname = '${PGDATABASE}'") - if [ "$db_count" = "0" ]; then - echo "Database ${PGDATABASE} does not exist, creating..." - PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -c "CREATE DATABASE \"${PGDATABASE}\"" - fi - - # Run setup and migration scripts - has_schema=$(PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d ${PGDATABASE} -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')") - if [ "$has_schema" = "f" ]; then - echo "Database appears to be empty, running migrations." - NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts - yarn database:migrate:prod - fi - - yarn command:prod upgrade - echo "Successfully migrated DB!" -} - -register_background_jobs() { - if [ "${DISABLE_CRON_JOBS_REGISTRATION}" = "true" ]; then - echo "Cron job registration is disabled, skipping..." - return - fi - - echo "Registering background sync jobs..." - if yarn command:prod cron:register:all; then - echo "Successfully registered all background sync jobs!" - else - echo "Warning: Failed to register background jobs, but continuing startup..." - fi -} - -setup_and_migrate_db -register_background_jobs - -# Continue with the original Docker command -exec "$@" diff --git a/twenty/update.js b/twenty/update.js deleted file mode 100644 index b6131b7..0000000 --- a/twenty/update.js +++ /dev/null @@ -1,25 +0,0 @@ -import utils from "../utils.js"; - -await utils.cloneOrPullRepo({ repo: "git@github.com:twentyhq/twenty.git" }); -await utils.copyDir("./repo/packages/twenty-docker", "./code"); - -await utils.removeContainerNames("./code/docker-compose.yml"); -await utils.removePorts("./code/docker-compose.yml"); - -await utils.searchReplace( - "./code/.env.example", - "#REDIS_URL=redis://redis:6379", - "REDIS_URL=redis://redis:6379" -); - -await utils.searchReplace( - "./code/.env.example", - "SERVER_URL=http://localhost:3000", - "SERVER_URL=https://$(PRIMARY_DOMAIN)" -); - -await utils.searchReplace( - "./code/.env.example", - "# APP_SECRET=replace_me_with_a_random_string", - "APP_SECRET=replace_me_with_a_random_string" -); diff --git a/utils.js b/utils.js index d22ef20..2644f31 100644 --- a/utils.js +++ b/utils.js @@ -81,6 +81,18 @@ async function searchReplace(path, search, replace) { await fs.promises.writeFile(path, newFile); } +async function removeDir(path) { + console.log(`Removing directory ${path}`); + + await execa("rm", ["-rf", path]); +} + +async function renameDir(src, dest) { + console.log(`Renaming directory ${src} to ${dest}`); + + await execa("mv", [src, dest]); +} + export default { cloneOrPullRepo, removeContainerNames, @@ -89,4 +101,6 @@ export default { downloadFile, renameFile, searchReplace, + removeDir, + renameDir, }; From c6b41076eaf45cff201b7dbcb84b2c384e1459a9 Mon Sep 17 00:00:00 2001 From: Ahson Shaikh Date: Thu, 28 Aug 2025 22:03:39 +0500 Subject: [PATCH 2/2] Updated Appwrite Env --- appwrite/code/.env.example | 2 +- appwrite/update.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appwrite/code/.env.example b/appwrite/code/.env.example index ad66348..71d2007 100644 --- a/appwrite/code/.env.example +++ b/appwrite/code/.env.example @@ -6,7 +6,7 @@ _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled _APP_OPTIONS_ROUTER_PROTECTION=disabled _APP_OPENSSL_KEY_V1=your-secret-key -_APP_DOMAIN=$(PRIMARY_DOMAIN)localhost +_APP_DOMAIN=$(PRIMARY_DOMAIN) _APP_CUSTOM_DOMAIN_DENY_LIST=example.com,test.com,app.example.com _APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_SITES=sites.localhost diff --git a/appwrite/update.js b/appwrite/update.js index 67a4d15..50e0743 100644 --- a/appwrite/update.js +++ b/appwrite/update.js @@ -14,6 +14,6 @@ await utils.removePorts("./code/docker-compose.yml"); await utils.searchReplace( "./code/.env.example", - "_APP_DOMAIN=", + "_APP_DOMAIN=localhost", "_APP_DOMAIN=$(PRIMARY_DOMAIN)" );