-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #180 from rtCamp/feat/dns-challenge-certbot
Add Cloudflare Let's Encrypt DNS challenge support
- Loading branch information
Showing
12 changed files
with
280 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,7 +25,7 @@ | |
from frappe_manager.migration_manager.migration_executor import MigrationExecutor | ||
from frappe_manager.site_manager.site import Bench | ||
from frappe_manager.site_manager.workers_manager.SiteWorker import BenchWorkers | ||
from frappe_manager.ssl_manager import SUPPORTED_SSL_TYPES | ||
from frappe_manager.ssl_manager import LETSENCRYPT_PREFERRED_CHALLENGE, SUPPORTED_SSL_TYPES | ||
from frappe_manager.ssl_manager.certificate import SSLCertificate | ||
from frappe_manager.ssl_manager.letsencrypt_certificate import LetsencryptSSLCertificate | ||
from frappe_manager.utils.callbacks import ( | ||
|
@@ -156,6 +156,14 @@ def create( | |
environment: Annotated[ | ||
FMBenchEnvType, typer.Option("--environment", "--env", help="Select bench environment type.") | ||
] = FMBenchEnvType.dev, | ||
letsencrypt_preferred_challenge: Annotated[ | ||
Optional[LETSENCRYPT_PREFERRED_CHALLENGE], | ||
typer.Option(help="Select preferred letsencrypt challenge.", show_default=False), | ||
] = None, | ||
letsencrypt_email: Annotated[ | ||
Optional[str], | ||
typer.Option(help="Specify email for letsencrypt", show_default=False), | ||
] = None, | ||
developer_mode: Annotated[ | ||
EnableDisableOptionsEnum, typer.Option(help="Toggle frappe developer mode.") | ||
] = EnableDisableOptionsEnum.disable, | ||
|
@@ -205,18 +213,36 @@ def create( | |
bench_config_path = bench_path / CLI_BENCH_CONFIG_FILE_NAME | ||
|
||
if ssl == SUPPORTED_SSL_TYPES.le: | ||
if fm_config_manager.le_email == '[email protected]': | ||
email = richprint.prompt_ask(prompt='Please enter [bold][green]email[/bold][/green] for Let\'s Encrypt') | ||
if not letsencrypt_preferred_challenge: | ||
if fm_config_manager.letsencrypt.exists: | ||
if letsencrypt_preferred_challenge is None: | ||
letsencrypt_preferred_challenge = LETSENCRYPT_PREFERRED_CHALLENGE.dns01 | ||
|
||
if not letsencrypt_preferred_challenge: | ||
letsencrypt_preferred_challenge = LETSENCRYPT_PREFERRED_CHALLENGE.http01 | ||
|
||
if fm_config_manager.letsencrypt.email == '[email protected]' or fm_config_manager.letsencrypt.email is None: | ||
if not letsencrypt_email: | ||
richprint.stop() | ||
raise typer.BadParameter("No email provided, required by certbot.", param_hint='--letsencrypt-email') | ||
else: | ||
email = letsencrypt_email | ||
|
||
validate_email(email, check_deliverability=False) | ||
fm_config_manager.le_email = email | ||
fm_config_manager.export_to_toml() | ||
richprint.print("Let's Encrypt email saved to configuration. It will be used automatically from now on.") | ||
else: | ||
richprint.print("Using Let's Encrypt email from configuration.") | ||
email = fm_config_manager.le_email | ||
fm_config_manager.export_to_toml() | ||
|
||
ssl_certificate = LetsencryptSSLCertificate(domain=benchname, ssl_type=ssl, email=email) | ||
richprint.print( | ||
"Defaulting to Let's Encrypt email from [blue]fm_config.toml[/blue] since [blue]'--letsencrypt-email'[/blue] is not given." | ||
) | ||
email = fm_config_manager.letsencrypt.email | ||
|
||
ssl_certificate = LetsencryptSSLCertificate( | ||
domain=benchname, | ||
ssl_type=ssl, | ||
email=email, | ||
preferred_challenge=letsencrypt_preferred_challenge, | ||
api_key=fm_config_manager.letsencrypt.api_key, | ||
api_token=fm_config_manager.letsencrypt.api_token, | ||
) | ||
|
||
elif ssl == SUPPORTED_SSL_TYPES.none: | ||
ssl_certificate = SSLCertificate(domain=benchname, ssl_type=ssl) | ||
|
@@ -483,9 +509,17 @@ def update( | |
Optional[EnableDisableOptionsEnum], | ||
typer.Option("--admin-tools", help="Toggle admin-tools.", show_default=False), | ||
] = None, | ||
letsencrypt_preferred_challenge: Annotated[ | ||
Optional[LETSENCRYPT_PREFERRED_CHALLENGE], | ||
typer.Option(help="Select preferred letsencrypt challenge.", show_default=False), | ||
] = None, | ||
letsencrypt_email: Annotated[ | ||
Optional[str], | ||
typer.Option(help="Specify email for letsencrypt", show_default=False), | ||
] = None, | ||
environment: Annotated[ | ||
Optional[FMBenchEnvType], | ||
typer.Option("--environment", help="Switch bench environment.", show_default=False), | ||
typer.Option("--environment", "--env", help="Switch bench environment.", show_default=False), | ||
] = None, | ||
developer_mode: Annotated[ | ||
Optional[EnableDisableOptionsEnum], | ||
|
@@ -530,20 +564,38 @@ def update( | |
new_ssl_certificate = SSLCertificate(domain=benchname, ssl_type=SUPPORTED_SSL_TYPES.none) | ||
|
||
if ssl == SUPPORTED_SSL_TYPES.le: | ||
if fm_config_manager.le_email == '[email protected]': | ||
email = richprint.prompt_ask(prompt='Please enter [bold][green]email[/bold][/green] for Let\'s Encrypt') | ||
if not letsencrypt_preferred_challenge: | ||
if fm_config_manager.letsencrypt.exists: | ||
if letsencrypt_preferred_challenge is None: | ||
letsencrypt_preferred_challenge = LETSENCRYPT_PREFERRED_CHALLENGE.dns01 | ||
|
||
if not letsencrypt_preferred_challenge: | ||
letsencrypt_preferred_challenge = LETSENCRYPT_PREFERRED_CHALLENGE.http01 | ||
|
||
if fm_config_manager.letsencrypt.email == '[email protected]' or fm_config_manager.letsencrypt.email is None: | ||
if not letsencrypt_email: | ||
richprint.stop() | ||
raise typer.BadParameter( | ||
"No email provided, required by certbot.", param_hint='--letsencrypt-email' | ||
) | ||
else: | ||
email = letsencrypt_email | ||
|
||
validate_email(email, check_deliverability=False) | ||
fm_config_manager.le_email = email | ||
fm_config_manager.export_to_toml() | ||
else: | ||
richprint.print( | ||
"Let's Encrypt email saved to configuration. It will be used automatically from now on." | ||
"Defaulting to Let's Encrypt email from [blue]fm_config.toml[/blue] since [blue]'--letsencrypt-email'[/blue] is not given." | ||
) | ||
else: | ||
richprint.print("Using Let's Encrypt email from configuration.") | ||
email = fm_config_manager.le_email | ||
fm_config_manager.export_to_toml() | ||
|
||
new_ssl_certificate = LetsencryptSSLCertificate(domain=benchname, ssl_type=ssl, email=email) | ||
email = fm_config_manager.letsencrypt.email | ||
|
||
new_ssl_certificate = LetsencryptSSLCertificate( | ||
domain=benchname, | ||
ssl_type=ssl, | ||
email=email, | ||
preferred_challenge=letsencrypt_preferred_challenge, | ||
api_key=fm_config_manager.letsencrypt.api_key, | ||
api_token=fm_config_manager.letsencrypt.api_token, | ||
) | ||
|
||
richprint.print("Updating Certificate.") | ||
bench.update_certificate(new_ssl_certificate) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,54 @@ | ||
from typing import Optional | ||
from pathlib import Path | ||
from pydantic import BaseModel, EmailStr | ||
from pydantic import BaseModel, EmailStr, Field | ||
import tomlkit | ||
from frappe_manager.migration_manager.version import Version | ||
from frappe_manager import CLI_FM_CONFIG_PATH | ||
from frappe_manager.utils.helpers import get_current_fm_version | ||
|
||
|
||
class FMLetsencryptConfig(BaseModel): | ||
email: Optional[EmailStr] = Field(None, description="Email used by certbot.") | ||
api_token: Optional[str] = Field(None, description="Cloudflare API token used by Certbot.") | ||
api_key: Optional[str] = Field(None, description="Cloudflare Global API Key used by Certbot.") | ||
|
||
@property | ||
def exists(self): | ||
if self.api_token or self.api_key: | ||
return True | ||
|
||
return False | ||
|
||
def get_toml_doc(self): | ||
model_dict = self.model_dump(exclude_none=True) | ||
toml_doc = tomlkit.document() | ||
|
||
for key, value in model_dict.items(): | ||
if isinstance(value, Path): | ||
toml_doc[key] = str(value.absolute()) | ||
else: | ||
toml_doc[key] = value | ||
return toml_doc | ||
|
||
@classmethod | ||
def import_from_toml_doc(cls, toml_doc): | ||
config_object = cls(**toml_doc) | ||
return config_object | ||
|
||
|
||
class FMConfigManager(BaseModel): | ||
root_path: Path | ||
version: Version | ||
le_email: EmailStr | ||
letsencrypt: FMLetsencryptConfig = Field(default=FMLetsencryptConfig()) | ||
|
||
def export_to_toml(self, path: Path = CLI_FM_CONFIG_PATH) -> bool: | ||
exclude = {'root_path'} | ||
|
||
if self.le_email == '[email protected]': | ||
exclude.add('le_email') | ||
if not self.letsencrypt.email and not self.letsencrypt.api_key and not self.letsencrypt.api_token: | ||
exclude.add('letsencrypt') | ||
else: | ||
if self.letsencrypt.email == '[email protected]': | ||
exclude.add('letsencrypt') | ||
|
||
if self.version < Version('0.13.0'): | ||
path = CLI_FM_CONFIG_PATH.parent / '.fm.toml' | ||
|
@@ -46,7 +79,7 @@ def import_from_toml(cls, path: Path = CLI_FM_CONFIG_PATH) -> "FMConfigManager": | |
old_config_path = path.parent / '.fm.toml' | ||
|
||
input_data['version'] = Version('0.8.3') | ||
input_data['le_email'] = '[email protected]' | ||
input_data['letsencrypt'] = FMLetsencryptConfig(email=None, api_key=None, api_token=None) | ||
input_data['root_path'] = str(path) | ||
|
||
if old_config_path.exists(): | ||
|
@@ -55,7 +88,9 @@ def import_from_toml(cls, path: Path = CLI_FM_CONFIG_PATH) -> "FMConfigManager": | |
elif path.exists(): | ||
data = tomlkit.parse(path.read_text()) | ||
input_data['version'] = Version(data.get('version', get_current_fm_version())) | ||
input_data['le_email'] = data.get('le_email', '[email protected]') | ||
input_data['letsencrypt'] = FMLetsencryptConfig( | ||
**data.get('letsencrypt', {'email': None, 'api_key': None, 'api_token': None}) | ||
) | ||
|
||
fm_config_instance = cls(**input_data) | ||
return fm_config_instance |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
frappe_manager/migration_manager/migrations/migrate_0_14_0.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from pathlib import Path | ||
import tomlkit | ||
from frappe_manager.migration_manager.migration_base import MigrationBase | ||
from frappe_manager.migration_manager.migration_helpers import MigrationBenches, MigrationServicesManager | ||
from frappe_manager.migration_manager.version import Version | ||
from frappe_manager.migration_manager.backup_manager import BackupManager | ||
from frappe_manager.display_manager.DisplayManager import richprint | ||
|
||
|
||
class MigrationV0140(MigrationBase): | ||
version = Version("0.14.0") | ||
|
||
def init(self): | ||
self.cli_dir: Path = Path.home() / 'frappe' | ||
self.benches_dir = self.cli_dir / "sites" | ||
self.backup_manager = BackupManager(str(self.version), self.benches_dir) | ||
self.benches_manager = MigrationBenches(self.benches_dir) | ||
self.services_manager: MigrationServicesManager = MigrationServicesManager( | ||
services_path=self.cli_dir / 'services' | ||
) | ||
|
||
def migrate_services(self): | ||
richprint.change_head("Migrating [blue]fm_config.toml[/blue] changes") | ||
config_path = self.cli_dir / 'fm_config.toml' | ||
|
||
if config_path.exists(): | ||
data = tomlkit.parse(config_path.read_text()) | ||
email = data.get('le_email', None) | ||
|
||
if email: | ||
data['letsencrypt'] = {'email': email} | ||
del data['le_email'] | ||
|
||
with open(config_path, 'w') as f: | ||
f.write(tomlkit.dumps(data)) | ||
richprint.print("Migrated [blue]fm_config.toml[/blue]") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.