test/lib/plane.sh

627 lines
22 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
#
# Plane Service
PATH=$HOME/.docker/cli-plugins:/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
. /federated/lib/helpers.sh
# FIXME plane's nginx proxy has a few additional rules that we may
# need to port to Traefik:
# add_header X-Content-Type-Options "nosniff" always;
# add_header Referrer-Policy "no-referrer-when-downgrade" always;
# add_header Permissions-Policy "interest-cohort=()" always;
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# add_header X-Forwarded-Proto "$scheme";
# add_header X-Forwarded-Host "$host";
# add_header X-Forwarded-For "$proxy_add_x_forwarded_for";
# add_header X-Real-IP "$remote_addr";
config_plane() {
echo -ne "\n* Configuring /federated/apps/plane container.."
if [ ! -d "/federated/apps/plane" ]; then
mkdir -p /federated/apps/plane/data/plane &> /dev/null
fi
POSTGRES_PASSWORD=$(create_password)
EMAIL_PASSWORD="$(cat /federated/apps/panel/.env |grep ^SMTP_PASSWORD= |cut -d= -f2-)"
USE_TRAEFIK=true
cat >/federated/apps/plane/.env <<'EOF'
APP_DOMAIN=plane.@DOMAIN@
IMAGE_VERSION=@VERSION@
VALKEY_RELEASE=7.2.5-alpine
RABBITMQ_RELEASE=3.13.6-management-alpine
MINIO_RELEASE=RELEASE.2024-12-18T13-15-44Z
WEB_REPLICAS=1
SPACE_REPLICAS=1
ADMIN_REPLICAS=1
API_REPLICAS=1
NGINX_PORT=80
WEB_URL=http://${APP_DOMAIN}
DEBUG=0
SENTRY_DSN=
SENTRY_ENVIRONMENT=production
CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
API_BASE_URL=http://api:8000
#DB SETTINGS
PGHOST=postgresql
PGDATABASE=plane
POSTGRES_USER=plane
POSTGRES_PASSWORD=@POSTGRES_PASSWORD@
POSTGRES_DB=plane
POSTGRES_PORT=5432
PGDATA=/var/lib/postgresql/data
DATABASE_URL=
# REDIS SETTINGS
REDIS_HOST=plane-redis
REDIS_PORT=6379
REDIS_URL=
# RabbitMQ Settings
RABBITMQ_HOST=plane-mq
RABBITMQ_PORT=5672
RABBITMQ_USER=plane
RABBITMQ_PASSWORD=plane
RABBITMQ_VHOST=plane
AMQP_URL=
# Secret Key
SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
# DATA STORE SETTINGS
USE_MINIO=1
AWS_REGION=
AWS_ACCESS_KEY_ID=access-key
AWS_SECRET_ACCESS_KEY=secret-key
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
MINIO_ROOT_USER=access-key
MINIO_ROOT_PASSWORD=secret-key
BUCKET_NAME=uploads
FILE_SIZE_LIMIT=5242880
# Gunicorn Workers
GUNICORN_WORKERS=1
# Email
EMAIL_HOST=mail.@DOMAIN@
EMAIL_HOST_USER=fcore@@DOMAIN@
EMAIL_HOST_PASSWORD=@EMAIL_PASSWORD@
EMAIL_PORT=587
EMAIL_FROM=admin@@DOMAIN@
EMAIL_USE_TLS=1
EMAIL_USE_SSL=0
# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `IMAGE_VERSION`
# DOCKER_PLATFORM=linux/amd64
DOCKERHUB_USER=makeplane
PULL_POLICY=if_not_present
CUSTOM_BUILD=false
EOF
cat > /federated/apps/plane/docker-compose.yml <<'EOF'
x-app-env: &app-env
environment:
- NGINX_PORT=${NGINX_PORT:-80}
- WEB_URL=${WEB_URL:-http://localhost}
- DEBUG=${DEBUG:-0}
- SENTRY_DSN=${SENTRY_DSN:-""}
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-}
# Gunicorn Workers
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-1}
#DB SETTINGS
- PGHOST=${PGHOST:-postgresql}
- PGDATABASE=${PGDATABASE:-plane}
- POSTGRES_USER=${POSTGRES_USER:-plane}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-@POSTGRES_PASSWORD@}
- POSTGRES_DB=${POSTGRES_DB:-plane}
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
- DATABASE_URL=${DATABASE_URL:-postgresql://plane:@POSTGRES_PASSWORD@@postgresql/plane}
# REDIS SETTINGS
- REDIS_HOST=${REDIS_HOST:-plane-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_URL=${REDIS_URL:-redis://plane-redis:6379/}
# RabbitMQ Settings
- RABBITMQ_HOST=${RABBITMQ_HOST:-plane-mq}
- RABBITMQ_PORT=${RABBITMQ_PORT:-5672}
- RABBITMQ_DEFAULT_USER=${RABBITMQ_USER:-plane}
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD:-plane}
- RABBITMQ_DEFAULT_VHOST=${RABBITMQ_VHOST:-plane}
- RABBITMQ_VHOST=${RABBITMQ_VHOST:-plane}
- AMQP_URL=${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
# Application secret
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
# DATA STORE SETTINGS
- USE_MINIO=${USE_MINIO:-1}
- AWS_REGION=${AWS_REGION:-}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-"access-key"}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-"secret-key"}
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-"access-key"}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
# Live server env
- API_BASE_URL=${API_BASE_URL:-http://api:8000}
services:
web:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${IMAGE_VERSION:-stable}
platform: ${DOCKER_PLATFORM:-}
expose:
- 3000
ports:
- 3000:3000
env_file:
- .env
EOF
if $USE_TRAEFIK; then
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
labels:
- "traefik.enable=true"
- "traefik.http.routers.plane-web.rule=Host(`plane.@DOMAIN@`)"
- "traefik.http.routers.plane-web.entrypoints=websecure"
- "traefik.http.routers.plane-web.tls.certresolver=letsencrypt"
- "traefik.http.services.plane-web.loadbalancer.server.port=3000"
EOF
fi
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
networks:
core:
ipv4_address: 192.168.0.50
pull_policy: if_not_present
restart: unless-stopped
command: node web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
depends_on:
- api
- worker
space:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${IMAGE_VERSION:-stable}
platform: ${DOCKER_PLATFORM:-}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.51
EOF
if $USE_TRAEFIK; then
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
labels:
- "traefik.enable=true"
- "traefik.http.routers.plane-space.rule=Host(`plane.@DOMAIN@`) && PathPrefix(`/spaces`)"
- "traefik.http.routers.plane-space.entrypoints=websecure"
- "traefik.http.routers.plane-space.tls.certresolver=letsencrypt"
- "traefik.http.services.plane-space.loadbalancer.server.port=3000"
EOF
fi
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
pull_policy: if_not_present
restart: unless-stopped
command: node space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
depends_on:
- api
- worker
- web
admin:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${IMAGE_VERSION:-stable}
platform: ${DOCKER_PLATFORM:-}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.52
EOF
if $USE_TRAEFIK; then
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
labels:
- "traefik.enable=true"
- "traefik.http.routers.plane-admin.rule=Host(`plane.@DOMAIN@`) && PathPrefix(`/god-mode`)"
- "traefik.http.routers.plane-admin.entrypoints=websecure"
- "traefik.http.routers.plane-admin.tls.certresolver=letsencrypt"
- "traefik.http.services.plane-admin.loadbalancer.server.port=3000"
EOF
fi
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
pull_policy: if_not_present
restart: unless-stopped
command: node admin/server.js admin
deploy:
replicas: ${ADMIN_REPLICAS:-1}
depends_on:
- api
- web
live:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-live:${IMAGE_VERSION:-stable}
platform: ${DOCKER_PLATFORM:-}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.53
EOF
if $USE_TRAEFIK; then
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
labels:
- "traefik.enable=true"
- "traefik.http.routers.plane-live.rule=Host(`plane.@DOMAIN@`) && PathPrefix(`/live`)"
- "traefik.http.routers.plane-live.entrypoints=websecure"
- "traefik.http.routers.plane-live.tls.certresolver=letsencrypt"
- "traefik.http.services.plane-live.loadbalancer.server.port=3000"
EOF
fi
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
pull_policy: if_not_present
restart: unless-stopped
command: node live/dist/server.js live
deploy:
replicas: ${LIVE_REPLICAS:-1}
depends_on:
- api
- web
api:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${IMAGE_VERSION:-stable}
platform: ${DOCKER_PLATFORM:-}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.54
EOF
if $USE_TRAEFIK; then
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
labels:
- "traefik.enable=true"
- "traefik.http.routers.plane-api.rule=Host(`plane.@DOMAIN@`) && (PathPrefix(`/api`) || PathPrefix(`/auth`))"
- "traefik.http.routers.plane-api.entrypoints=websecure"
- "traefik.http.routers.plane-api.tls.certresolver=letsencrypt"
- "traefik.http.services.plane-api.loadbalancer.server.port=8000"
EOF
fi
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-api.sh
deploy:
replicas: ${API_REPLICAS:-1}
volumes:
- logs_api:/code/plane/logs
depends_on:
- plane-redis
- plane-mq
worker:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${IMAGE_VERSION:-stable}
platform: ${DOCKER_PLATFORM:-}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.55
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-worker.sh
volumes:
- logs_worker:/code/plane/logs
depends_on:
- api
- plane-redis
- plane-mq
beat-worker:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${IMAGE_VERSION:-stable}
platform: ${DOCKER_PLATFORM:-}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.56
pull_policy: if_not_present
restart: unless-stopped
command: ./bin/docker-entrypoint-beat.sh
volumes:
- logs_beat-worker:/code/plane/logs
depends_on:
- api
- plane-redis
- plane-mq
migrator:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${IMAGE_VERSION:-stable}
platform: ${DOCKER_PLATFORM:-}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.57
pull_policy: if_not_present
restart: "no"
command: ./bin/docker-entrypoint-migrator.sh
volumes:
- logs_migrator:/code/plane/logs
depends_on:
- plane-redis
plane-redis:
<<: *app-env
image: valkey/valkey:${VALKEY_RELEASE:-7.2.5-alpine}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.58
pull_policy: if_not_present
restart: unless-stopped
volumes:
- redisdata:/data
plane-mq:
<<: *app-env
image: rabbitmq:${RABBITMQ_RELEASE:-3.13.6-management-alpine}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.59
restart: always
volumes:
- rabbitmq_data:/var/lib/rabbitmq
plane-minio:
<<: *app-env
image: minio/minio:${MINIO_RELEASE:-RELEASE.2024-12-18T13-15-44Z}
env_file:
- .env
networks:
core:
ipv4_address: 192.168.0.60
EOF
if $USE_TRAEFIK; then
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
labels:
- "traefik.enable=true"
- "traefik.http.routers.plane-uploads.rule=Host(`plane.@DOMAIN@`) && PathPrefix(`/uploads`)"
- "traefik.http.routers.plane-uploads.entrypoints=websecure"
- "traefik.http.routers.plane-uploads.tls.certresolver=letsencrypt"
- "traefik.http.services.plane-uploads.loadbalancer.server.port=9000"
EOF
fi
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
pull_policy: if_not_present
restart: unless-stopped
command: server /export --console-address ":9090"
volumes:
- uploads:/export
EOF
if ! $USE_TRAEFIK; then
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
proxy:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${IMAGE_VERSION:-stable}
env_file:
- .env
platform: ${DOCKER_PLATFORM:-}
pull_policy: if_not_present
restart: unless-stopped
ports:
- ${NGINX_PORT}:80
depends_on:
- web
- api
- space
EOF
fi
cat >> /federated/apps/plane/docker-compose.yml <<'EOF'
volumes:
redisdata:
uploads:
logs_api:
logs_worker:
logs_beat-worker:
logs_migrator:
rabbitmq_data:
networks:
core:
external: true
EOF
sed -i -e "s,@DOMAIN@,${DOMAIN},g" \
-e "s,@VERSION@,$(current_version plane),g" \
-e "s,@POSTGRES_PASSWORD@,${POSTGRES_PASSWORD},g" \
-e "s,@EMAIL_PASSWORD@,${EMAIL_PASSWORD},g" \
/federated/apps/plane/docker-compose.yml \
/federated/apps/plane/.env
chmod 600 /federated/apps/plane/.env
# Create database and user in postgresql
SQL="docker exec postgresql psql --csv -U postgres"
$SQL -c "CREATE DATABASE plane" &> /dev/null
$SQL -c "CREATE USER plane WITH PASSWORD '${POSTGRES_PASSWORD}'" &> /dev/null
$SQL -c "GRANT ALL PRIVILEGES ON DATABASE plane TO plane" &> /dev/null
unset POSTGRES_PASSWORD
# migrator is usually started at the same time as plane - we need to
# run it manually once to create the initial database so we can make
# modifications to it (like creating the admin user) before plane is
# run the regular way
pushd /federated/apps/plane
docker compose up -d migrator
popd
# Wait for the migrator to exit -- at that point, the database should be
# ready for manipulation
echo "Waiting for completion of the initial plane database - this will take some time."
while [ -n "$(docker ps -q -f name=plane-migrator-1)" ]; do
sleep 1s
echo -n .
done
echo
dnf -y --refresh install python-passlib || (apt update ; apt -y install python3-passlib)
INSTANCE_ID=$(random xxxxxxxxxxxxxxxxxxxxxxxx)
INSTANCE_UUID=$(random xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
USER_UUID=$(random xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
ADMIN_UUID=$(random xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
NOTIFICATION_UUID=$(random xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
PROFILE_UUID=$(random xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
SIGNUP_UUID=$(random xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
TOKEN=$(random xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
ADMINPASS=$(grep ADMINPASS= /var/lib/cloud/instances/[0-9]*/cloud-config.txt |cut -d= -f2- |cut -d'"' -f2 |tr -d '\\')
ENCODED_PASSWORD=$(python3 -c "from passlib.hash import django_pbkdf2_sha256; print(django_pbkdf2_sha256.hash('$ADMINPASS'))")
# FIXME don't hardcode 0.24.0
cat >/federated/apps/postgresql/data/var/lib/postgresql/data/plane-initial-user.sql <<EOF
INSERT INTO instances(created_at, updated_at, id, instance_name, instance_id, current_version, last_checked_at, is_telemetry_enabled, is_support_required, is_setup_done, is_signup_screen_visited, is_verified, domain, latest_version, edition, is_test)
VALUES(NOW(), NOW(), '$INSTANCE_UUID', 'Federated Computer - Plane', '$INSTANCE_ID', '0.24.0', NOW(), FALSE, FALSE, TRUE, TRUE, TRUE, '$DOMAIN', '0.24.0', 'PLANE_COMMUNITY', FALSE);
UPDATE instances SET instance_name='Federated Computer - Plane';
UPDATE instances SET is_telemetry_enabled=FALSE;
UPDATE instances SET is_setup_done=TRUE;
INSERT INTO users (password, last_login, id, username, email, first_name, last_name, avatar, date_joined, created_at, updated_at, last_location, created_location, is_superuser, is_managed, is_password_expired, is_active, is_staff, is_email_verified, is_password_autoset, token, user_timezone, last_active, last_login_time, last_logout_time, last_login_ip, last_logout_ip, last_login_medium, last_login_uagent, is_bot, display_name)
VALUES('$ENCODED_PASSWORD', NOW(), '$USER_UUID', 'admin', 'admin@$DOMAIN', 'Plane', 'Admin', '', NOW(), NOW(), NOW(), '', '', TRUE, FALSE, FALSE, TRUE, TRUE, TRUE, FALSE, '$TOKEN', 'UTC', NOW(), NOW(), NOW(), '192.168.0.13', '192.168.0.13', 'email', 'Federated Signup/1.0', FALSE, 'admin');
INSERT INTO user_notification_preferences(created_at, updated_at, id, property_change, state_change, comment, mention, issue_completed, user_id)
VALUES(NOW(), NOW(), '$NOTIFICATION_UUID', FALSE, FALSE, FALSE, FALSE, FALSE, '$USER_UUID');
INSERT INTO profiles(created_at, updated_at, id, theme, is_tour_completed, onboarding_step, use_case, role, is_onboarded, billing_address_country, has_billing_address, company_name, user_id)
VALUES(NOW(), NOW(), '$PROFILE_UUID', '{}', FALSE, '{"workspace_join": false, "profile_complete": false, "workspace_create": false, "workspace_invite": false}', '', '', FALSE, 'INDIA', FALSE, '$DOMAIN', '$USER_UUID');
INSERT INTO instance_admins(created_at, updated_at, id, role, is_verified, instance_id, user_id)
VALUES(NOW(), NOW(), '$ADMIN_UUID', 20, TRUE, '$INSTANCE_UUID', '$USER_UUID');
INSERT INTO instance_configurations(created_at, updated_at, id, key, value, category, is_encrypted)
VALUES(NOW(), NOW(), '$SIGNUP_UUID', 'ENABLE_SIGNUP', '0', 'AUTHENTICATION', FALSE);
UPDATE instance_configurations SET value=0 WHERE key='ENABLE_SIGNUP';
EOF
$SQL plane -f /var/lib/postgresql/data/plane-initial-user.sql
# rm /federated/apps/postgresql/data/var/lib/postgresql/data/plane-initial-user.sql
echo -ne "done."
}
start_plane() {
# Start service with command to make sure it's up before proceeding
# start_service "plane" "nc -z 192.168.0.50 3000 &> /dev/null" "7"
/federated/bin/start plane
echo -ne "done."
}
uninstall_plane() {
docker exec postgresql psql -U postgres -c "DROP DATABASE plane" &> /dev/null
docker exec postgresql psql -U postgres -c "DROP USER plane" &> /dev/null
rm -rf /federated/apps/plane
}
email_plane() {
echo -ne "* Sending email to customer.."
spin &
SPINPID=$!
cat > /federated/apps/mail/data/root/certs/mailfile <<EOF
<html>
<img src="https://www.federated.computer/wp-content/uploads/2023/11/logo.png" alt="" /><br>
<p>
<h4>Plane is now installed on $DOMAIN</h4>
<p>
Here is your applications chart with on how to access this service:<br>
<p>
<h4>Applications</h4>
<style type="text/css">
.tg {border-collapse:collapse;border-spacing:0;}
.tg td{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
overflow:hidden;padding:10px 5px;word-break:normal;}
.tg th{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal;}
.tg .tg-cul6{border-color:inherit;color:#340096;text-align:left;text-decoration:underline;vertical-align:top}
.tg .tg-acii{background-color:#FFF;border-color:inherit;color:#333;text-align:left;vertical-align:top}
.tg .tg-0hty{background-color:#000000;border-color:inherit;color:#ffffff;font-weight:bold;text-align:left;vertical-align:top}
.tg .tg-kwiq{border-color:inherit;color:#000000;text-align:left;vertical-align:top;word-wrap:break-word}
.tg .tg-0pky{border-color:inherit;text-align:left;vertical-align:top}
</style>
<table class="tg" style="undefined;table-layout: fixed; width: 996px">
<colgroup>
<col style="width: 101.333333px">
<col style="width: 203.333333px">
<col style="width: 282.333333px">
<col style="width: 185.33333px">
<col style="width: 78.333333px">
<col style="width: 220.333333px">
</colgroup>
<thead>
<tr>
<th class="tg-0hty">Service</th>
<th class="tg-0hty">Link</th>
<th class="tg-0hty">User / Pass</th>
<th class="tg-0hty">Access</th>
<th class="tg-0hty">Docs</th>
<th class="tg-0hty">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tg-kwiq">Plane</td>
<td class="tg-kwiq"><a href="https://plane.$DOMAIN" target="_blank" rel="noopener noreferrer"><span style="color:#340096">plane.$DOMAIN</span></a></td>
<td class="tg-kwiq">admin@$DOMAIN<br>admin password above</td>
<td class="tg-kwiq">User access is separate from panel. Use the admin account to login and then invite other users</td>
<td class="tg-kwiq"><a href="https://documentation.federated.computer/docs/getting_started/welcome/" target="_blank" rel="noopener noreferrer"><span style="color:#340096">Click here</span></a></td>
<td class="tg-kwiq">Plane is a flexible project management tool that helps teams plan, track, and manage their work more efficiently. Its built to grow with you, offering features like issue tracking, sprint management, time tracking, knowledge management, analytics and more. These tools help your team stay organized, keep things clear, and stay aligned throughout the project.</td>
</tr>
</tbody>
</table>
<h4>Thanks for your support!</h4>
<p>
Thank you for your support of Federated Computer. We really appreciate it and hope you have a very successful
time with Federated Core.
<p>
Again, if we can be of any assistance, please don't hesitate to get in touch.
<p>
Support: https://support.federated.computer<br>
Phone: (970) 722-8715<br>
Email: support@federated.computer<br>
<p>
It's <b>your</b> computer. Let's make it work for you!
</html>
EOF
# Send out e-mail from mail container with details
docker exec mail bash -c "mail -r admin@$DOMAIN -a \"Content-type: text/html\" -s \"Application installed on $DOMAIN\" $EMAIL < /root/certs/mailfile"
rm /federated/apps/mail/data/root/certs/mailfile
kill -9 $SPINPID &> /dev/null
echo -ne "done.\n"
}