2
0
mirror of https://github.com/frappe/bench.git synced 2025-01-10 09:02:10 +00:00

Merge branch 'develop' into staging

This commit is contained in:
Ankush Menat 2023-01-02 17:02:56 +05:30
commit b0fec0fd4e
8 changed files with 428 additions and 21 deletions

View File

@ -6,6 +6,10 @@ on:
push: push:
branches: [ develop ] branches: [ develop ]
concurrency:
group: ci-develop-${{ github.event_name }}-${{ github.event.number }}
cancel-in-progress: true
permissions: permissions:
contents: read contents: read

32
.github/workflows/easy-install.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: "Easy Install Test"
on:
pull_request:
workflow_dispatch:
push:
branches: [develop]
concurrency:
group: easy-install-develop-${{ github.event_name }}-${{ github.event.number }}
cancel-in-progress: true
permissions:
contents: read
jobs:
easy-install-setup:
runs-on: ubuntu-latest
timeout-minutes: 60
name: Easy Install Test
steps:
- uses: actions/checkout@v3
- name: Perform production easy install
run: |
python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io
docker compose -p actions_test exec backend bench version --format json
docker compose -p actions_test exec backend bench --site site1.local list-apps --format json
result=$(curl -sk https://127.0.0.1/api/method/ping | jq -r ."message")
if [[ "$result" == "pong" ]]; then echo "New instance works fine"; else exit 1; fi
docker compose -p actions_test down
docker volume prune -f

View File

@ -31,12 +31,16 @@ Bench is a command-line utility that helps you to install, update, and manage mu
## Table of Contents ## Table of Contents
- [Table of Contents](#table-of-contents)
- [Installation](#installation) - [Installation](#installation)
- [Containerized Installation](#containerized-installation) - [Containerized Installation](#containerized-installation)
- [Easy Install Script](#easy-install-script)
- [Setup](#setup)
- [Arguments](#arguments)
- [Troubleshooting](#troubleshooting)
- [Manual Installation](#manual-installation) - [Manual Installation](#manual-installation)
- [Usage](#basic-usage) - [Basic Usage](#basic-usage)
- [Custom Bench commands](#custom-bench-commands) - [Custom Bench Commands](#custom-bench-commands)
- [Bench Manager](#bench-manager)
- [Guides](#guides) - [Guides](#guides)
- [Resources](#resources) - [Resources](#resources)
- [Development](#development) - [Development](#development)
@ -53,7 +57,7 @@ The setup for each of these installations can be achieved in multiple ways:
- [Containerized Installation](#containerized-installation) - [Containerized Installation](#containerized-installation)
- [Manual Installation](#manual-installation) - [Manual Installation](#manual-installation)
We recommend using either the Docker Installation to setup a Production Environment. For Development, you may choose either of the two methods to setup an instance. We recommend using Docker Installation to setup a Production Environment. For Development, you may choose either of the two methods to setup an instance.
Otherwise, if you are looking to evaluate Frappe apps without hassle of hosting, you can try them [on frappecloud.com](https://frappecloud.com/). Otherwise, if you are looking to evaluate Frappe apps without hassle of hosting, you can try them [on frappecloud.com](https://frappecloud.com/).
@ -71,6 +75,53 @@ $ cd frappe_docker
A quick setup guide for both the environments can be found below. For more details, check out the [Frappe/ERPNext Docker Repository](https://github.com/frappe/frappe_docker). A quick setup guide for both the environments can be found below. For more details, check out the [Frappe/ERPNext Docker Repository](https://github.com/frappe/frappe_docker).
### Easy Install Script
The Easy Install script should get you going with a Frappe/ERPNext setup with minimal manual intervention and effort.
This script uses Docker with the [Frappe/ERPNext Docker Repository](https://github.com/frappe/frappe_docker) and can be used for both Development setup and Production setup.
#### Setup
Download the Easy Install script and execute it:
```sh
$ wget https://raw.githubusercontent.com/frappe/bench/develop/easy-install.py
$ python3 easy-install.py --prod
```
This script will install docker on your system and will fetch the required containers, setup bench and a default ERPNext instance.
The script will generate MySQL root password and an Administrator password for the Frappe/ERPNext instance, which will then be saved under `$HOME/passwords.txt` of the user used to setup the instance.
It will also generate a new compose file under `$HOME/<project-name>-compose.yml`.
When the setup is complete, you will be able to access the system at `http://<your-server-ip>`, wherein you can use the Administrator password to login.
#### Arguments
Here are the arguments for the easy-install script
```txt
usage: easy-install.py [-h] [-p] [-d] [-s SITENAME] [-n PROJECT] [--email EMAIL]
Install Frappe with Docker
options:
-h, --help show this help message and exit
-p, --prod Setup Production System
-d, --dev Setup Development System
-s SITENAME, --sitename SITENAME
The Site Name for your production site
-n PROJECT, --project PROJECT
Project Name
--email EMAIL Add email for the SSL.
```
#### Troubleshooting
In case the setup fails, the log file is saved under `$HOME/easy-install.log`. You may then
- Create an Issue in this repository with the log file attached.
### Manual Installation ### Manual Installation

View File

@ -15,12 +15,10 @@ from bench.bench import Bench
PYTHON_VER = sys.version_info PYTHON_VER = sys.version_info
FRAPPE_BRANCH = "version-12" FRAPPE_BRANCH = "version-13-hotfix"
if PYTHON_VER.major == 3: if PYTHON_VER.major == 3:
if PYTHON_VER.minor >= 10: if PYTHON_VER.minor >= 10:
FRAPPE_BRANCH = "develop" FRAPPE_BRANCH = "develop"
if 7 >= PYTHON_VER.minor >= 9:
FRAPPE_BRANCH = "version-13"
class TestBenchBase(unittest.TestCase): class TestBenchBase(unittest.TestCase):

View File

@ -46,7 +46,7 @@ class TestBenchInit(TestBenchBase):
def test_multiple_benches(self): def test_multiple_benches(self):
for bench_name in ("test-bench-1", "test-bench-2"): for bench_name in ("test-bench-1", "test-bench-2"):
self.init_bench(bench_name) self.init_bench(bench_name, skip_assets=True)
self.assert_common_site_config( self.assert_common_site_config(
"test-bench-1", "test-bench-1",
@ -96,7 +96,7 @@ class TestBenchInit(TestBenchBase):
self.assertTrue(site_config[key]) self.assertTrue(site_config[key])
def test_get_app(self): def test_get_app(self):
self.init_bench("test-bench") self.init_bench("test-bench", skip_assets=True)
bench_path = os.path.join(self.benches_path, "test-bench") bench_path = os.path.join(self.benches_path, "test-bench")
exec_cmd(f"bench get-app {TEST_FRAPPE_APP} --skip-assets", cwd=bench_path) exec_cmd(f"bench get-app {TEST_FRAPPE_APP} --skip-assets", cwd=bench_path)
self.assertTrue(os.path.exists(os.path.join(bench_path, "apps", TEST_FRAPPE_APP))) self.assertTrue(os.path.exists(os.path.join(bench_path, "apps", TEST_FRAPPE_APP)))
@ -108,7 +108,7 @@ class TestBenchInit(TestBenchBase):
@unittest.skipIf(FRAPPE_BRANCH != "develop", "only for develop branch") @unittest.skipIf(FRAPPE_BRANCH != "develop", "only for develop branch")
def test_get_app_resolve_deps(self): def test_get_app_resolve_deps(self):
FRAPPE_APP = "healthcare" FRAPPE_APP = "healthcare"
self.init_bench("test-bench") self.init_bench("test-bench", skip_assets=True)
bench_path = os.path.join(self.benches_path, "test-bench") bench_path = os.path.join(self.benches_path, "test-bench")
exec_cmd(f"bench get-app {FRAPPE_APP} --resolve-deps --skip-assets", cwd=bench_path) exec_cmd(f"bench get-app {FRAPPE_APP} --resolve-deps --skip-assets", cwd=bench_path)
self.assertTrue(os.path.exists(os.path.join(bench_path, "apps", FRAPPE_APP))) self.assertTrue(os.path.exists(os.path.join(bench_path, "apps", FRAPPE_APP)))
@ -126,7 +126,7 @@ class TestBenchInit(TestBenchBase):
site_name = "install-app.test" site_name = "install-app.test"
bench_path = os.path.join(self.benches_path, "test-bench") bench_path = os.path.join(self.benches_path, "test-bench")
self.init_bench(bench_name) self.init_bench(bench_name, skip_assets=True)
exec_cmd( exec_cmd(
f"bench get-app {TEST_FRAPPE_APP} --branch master --skip-assets", cwd=bench_path f"bench get-app {TEST_FRAPPE_APP} --branch master --skip-assets", cwd=bench_path
) )
@ -154,7 +154,7 @@ class TestBenchInit(TestBenchBase):
self.assertTrue(TEST_FRAPPE_APP in app_installed_on_site) self.assertTrue(TEST_FRAPPE_APP in app_installed_on_site)
def test_remove_app(self): def test_remove_app(self):
self.init_bench("test-bench") self.init_bench("test-bench", skip_assets=True)
bench_path = os.path.join(self.benches_path, "test-bench") bench_path = os.path.join(self.benches_path, "test-bench")
exec_cmd( exec_cmd(
@ -172,7 +172,7 @@ class TestBenchInit(TestBenchBase):
self.assertFalse(os.path.exists(os.path.join(bench_path, "apps", TEST_FRAPPE_APP))) self.assertFalse(os.path.exists(os.path.join(bench_path, "apps", TEST_FRAPPE_APP)))
def test_switch_to_branch(self): def test_switch_to_branch(self):
self.init_bench("test-bench") self.init_bench("test-bench", skip_assets=True)
bench_path = os.path.join(self.benches_path, "test-bench") bench_path = os.path.join(self.benches_path, "test-bench")
app_path = os.path.join(bench_path, "apps", "frappe") app_path = os.path.join(bench_path, "apps", "frappe")

View File

@ -284,7 +284,7 @@ def get_current_version(app, bench_path="."):
with open(init_path) as f: with open(init_path) as f:
current_version = get_version_from_string(f.read()) current_version = get_version_from_string(f.read())
except AttributeError: except (AttributeError, VersionNotFound):
# backward compatibility # backward compatibility
with open(setup_path) as f: with open(setup_path) as f:
current_version = get_version_from_string(f.read(), field="version") current_version = get_version_from_string(f.read(), field="version")

322
easy-install.py Executable file
View File

@ -0,0 +1,322 @@
#!/usr/bin/env python3
import argparse
import logging
import os
import platform
import subprocess
import sys
import time
import urllib.request
from shutil import move, unpack_archive, which
from typing import Dict
logging.basicConfig(
filename="easy-install.log",
filemode="w",
format="%(asctime)s - %(levelname)s - %(message)s",
level=logging.INFO,
)
def cprint(*args, level: int = 1):
"""
logs colorful messages
level = 1 : RED
level = 2 : GREEN
level = 3 : YELLOW
default level = 1
"""
CRED = "\033[31m"
CGRN = "\33[92m"
CYLW = "\33[93m"
reset = "\033[0m"
message = " ".join(map(str, args))
if level == 1:
print(CRED, message, reset)
if level == 2:
print(CGRN, message, reset)
if level == 3:
print(CYLW, message, reset)
def clone_frappe_docker_repo() -> None:
try:
urllib.request.urlretrieve(
"https://github.com/frappe/frappe_docker/archive/refs/heads/main.zip",
"frappe_docker.zip",
)
logging.info("Downloaded frappe_docker zip file from GitHub")
unpack_archive(
"frappe_docker.zip", "."
) # Unzipping the frappe_docker.zip creates a folder "frappe_docker-main"
move("frappe_docker-main", "frappe_docker")
logging.info("Unzipped and Renamed frappe_docker")
os.remove("frappe_docker.zip")
logging.info("Removed the downloaded zip file")
except Exception as e:
logging.error("Download and unzip failed", exc_info=True)
cprint("\nCloning frappe_docker Failed\n\n", "[ERROR]: ", e, level=1)
def get_from_env(dir, file) -> Dict:
env_vars = {}
with open(os.path.join(dir, file)) as f:
for line in f:
if line.startswith("#") or not line.strip():
continue
key, value = line.strip().split("=", 1)
env_vars[key] = value
return env_vars
def write_to_env(wd: str, site: str, db_pass: str, admin_pass: str, email: str) -> None:
site_name = site or ""
example_env = get_from_env(wd, "example.env")
with open(os.path.join(wd, ".env"), "w") as f:
f.writelines(
[
f"FRAPPE_VERSION={example_env['FRAPPE_VERSION']}\n", # Defaults to latest version of Frappe
f"ERPNEXT_VERSION={example_env['ERPNEXT_VERSION']}\n", # defaults to latest version of ERPNext
f"DB_PASSWORD={db_pass}\n",
"DB_HOST=db\n",
"DB_PORT=3306\n",
"REDIS_CACHE=redis-cache:6379\n",
"REDIS_QUEUE=redis-queue:6379\n",
"REDIS_SOCKETIO=redis-socketio:6379\n",
f"LETSENCRYPT_EMAIL={email}\n",
f"FRAPPE_SITE_NAME_HEADER={site_name}\n",
f"SITE_ADMIN_PASS={admin_pass}",
]
)
def generate_pass(length: int = 12) -> str:
"""Generate random hash using best available randomness source."""
import math
import secrets
if not length:
length = 56
return secrets.token_hex(math.ceil(length / 2))[:length]
def check_repo_exists() -> bool:
return os.path.exists(os.path.join(os.getcwd(), "frappe_docker"))
def setup_prod(project: str, sitename: str, email: str) -> None:
if check_repo_exists():
compose_file_name = os.path.join(os.path.expanduser("~"), f"{project}-compose.yml")
docker_repo_path = os.path.join(os.getcwd(), "frappe_docker")
cprint(
"\nPlease refer to .example.env file in the frappe_docker folder to know which keys to set\n\n",
level=3,
)
admin_pass = ""
db_pass = ""
with open(compose_file_name, "w") as f:
# Writing to compose file
if not os.path.exists(os.path.join(docker_repo_path, ".env")):
admin_pass = generate_pass()
db_pass = generate_pass(9)
write_to_env(docker_repo_path, sitename, db_pass, admin_pass, email)
cprint(
"\nA .env file is generated with basic configs. Please edit it to fit to your needs \n",
level=3,
)
with open(os.path.join(os.path.expanduser("~"), "passwords.txt"), "w") as en:
en.writelines(f"ADMINISTRATOR_PASSWORD={admin_pass}\n")
en.writelines(f"MARIADB_ROOT_PASSWORD={db_pass}\n")
else:
env = get_from_env(docker_repo_path, ".env")
admin_pass = env["SITE_ADMIN_PASS"]
db_pass = env["DB_PASSWORD"]
try:
# TODO: Include flags for non-https and non-erpnext installation
subprocess.run(
[
which("docker"),
"compose",
"--project-name",
project,
"-f",
"compose.yaml",
"-f",
"overrides/compose.mariadb.yaml",
"-f",
"overrides/compose.redis.yaml",
# "-f", "overrides/compose.noproxy.yaml", TODO: Add support for local proxying without HTTPs
"-f",
"overrides/compose.erpnext.yaml",
"-f",
"overrides/compose.https.yaml",
"--env-file",
".env",
"config",
],
cwd=docker_repo_path,
stdout=f,
check=True,
)
except Exception:
logging.error("Docker Compose generation failed", exc_info=True)
cprint("\nGenerating Compose File failed\n")
sys.exit(1)
try:
# Starting with generated compose file
subprocess.run(
[
which("docker"),
"compose",
"-p",
project,
"-f",
compose_file_name,
"up",
"-d",
],
check=True,
)
logging.info(f"Docker Compose file generated at ~/{project}-compose.yml")
except Exception as e:
logging.error("Prod docker-compose failed", exc_info=True)
cprint(" Docker Compose failed, please check the container logs\n", e)
sys.exit(1)
cprint(f"\nCreating site: {sitename} \n", level=3)
try:
subprocess.run(
[
which("docker"),
"compose",
"-p",
project,
"exec",
"backend",
"bench",
"new-site",
sitename,
"--db-root-password",
db_pass,
"--admin-password",
admin_pass,
"--install-app",
"erpnext",
"--set-default",
],
check=True,
)
logging.info("New site creation completed")
except Exception as e:
logging.error("Bench site creation failed", exc_info=True)
cprint("Bench Site creation failed\n", e)
sys.exit(1)
else:
install_docker()
clone_frappe_docker_repo()
setup_prod(project, sitename, email) # Recursive
def setup_dev_instance(project: str):
if check_repo_exists():
try:
subprocess.run(
[
"docker",
"compose",
"-f",
"devcontainer-example/docker-compose.yml",
"--project-name",
project,
"up",
"-d",
],
cwd=os.path.join(os.getcwd(), "frappe_docker"),
check=True,
)
cprint(
"Please go through the Development Documentation: https://github.com/frappe/frappe_docker/tree/main/development to fully complete the setup.",
level=2,
)
logging.info("Development Setup completed")
except Exception as e:
logging.error("Dev Environment setup failed", exc_info=True)
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():
if which("docker") is not None:
return
cprint("Docker is not installed, Installing Docker...", level=3)
logging.info("Docker not found, installing Docker")
if platform.system() == "Darwin" or platform.system() == "Windows":
print(
f"""
This script doesn't install Docker on {"Mac" if platform.system()=="Darwin" else "Windows"}.
Please go through the Docker Installation docs for your system and run this script again"""
)
logging.debug("Docker setup failed due to platform is not Linux")
sys.exit(1)
try:
ps = subprocess.run(
["curl", "-fsSL", "https://get.docker.com"],
capture_output=True,
check=True,
)
subprocess.run(["/bin/bash"], input=ps.stdout, capture_output=True)
subprocess.run(
["sudo", "usermod", "-aG", "docker", str(os.getenv("USER"))], check=True
)
cprint("Waiting Docker to start", level=3)
time.sleep(10)
subprocess.run(["sudo", "systemctl", "restart", "docker.service"], check=True)
except Exception as e:
logging.error("Installing Docker failed", exc_info=True)
cprint("Failed to Install Docker\n", e)
cprint("\n Try Installing Docker Manually and re-run this script again\n")
sys.exit(1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Install Frappe with Docker")
parser.add_argument(
"-p", "--prod", help="Setup Production System", action="store_true"
)
parser.add_argument(
"-d", "--dev", help="Setup Development System", action="store_true"
)
parser.add_argument(
"-s",
"--sitename",
help="The Site Name for your production site",
default="site1.local",
)
parser.add_argument("-n", "--project", help="Project Name", default="frappe")
parser.add_argument(
"--email", help="Add email for the SSL.", required="--prod" in sys.argv
)
args = parser.parse_args()
if args.dev:
cprint("\nSetting Up Development Instance\n", level=2)
logging.info("Running Development Setup")
setup_dev_instance(args.project)
elif args.prod:
cprint("\nSetting Up Production Instance\n", level=2)
logging.info("Running Production Setup")
if "example.com" in args.email:
cprint("Emails with example.com not acceptable", level=1)
sys.exit(1)
setup_prod(args.project, args.sitename, args.email)
else:
parser.print_help()

View File

@ -10,7 +10,7 @@ authors = [
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Environment :: Console", "Environment :: Console",
"License :: OSI Approved :: GNU Affero General Public License v3", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Natural Language :: English", "Natural Language :: English",
"Operating System :: MacOS", "Operating System :: MacOS",
"Operating System :: OS Independent", "Operating System :: OS Independent",