mirror of
https://github.com/frappe/bench.git
synced 2025-01-08 00:04:38 +00:00
Merge pull request #1592 from revant/easy-build
This commit is contained in:
commit
1bf53b4e7c
2
.github/workflows/easy-install.yml
vendored
2
.github/workflows/easy-install.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Perform production easy install
|
- name: Perform production easy install
|
||||||
run: |
|
run: |
|
||||||
python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io
|
python3 ${GITHUB_WORKSPACE}/easy-install.py build --deploy --tag=custom-apps:latest --project=actions_test --email=test@frappe.io --image=custom-apps --version=latest --app=erpnext
|
||||||
docker compose -p actions_test exec backend bench version --format json
|
docker compose -p actions_test exec backend bench version --format json
|
||||||
docker compose -p actions_test exec backend bench --site site1.localhost list-apps --format json
|
docker compose -p actions_test exec backend bench --site site1.localhost list-apps --format json
|
||||||
result=$(curl -H "Host: site1.localhost" -sk https://127.0.0.1/api/method/ping | jq -r ."message")
|
result=$(curl -H "Host: site1.localhost" -sk https://127.0.0.1/api/method/ping | jq -r ."message")
|
||||||
|
148
README.md
148
README.md
@ -87,7 +87,7 @@ Download the Easy Install script and execute it:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ wget https://raw.githubusercontent.com/frappe/bench/develop/easy-install.py
|
$ wget https://raw.githubusercontent.com/frappe/bench/develop/easy-install.py
|
||||||
$ python3 easy-install.py --prod --email your@email.tld
|
$ python3 easy-install.py deploy --email=user@domain.tld --sitename=subdomain.domain.tld --app=erpnext
|
||||||
```
|
```
|
||||||
|
|
||||||
This script will install docker on your system and will fetch the required containers, setup bench and a default ERPNext instance.
|
This script will install docker on your system and will fetch the required containers, setup bench and a default ERPNext instance.
|
||||||
@ -101,20 +101,150 @@ When the setup is complete, you will be able to access the system at `http://<yo
|
|||||||
|
|
||||||
Here are the arguments for the easy-install script
|
Here are the arguments for the easy-install script
|
||||||
|
|
||||||
```txt
|
**Build custom images**
|
||||||
usage: easy-install.py [-h] [-p] [-d] [-s SITENAME] [-n PROJECT] [--email EMAIL]
|
|
||||||
|
|
||||||
Install Frappe with Docker
|
```txt
|
||||||
|
usage: easy-install.py build [-h] [-n PROJECT] [-i IMAGE] [-q] [-m HTTP_PORT] [-v VERSION] [-a APPS] [-s SITES] [-e EMAIL]
|
||||||
|
[-p] [-r FRAPPE_PATH] [-b FRAPPE_BRANCH] [-j APPS_JSON] [-t TAGS] [-c CONTAINERFILE]
|
||||||
|
[-y PYTHON_VERSION] [-d NODE_VERSION] [-x] [-u]
|
||||||
|
|
||||||
options:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-p, --prod Setup Production System
|
-n PROJECT, --project PROJECT
|
||||||
-d, --dev Setup Development System
|
Project Name
|
||||||
-s SITENAME, --sitename SITENAME The Site Name for your production site
|
-i IMAGE, --image IMAGE
|
||||||
-n PROJECT, --project PROJECT Project Name
|
Full Image Name
|
||||||
--email EMAIL Add email for the SSL.
|
-q, --no-ssl No https
|
||||||
|
-m HTTP_PORT, --http-port HTTP_PORT
|
||||||
|
Http port in case of no-ssl
|
||||||
|
-v VERSION, --version VERSION
|
||||||
|
ERPNext version to install, defaults to latest stable
|
||||||
|
-a APPS, --app APPS list of app(s) to be installed
|
||||||
|
-s SITES, --sitename SITES
|
||||||
|
Site Name(s) for your production bench
|
||||||
|
-e EMAIL, --email EMAIL
|
||||||
|
Add email for the SSL.
|
||||||
|
-p, --push Push the built image to registry
|
||||||
|
-r FRAPPE_PATH, --frappe-path FRAPPE_PATH
|
||||||
|
Frappe Repository to use, default: https://github.com/frappe/frappe
|
||||||
|
-b FRAPPE_BRANCH, --frappe-branch FRAPPE_BRANCH
|
||||||
|
Frappe branch to use, default: version-15
|
||||||
|
-j APPS_JSON, --apps-json APPS_JSON
|
||||||
|
Path to apps json, default: frappe_docker/development/apps-example.json
|
||||||
|
-t TAGS, --tag TAGS Full Image Name(s), default: custom-apps:latest
|
||||||
|
-c CONTAINERFILE, --containerfile CONTAINERFILE
|
||||||
|
Path to Containerfile: images/layered/Containerfile
|
||||||
|
-y PYTHON_VERSION, --python-version PYTHON_VERSION
|
||||||
|
Python Version, default: 3.11.6
|
||||||
|
-d NODE_VERSION, --node-version NODE_VERSION
|
||||||
|
NodeJS Version, default: 18.18.2
|
||||||
|
-x, --deploy Deploy after build
|
||||||
|
-u, --upgrade Upgrade after build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Deploy using compose**
|
||||||
|
|
||||||
|
```txt
|
||||||
|
usage: easy-install.py deploy [-h] [-n PROJECT] [-i IMAGE] [-q] [-m HTTP_PORT] [-v VERSION] [-a APPS] [-s SITES] [-e EMAIL]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-n PROJECT, --project PROJECT
|
||||||
|
Project Name
|
||||||
|
-i IMAGE, --image IMAGE
|
||||||
|
Full Image Name
|
||||||
|
-q, --no-ssl No https
|
||||||
|
-m HTTP_PORT, --http-port HTTP_PORT
|
||||||
|
Http port in case of no-ssl
|
||||||
|
-v VERSION, --version VERSION
|
||||||
|
ERPNext version to install, defaults to latest stable
|
||||||
|
-a APPS, --app APPS list of app(s) to be installed
|
||||||
|
-s SITES, --sitename SITES
|
||||||
|
Site Name(s) for your production bench
|
||||||
|
-e EMAIL, --email EMAIL
|
||||||
|
Add email for the SSL.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Upgrade existing project**
|
||||||
|
|
||||||
|
```txt
|
||||||
|
usage: easy-install.py upgrade [-h] [-n PROJECT] [-i IMAGE] [-q] [-m HTTP_PORT] [-v VERSION]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-n PROJECT, --project PROJECT
|
||||||
|
Project Name
|
||||||
|
-i IMAGE, --image IMAGE
|
||||||
|
Full Image Name
|
||||||
|
-q, --no-ssl No https
|
||||||
|
-m HTTP_PORT, --http-port HTTP_PORT
|
||||||
|
Http port in case of no-ssl
|
||||||
|
-v VERSION, --version VERSION
|
||||||
|
ERPNext or image version to install, defaults to latest stable
|
||||||
|
```
|
||||||
|
|
||||||
|
**Development setup using compose**
|
||||||
|
|
||||||
|
```txt
|
||||||
|
usage: easy-install.py develop [-h] [-n PROJECT]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-n PROJECT, --project PROJECT
|
||||||
|
Compose project name
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exec into existing project**
|
||||||
|
|
||||||
|
```txt
|
||||||
|
usage: easy-install.py exec [-h] [-n PROJECT]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-n PROJECT, --project PROJECT
|
||||||
|
Project Name
|
||||||
|
```
|
||||||
|
|
||||||
|
To use custom apps, you need to create a json file with list of apps and pass it to build command.
|
||||||
|
|
||||||
|
Example apps.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"url": "https://github.com/frappe/wiki.git",
|
||||||
|
"branch": "master"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute following command to build and deploy above apps:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ python3 easy-install.py build \
|
||||||
|
--tag=ghcr.io/org/repo/custom-apps:latest \
|
||||||
|
--push \
|
||||||
|
--image=ghcr.io/org/repo/custom-apps \
|
||||||
|
--version=latest \
|
||||||
|
--deploy \
|
||||||
|
--project=actions_test \
|
||||||
|
--email=test@frappe.io \
|
||||||
|
--apps-json=apps.json \
|
||||||
|
--app=wiki
|
||||||
|
```
|
||||||
|
|
||||||
|
Note:
|
||||||
|
|
||||||
|
- `--tag`, tag to set for built image, can be multiple.
|
||||||
|
- `--push`, push the built image.
|
||||||
|
- `--image`, the image to use when starting docker compose project.
|
||||||
|
- `--version`, the version to use when starting docker compose project.
|
||||||
|
- `--app`, app to install on site creation, can be multiple.
|
||||||
|
- `--deploy`, flag to deploy after build/push is complete
|
||||||
|
- `--project=actions_test`, name of the project, compose file with project name will be stored in user home directory.
|
||||||
|
- `--email=test@frappe.io`, valid email for letsencrypt certificate expiry notification.
|
||||||
|
- `--apps-json`, path to json file with list of apps to be added to bench.
|
||||||
|
|
||||||
#### Troubleshooting
|
#### Troubleshooting
|
||||||
|
|
||||||
In case the setup fails, the log file is saved under `$HOME/easy-install.log`. You may then
|
In case the setup fails, the log file is saved under `$HOME/easy-install.log`. You may then
|
||||||
|
587
easy-install.py
587
easy-install.py
@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import base64
|
||||||
import fileinput
|
import fileinput
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -10,7 +11,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from shutil import move, unpack_archive, which
|
from shutil import move, unpack_archive, which
|
||||||
from typing import Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
filename="easy-install.log",
|
filename="easy-install.log",
|
||||||
@ -49,9 +50,8 @@ def clone_frappe_docker_repo() -> None:
|
|||||||
"frappe_docker.zip",
|
"frappe_docker.zip",
|
||||||
)
|
)
|
||||||
logging.info("Downloaded frappe_docker zip file from GitHub")
|
logging.info("Downloaded frappe_docker zip file from GitHub")
|
||||||
unpack_archive(
|
unpack_archive("frappe_docker.zip", ".")
|
||||||
"frappe_docker.zip", "."
|
# Unzipping the frappe_docker.zip creates a folder "frappe_docker-main"
|
||||||
) # Unzipping the frappe_docker.zip creates a folder "frappe_docker-main"
|
|
||||||
move("frappe_docker-main", "frappe_docker")
|
move("frappe_docker-main", "frappe_docker")
|
||||||
logging.info("Unzipped and Renamed frappe_docker")
|
logging.info("Unzipped and Renamed frappe_docker")
|
||||||
os.remove("frappe_docker.zip")
|
os.remove("frappe_docker.zip")
|
||||||
@ -74,19 +74,19 @@ def get_from_env(dir, file) -> Dict:
|
|||||||
|
|
||||||
def write_to_env(
|
def write_to_env(
|
||||||
wd: str,
|
wd: str,
|
||||||
sites,
|
sites: List[str],
|
||||||
db_pass: str,
|
db_pass: str,
|
||||||
admin_pass: str,
|
admin_pass: str,
|
||||||
email: str,
|
email: str,
|
||||||
erpnext_version: str = None,
|
erpnext_version: str = None,
|
||||||
|
http_port: str = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
quoted_sites = ",".join([f"`{site}`" for site in sites]).strip(",")
|
quoted_sites = ",".join([f"`{site}`" for site in sites]).strip(",")
|
||||||
example_env = get_from_env(wd, "example.env")
|
example_env = get_from_env(wd, "example.env")
|
||||||
erpnext_version = erpnext_version or example_env["ERPNEXT_VERSION"]
|
erpnext_version = erpnext_version or example_env["ERPNEXT_VERSION"]
|
||||||
with open(os.path.join(wd, ".env"), "w") as f:
|
env_file_lines = [
|
||||||
f.writelines(
|
# defaults to latest version of ERPNext
|
||||||
[
|
f"ERPNEXT_VERSION={erpnext_version}\n",
|
||||||
f"ERPNEXT_VERSION={erpnext_version}\n", # defaults to latest version of ERPNext
|
|
||||||
f"DB_PASSWORD={db_pass}\n",
|
f"DB_PASSWORD={db_pass}\n",
|
||||||
"DB_HOST=db\n",
|
"DB_HOST=db\n",
|
||||||
"DB_PORT=3306\n",
|
"DB_PORT=3306\n",
|
||||||
@ -96,8 +96,14 @@ def write_to_env(
|
|||||||
f"LETSENCRYPT_EMAIL={email}\n",
|
f"LETSENCRYPT_EMAIL={email}\n",
|
||||||
f"SITE_ADMIN_PASS={admin_pass}\n",
|
f"SITE_ADMIN_PASS={admin_pass}\n",
|
||||||
f"SITES={quoted_sites}\n",
|
f"SITES={quoted_sites}\n",
|
||||||
|
"PULL_POLICY=missing\n",
|
||||||
]
|
]
|
||||||
)
|
|
||||||
|
if http_port:
|
||||||
|
env_file_lines.append(f"HTTP_PUBLISH_PORT={http_port}\n")
|
||||||
|
|
||||||
|
with open(os.path.join(wd, ".env"), "w") as f:
|
||||||
|
f.writelines(env_file_lines)
|
||||||
|
|
||||||
|
|
||||||
def generate_pass(length: int = 12) -> str:
|
def generate_pass(length: int = 12) -> str:
|
||||||
@ -115,12 +121,23 @@ def check_repo_exists() -> bool:
|
|||||||
return os.path.exists(os.path.join(os.getcwd(), "frappe_docker"))
|
return os.path.exists(os.path.join(os.getcwd(), "frappe_docker"))
|
||||||
|
|
||||||
|
|
||||||
def setup_prod(project: str, sites, email: str, version: str = None, image = None) -> None:
|
def start_prod(
|
||||||
if len(sites) == 0:
|
project: str,
|
||||||
sites = ["site1.localhost"]
|
sites: List[str] = [],
|
||||||
|
email: str = None,
|
||||||
|
version: str = None,
|
||||||
|
image: str = None,
|
||||||
|
is_https: bool = True,
|
||||||
|
http_port: str = None,
|
||||||
|
):
|
||||||
|
if not check_repo_exists():
|
||||||
|
clone_frappe_docker_repo()
|
||||||
|
install_container_runtime()
|
||||||
|
|
||||||
if check_repo_exists():
|
compose_file_name = os.path.join(
|
||||||
compose_file_name = os.path.join(os.path.expanduser("~"), f"{project}-compose.yml")
|
os.path.expanduser("~"),
|
||||||
|
f"{project}-compose.yml",
|
||||||
|
)
|
||||||
docker_repo_path = os.path.join(os.getcwd(), "frappe_docker")
|
docker_repo_path = os.path.join(os.getcwd(), "frappe_docker")
|
||||||
cprint(
|
cprint(
|
||||||
"\nPlease refer to .example.env file in the frappe_docker folder to know which keys to set\n\n",
|
"\nPlease refer to .example.env file in the frappe_docker folder to know which keys to set\n\n",
|
||||||
@ -133,23 +150,43 @@ def setup_prod(project: str, sites, email: str, version: str = None, image = Non
|
|||||||
if not os.path.exists(os.path.join(docker_repo_path, ".env")):
|
if not os.path.exists(os.path.join(docker_repo_path, ".env")):
|
||||||
admin_pass = generate_pass()
|
admin_pass = generate_pass()
|
||||||
db_pass = generate_pass(9)
|
db_pass = generate_pass(9)
|
||||||
write_to_env(docker_repo_path, sites, db_pass, admin_pass, email, version)
|
write_to_env(
|
||||||
|
wd=docker_repo_path,
|
||||||
|
sites=sites,
|
||||||
|
db_pass=db_pass,
|
||||||
|
admin_pass=admin_pass,
|
||||||
|
email=email,
|
||||||
|
erpnext_version=version,
|
||||||
|
http_port=http_port if not is_https and http_port else None,
|
||||||
|
)
|
||||||
cprint(
|
cprint(
|
||||||
"\nA .env file is generated with basic configs. Please edit it to fit to your needs \n",
|
"\nA .env file is generated with basic configs. Please edit it to fit to your needs \n",
|
||||||
level=3,
|
level=3,
|
||||||
)
|
)
|
||||||
with open(os.path.join(os.path.expanduser("~"), "passwords.txt"), "w") as en:
|
with open(
|
||||||
|
os.path.join(os.path.expanduser("~"), "passwords.txt"), "w"
|
||||||
|
) as en:
|
||||||
en.writelines(f"ADMINISTRATOR_PASSWORD={admin_pass}\n")
|
en.writelines(f"ADMINISTRATOR_PASSWORD={admin_pass}\n")
|
||||||
en.writelines(f"MARIADB_ROOT_PASSWORD={db_pass}\n")
|
en.writelines(f"MARIADB_ROOT_PASSWORD={db_pass}\n")
|
||||||
else:
|
else:
|
||||||
env = get_from_env(docker_repo_path, ".env")
|
env = get_from_env(docker_repo_path, ".env")
|
||||||
admin_pass = env["SITE_ADMIN_PASS"]
|
sites = env["SITES"].replace("`", "").split(",") if env["SITES"] else []
|
||||||
db_pass = env["DB_PASSWORD"]
|
db_pass = env["DB_PASSWORD"]
|
||||||
|
admin_pass = env["SITE_ADMIN_PASS"]
|
||||||
|
email = env["LETSENCRYPT_EMAIL"]
|
||||||
|
write_to_env(
|
||||||
|
wd=docker_repo_path,
|
||||||
|
sites=sites,
|
||||||
|
db_pass=db_pass,
|
||||||
|
admin_pass=admin_pass,
|
||||||
|
email=email,
|
||||||
|
erpnext_version=version,
|
||||||
|
http_port=http_port if not is_https and http_port else None,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# TODO: Include flags for non-https and non-erpnext installation
|
command = [
|
||||||
subprocess.run(
|
"docker",
|
||||||
[
|
|
||||||
which("docker"),
|
|
||||||
"compose",
|
"compose",
|
||||||
"--project-name",
|
"--project-name",
|
||||||
project,
|
project,
|
||||||
@ -159,13 +196,19 @@ def setup_prod(project: str, sites, email: str, version: str = None, image = Non
|
|||||||
"overrides/compose.mariadb.yaml",
|
"overrides/compose.mariadb.yaml",
|
||||||
"-f",
|
"-f",
|
||||||
"overrides/compose.redis.yaml",
|
"overrides/compose.redis.yaml",
|
||||||
# "-f", "overrides/compose.noproxy.yaml", TODO: Add support for local proxying without HTTPs
|
|
||||||
"-f",
|
"-f",
|
||||||
"overrides/compose.https.yaml",
|
(
|
||||||
|
"overrides/compose.https.yaml"
|
||||||
|
if is_https
|
||||||
|
else "overrides/compose.noproxy.yaml"
|
||||||
|
),
|
||||||
"--env-file",
|
"--env-file",
|
||||||
".env",
|
".env",
|
||||||
"config",
|
"config",
|
||||||
],
|
]
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
cwd=docker_repo_path,
|
cwd=docker_repo_path,
|
||||||
stdout=f,
|
stdout=f,
|
||||||
check=True,
|
check=True,
|
||||||
@ -185,17 +228,20 @@ def setup_prod(project: str, sites, email: str, version: str = None, image = Non
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Starting with generated compose file
|
# Starting with generated compose file
|
||||||
subprocess.run(
|
command = [
|
||||||
[
|
"docker",
|
||||||
which("docker"),
|
|
||||||
"compose",
|
"compose",
|
||||||
"-p",
|
"-p",
|
||||||
project,
|
project,
|
||||||
"-f",
|
"-f",
|
||||||
compose_file_name,
|
compose_file_name,
|
||||||
"up",
|
"up",
|
||||||
|
"--force-recreate",
|
||||||
|
"--remove-orphans",
|
||||||
"-d",
|
"-d",
|
||||||
],
|
]
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
logging.info(f"Docker Compose file generated at ~/{project}-compose.yml")
|
logging.info(f"Docker Compose file generated at ~/{project}-compose.yml")
|
||||||
@ -205,20 +251,74 @@ def setup_prod(project: str, sites, email: str, version: str = None, image = Non
|
|||||||
cprint(" Docker Compose failed, please check the container logs\n", e)
|
cprint(" Docker Compose failed, please check the container logs\n", e)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
for sitename in sites:
|
return db_pass, admin_pass
|
||||||
create_site(sitename, project, db_pass, admin_pass)
|
|
||||||
|
|
||||||
else:
|
|
||||||
install_docker()
|
def setup_prod(
|
||||||
clone_frappe_docker_repo()
|
project: str,
|
||||||
setup_prod(project, sites, email, version, image) # Recursive
|
sites: List[str],
|
||||||
|
email: str,
|
||||||
|
version: str = None,
|
||||||
|
image: str = None,
|
||||||
|
apps: List[str] = [],
|
||||||
|
is_https: bool = False,
|
||||||
|
http_port: str = None,
|
||||||
|
) -> None:
|
||||||
|
if len(sites) == 0:
|
||||||
|
sites = ["site1.localhost"]
|
||||||
|
|
||||||
|
db_pass, admin_pass = start_prod(
|
||||||
|
project=project,
|
||||||
|
sites=sites,
|
||||||
|
email=email,
|
||||||
|
version=version,
|
||||||
|
image=image,
|
||||||
|
is_https=is_https,
|
||||||
|
http_port=http_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
for sitename in sites:
|
||||||
|
create_site(sitename, project, db_pass, admin_pass, apps)
|
||||||
|
|
||||||
|
cprint(
|
||||||
|
f"MariaDB root password is {db_pass}",
|
||||||
|
level=2,
|
||||||
|
)
|
||||||
|
cprint(
|
||||||
|
f"Site administrator password is {admin_pass}",
|
||||||
|
level=2,
|
||||||
|
)
|
||||||
|
passwords_file_path = os.path.join(
|
||||||
|
os.path.expanduser("~"),
|
||||||
|
"passwords.txt",
|
||||||
|
)
|
||||||
|
cprint(f"Passwords are stored in {passwords_file_path}", level=3)
|
||||||
|
|
||||||
|
|
||||||
|
def update_prod(
|
||||||
|
project: str,
|
||||||
|
version: str = None,
|
||||||
|
image=None,
|
||||||
|
is_https: bool = False,
|
||||||
|
http_port: str = None,
|
||||||
|
) -> None:
|
||||||
|
start_prod(
|
||||||
|
project=project,
|
||||||
|
version=version,
|
||||||
|
image=image,
|
||||||
|
is_https=is_https,
|
||||||
|
http_port=http_port,
|
||||||
|
)
|
||||||
|
migrate_site(project=project)
|
||||||
|
|
||||||
|
|
||||||
def setup_dev_instance(project: str):
|
def setup_dev_instance(project: str):
|
||||||
if check_repo_exists():
|
if not check_repo_exists():
|
||||||
|
clone_frappe_docker_repo()
|
||||||
|
install_container_runtime()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
command = [
|
||||||
[
|
|
||||||
"docker",
|
"docker",
|
||||||
"compose",
|
"compose",
|
||||||
"-f",
|
"-f",
|
||||||
@ -227,7 +327,9 @@ def setup_dev_instance(project: str):
|
|||||||
project,
|
project,
|
||||||
"up",
|
"up",
|
||||||
"-d",
|
"-d",
|
||||||
],
|
]
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
cwd=os.path.join(os.getcwd(), "frappe_docker"),
|
cwd=os.path.join(os.getcwd(), "frappe_docker"),
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
@ -239,19 +341,13 @@ def setup_dev_instance(project: str):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Dev Environment setup failed", exc_info=True)
|
logging.error("Dev Environment setup failed", exc_info=True)
|
||||||
cprint("Setting Up Development Environment Failed\n", e)
|
cprint("Setting Up Development Environment Failed\n", e)
|
||||||
else:
|
|
||||||
install_docker()
|
|
||||||
clone_frappe_docker_repo()
|
|
||||||
setup_dev_instance(project) # Recursion on goes brrrr
|
|
||||||
|
|
||||||
|
|
||||||
def install_docker():
|
def install_docker():
|
||||||
if which("docker") is not None:
|
|
||||||
return
|
|
||||||
cprint("Docker is not installed, Installing Docker...", level=3)
|
cprint("Docker is not installed, Installing Docker...", level=3)
|
||||||
logging.info("Docker not found, installing Docker")
|
logging.info("Docker not found, installing Docker")
|
||||||
if platform.system() == "Darwin" or platform.system() == "Windows":
|
if platform.system() == "Darwin" or platform.system() == "Windows":
|
||||||
print(
|
cprint(
|
||||||
f"""
|
f"""
|
||||||
This script doesn't install Docker on {"Mac" if platform.system()=="Darwin" else "Windows"}.
|
This script doesn't install Docker on {"Mac" if platform.system()=="Darwin" else "Windows"}.
|
||||||
|
|
||||||
@ -267,11 +363,26 @@ def install_docker():
|
|||||||
)
|
)
|
||||||
subprocess.run(["/bin/bash"], input=ps.stdout, capture_output=True)
|
subprocess.run(["/bin/bash"], input=ps.stdout, capture_output=True)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["sudo", "usermod", "-aG", "docker", str(os.getenv("USER"))], check=True
|
[
|
||||||
|
"sudo",
|
||||||
|
"usermod",
|
||||||
|
"-aG",
|
||||||
|
"docker",
|
||||||
|
str(os.getenv("USER")),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
)
|
)
|
||||||
cprint("Waiting Docker to start", level=3)
|
cprint("Waiting Docker to start", level=3)
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
subprocess.run(["sudo", "systemctl", "restart", "docker.service"], check=True)
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"sudo",
|
||||||
|
"systemctl",
|
||||||
|
"restart",
|
||||||
|
"docker.service",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Installing Docker failed", exc_info=True)
|
logging.error("Installing Docker failed", exc_info=True)
|
||||||
cprint("Failed to Install Docker\n", e)
|
cprint("Failed to Install Docker\n", e)
|
||||||
@ -279,18 +390,25 @@ def install_docker():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def install_container_runtime(runtime="docker"):
|
||||||
|
if which(runtime) is not None:
|
||||||
|
cprint(runtime.title() + " is already installed", level=2)
|
||||||
|
return
|
||||||
|
if runtime == "docker":
|
||||||
|
install_docker()
|
||||||
|
|
||||||
|
|
||||||
def create_site(
|
def create_site(
|
||||||
sitename: str,
|
sitename: str,
|
||||||
project: str,
|
project: str,
|
||||||
db_pass: str,
|
db_pass: str,
|
||||||
admin_pass: str,
|
admin_pass: str,
|
||||||
|
apps: List[str] = [],
|
||||||
):
|
):
|
||||||
|
apps = apps or []
|
||||||
cprint(f"\nCreating site: {sitename} \n", level=3)
|
cprint(f"\nCreating site: {sitename} \n", level=3)
|
||||||
|
command = [
|
||||||
try:
|
"docker",
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
which("docker"),
|
|
||||||
"compose",
|
"compose",
|
||||||
"-p",
|
"-p",
|
||||||
project,
|
project,
|
||||||
@ -298,16 +416,20 @@ def create_site(
|
|||||||
"backend",
|
"backend",
|
||||||
"bench",
|
"bench",
|
||||||
"new-site",
|
"new-site",
|
||||||
sitename,
|
|
||||||
"--no-mariadb-socket",
|
"--no-mariadb-socket",
|
||||||
"--db-root-password",
|
f"--db-root-password={db_pass}",
|
||||||
db_pass,
|
f"--admin-password={admin_pass}",
|
||||||
"--admin-password",
|
]
|
||||||
admin_pass,
|
|
||||||
"--install-app",
|
for app in apps:
|
||||||
"erpnext",
|
command.append("--install-app")
|
||||||
"--set-default",
|
command.append(app)
|
||||||
],
|
|
||||||
|
command.append(sitename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
logging.info("New site creation completed")
|
logging.info("New site creation completed")
|
||||||
@ -316,13 +438,68 @@ def create_site(
|
|||||||
cprint(f"Bench Site creation failed for {sitename}\n", e)
|
cprint(f"Bench Site creation failed for {sitename}\n", e)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def migrate_site(project: str):
|
||||||
parser = argparse.ArgumentParser(description="Install Frappe with Docker")
|
cprint(f"\nMigrating sites for {project}", level=3)
|
||||||
parser.add_argument(
|
|
||||||
"-p", "--prod", help="Setup Production System", action="store_true"
|
exec_command(
|
||||||
|
project=project,
|
||||||
|
command=[
|
||||||
|
"bench",
|
||||||
|
"--site",
|
||||||
|
"all",
|
||||||
|
"migrate",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def exec_command(project: str, command: List[str] = [], interactive_terminal=False):
|
||||||
|
if not command:
|
||||||
|
command = ["echo", '"Please execute a command"']
|
||||||
|
|
||||||
|
cprint(f"\nExecuting Command:\n{' '.join(command)}", level=3)
|
||||||
|
exec_command = [
|
||||||
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"-p",
|
||||||
|
project,
|
||||||
|
"exec",
|
||||||
|
]
|
||||||
|
|
||||||
|
if interactive_terminal:
|
||||||
|
exec_command.append("-it")
|
||||||
|
|
||||||
|
exec_command.append("backend")
|
||||||
|
exec_command += command
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
exec_command,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
logging.info("New site creation completed")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Exec command failed for {project}", exc_info=True)
|
||||||
|
cprint(f"Exec command failed for {project}\n", e)
|
||||||
|
|
||||||
|
|
||||||
|
def add_project_option(parser: argparse.ArgumentParser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-d", "--dev", help="Setup Development System", action="store_true"
|
"-n",
|
||||||
|
"--project",
|
||||||
|
help="Project Name",
|
||||||
|
default="frappe",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def add_setup_options(parser: argparse.ArgumentParser):
|
||||||
|
parser.add_argument(
|
||||||
|
"-a",
|
||||||
|
"--app",
|
||||||
|
dest="apps",
|
||||||
|
default=[],
|
||||||
|
help="list of app(s) to be installed",
|
||||||
|
action="append",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-s",
|
"-s",
|
||||||
@ -332,25 +509,279 @@ if __name__ == "__main__":
|
|||||||
action="append",
|
action="append",
|
||||||
dest="sites",
|
dest="sites",
|
||||||
)
|
)
|
||||||
parser.add_argument("-n", "--project", help="Project Name", default="frappe")
|
parser.add_argument("-e", "--email", help="Add email for the SSL.")
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def add_common_parser(parser: argparse.ArgumentParser):
|
||||||
|
parser = add_project_option(parser)
|
||||||
parser.add_argument("-i", "--image", help="Full Image Name")
|
parser.add_argument("-i", "--image", help="Full Image Name")
|
||||||
|
parser.add_argument("-q", "--no-ssl", action="store_true", help="No https")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--email", help="Add email for the SSL.", required="--prod" in sys.argv
|
"-m", "--http-port", help="Http port in case of no-ssl", default="8080"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-v", "--version", help="ERPNext version to install, defaults to latest stable"
|
"-v",
|
||||||
|
"--version",
|
||||||
|
help="ERPNext or image version to install, defaults to latest stable",
|
||||||
)
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def add_build_parser(subparsers: argparse.ArgumentParser):
|
||||||
|
parser = subparsers.add_parser("build", help="Build custom images")
|
||||||
|
parser = add_common_parser(parser)
|
||||||
|
parser = add_setup_options(parser)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--push",
|
||||||
|
help="Push the built image to registry",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--frappe-path",
|
||||||
|
help="Frappe Repository to use, default: https://github.com/frappe/frappe",
|
||||||
|
default="https://github.com/frappe/frappe",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-b",
|
||||||
|
"--frappe-branch",
|
||||||
|
help="Frappe branch to use, default: version-15",
|
||||||
|
default="version-15",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-j",
|
||||||
|
"--apps-json",
|
||||||
|
help="Path to apps json, default: frappe_docker/development/apps-example.json",
|
||||||
|
default="frappe_docker/development/apps-example.json",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-t",
|
||||||
|
"--tag",
|
||||||
|
dest="tags",
|
||||||
|
help="Full Image Name(s), default: custom-apps:latest",
|
||||||
|
action="append",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-c",
|
||||||
|
"--containerfile",
|
||||||
|
help="Path to Containerfile: images/layered/Containerfile",
|
||||||
|
default="images/layered/Containerfile",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-y",
|
||||||
|
"--python-version",
|
||||||
|
help="Python Version, default: 3.11.6",
|
||||||
|
default="3.11.6",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--node-version",
|
||||||
|
help="NodeJS Version, default: 18.18.2",
|
||||||
|
default="18.18.2",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-x",
|
||||||
|
"--deploy",
|
||||||
|
help="Deploy after build",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-u",
|
||||||
|
"--upgrade",
|
||||||
|
help="Upgrade after build",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_deploy_parser(subparsers: argparse.ArgumentParser):
|
||||||
|
parser = subparsers.add_parser("deploy", help="Deploy using compose")
|
||||||
|
parser = add_common_parser(parser)
|
||||||
|
parser = add_setup_options(parser)
|
||||||
|
|
||||||
|
|
||||||
|
def add_develop_parser(subparsers: argparse.ArgumentParser):
|
||||||
|
parser = subparsers.add_parser("develop", help="Development setup using compose")
|
||||||
|
parser.add_argument(
|
||||||
|
"-n", "--project", default="frappe", help="Compose project name"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_upgrade_parser(subparsers: argparse.ArgumentParser):
|
||||||
|
parser = subparsers.add_parser("upgrade", help="Upgrade existing project")
|
||||||
|
parser = add_common_parser(parser)
|
||||||
|
|
||||||
|
|
||||||
|
def add_exec_parser(subparsers: argparse.ArgumentParser):
|
||||||
|
parser = subparsers.add_parser("exec", help="Exec into existing project")
|
||||||
|
parser = add_project_option(parser)
|
||||||
|
|
||||||
|
|
||||||
|
def build_image(
|
||||||
|
push: bool,
|
||||||
|
frappe_path: str,
|
||||||
|
frappe_branch: str,
|
||||||
|
containerfile_path: str,
|
||||||
|
apps_json_path: str,
|
||||||
|
tags: List[str],
|
||||||
|
python_version: str,
|
||||||
|
node_version: str,
|
||||||
|
):
|
||||||
|
if not check_repo_exists():
|
||||||
|
clone_frappe_docker_repo()
|
||||||
|
install_container_runtime()
|
||||||
|
|
||||||
|
if not tags:
|
||||||
|
tags = ["custom-apps:latest"]
|
||||||
|
|
||||||
|
apps_json_base64 = None
|
||||||
|
try:
|
||||||
|
with open(apps_json_path, "rb") as file_text:
|
||||||
|
file_read = file_text.read()
|
||||||
|
apps_json_base64 = (
|
||||||
|
base64.encodebytes(file_read).decode("utf-8").replace("\n", "")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Unable to base64 encode apps.json", exc_info=True)
|
||||||
|
cprint("\nUnable to base64 encode apps.json\n\n", "[ERROR]: ", e, level=1)
|
||||||
|
|
||||||
|
command = [
|
||||||
|
which("docker"),
|
||||||
|
"build",
|
||||||
|
"--progress=plain",
|
||||||
|
]
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
command.append(f"--tag={tag}")
|
||||||
|
|
||||||
|
command += [
|
||||||
|
f"--file={containerfile_path}",
|
||||||
|
f"--build-arg=FRAPPE_PATH={frappe_path}",
|
||||||
|
f"--build-arg=FRAPPE_BRANCH={frappe_branch}",
|
||||||
|
f"--build-arg=PYTHON_VERSION={python_version}",
|
||||||
|
f"--build-arg=NODE_VERSION={node_version}",
|
||||||
|
f"--build-arg=APPS_JSON_BASE64={apps_json_base64}",
|
||||||
|
".",
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
|
check=True,
|
||||||
|
cwd="frappe_docker",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Image build failed", exc_info=True)
|
||||||
|
cprint("\nImage build failed\n\n", "[ERROR]: ", e, level=1)
|
||||||
|
|
||||||
|
if push:
|
||||||
|
try:
|
||||||
|
for tag in tags:
|
||||||
|
subprocess.run(
|
||||||
|
[which("docker"), "push", tag],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Image push failed", exc_info=True)
|
||||||
|
cprint("\nImage push failed\n\n", "[ERROR]: ", e, level=1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_args_parser():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Easy install script for Frappe Framework"
|
||||||
|
)
|
||||||
|
# Setup sub-commands
|
||||||
|
subparsers = parser.add_subparsers(dest="subcommand")
|
||||||
|
# Build command
|
||||||
|
add_build_parser(subparsers)
|
||||||
|
# Deploy command
|
||||||
|
add_deploy_parser(subparsers)
|
||||||
|
# Upgrade command
|
||||||
|
add_upgrade_parser(subparsers)
|
||||||
|
# Develop command
|
||||||
|
add_develop_parser(subparsers)
|
||||||
|
# Exec command
|
||||||
|
add_exec_parser(subparsers)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = get_args_parser()
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
parser.print_help(sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.dev:
|
|
||||||
cprint("\nSetting Up Development Instance\n", level=2)
|
if args.subcommand == "build":
|
||||||
logging.info("Running Development Setup")
|
build_image(
|
||||||
setup_dev_instance(args.project)
|
push=args.push,
|
||||||
elif args.prod:
|
frappe_path=args.frappe_path,
|
||||||
|
frappe_branch=args.frappe_branch,
|
||||||
|
apps_json_path=args.apps_json,
|
||||||
|
tags=args.tags,
|
||||||
|
containerfile_path=args.containerfile,
|
||||||
|
python_version=args.python_version,
|
||||||
|
node_version=args.node_version,
|
||||||
|
)
|
||||||
|
if args.deploy:
|
||||||
|
setup_prod(
|
||||||
|
project=args.project,
|
||||||
|
sites=args.sites,
|
||||||
|
email=args.email,
|
||||||
|
version=args.version,
|
||||||
|
image=args.image,
|
||||||
|
apps=args.apps,
|
||||||
|
is_https=not args.no_ssl,
|
||||||
|
http_port=args.http_port,
|
||||||
|
)
|
||||||
|
elif args.upgrade:
|
||||||
|
update_prod(
|
||||||
|
project=args.project,
|
||||||
|
version=args.version,
|
||||||
|
image=args.image,
|
||||||
|
is_https=not args.no_ssl,
|
||||||
|
http_port=args.http_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif args.subcommand == "deploy":
|
||||||
cprint("\nSetting Up Production Instance\n", level=2)
|
cprint("\nSetting Up Production Instance\n", level=2)
|
||||||
logging.info("Running Production Setup")
|
logging.info("Running Production Setup")
|
||||||
if "example.com" in args.email:
|
if "example.com" in args.email:
|
||||||
cprint("Emails with example.com not acceptable", level=1)
|
cprint("Emails with example.com not acceptable", level=1)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
setup_prod(args.project, args.sites, args.email, args.version, args.image)
|
setup_prod(
|
||||||
else:
|
project=args.project,
|
||||||
parser.print_help()
|
sites=args.sites,
|
||||||
|
email=args.email,
|
||||||
|
version=args.version,
|
||||||
|
image=args.image,
|
||||||
|
apps=args.apps,
|
||||||
|
is_https=not args.no_ssl,
|
||||||
|
http_port=args.http_port,
|
||||||
|
)
|
||||||
|
elif args.subcommand == "develop":
|
||||||
|
cprint("\nSetting Up Development Instance\n", level=2)
|
||||||
|
logging.info("Running Development Setup")
|
||||||
|
setup_dev_instance(args.project)
|
||||||
|
elif args.subcommand == "upgrade":
|
||||||
|
cprint("\nUpgrading Production Instance\n", level=2)
|
||||||
|
logging.info("Upgrading Development Setup")
|
||||||
|
update_prod(
|
||||||
|
project=args.project,
|
||||||
|
version=args.version,
|
||||||
|
image=args.image,
|
||||||
|
is_https=not args.no_ssl,
|
||||||
|
http_port=args.http_port,
|
||||||
|
)
|
||||||
|
elif args.subcommand == "exec":
|
||||||
|
cprint(f"\nExec into {args.project} backend\n", level=2)
|
||||||
|
logging.info(f"Exec into {args.project} backend")
|
||||||
|
exec_command(
|
||||||
|
project=args.project,
|
||||||
|
command=["bash"],
|
||||||
|
interactive_terminal=True,
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user