Coverage for frappe_manager / commands / ssl / renew.py: 33%
72 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-07-02 18:13 +0530
« prev ^ index » next coverage.py v7.13.5, created at 2026-07-02 18:13 +0530
1"""Renew SSL certificates command."""
3from typing import Annotated
5import typer
6from typer_examples import example
8from frappe_manager import CLI_BENCHES_DIRECTORY
9from frappe_manager.logger.context import LoggerContext
10from frappe_manager.output_manager import spinner, temporary_stop
11from frappe_manager.site_manager.bench_service import BenchService
12from frappe_manager.site_manager.exceptions import BenchSSLCertificateNotIssued
13from frappe_manager.site_manager.site import Bench
14from frappe_manager.ssl_manager.certificate_exceptions import SSLCertificateNotDueForRenewalError
15from frappe_manager.utils.callbacks import prompt_for_bench_selection, sites_autocompletion_callback
17from .external_helpers import _renew_all_external_certificates, _renew_external_certificate
18from .helpers import get_output_handler
20ssl_renew_command = typer.Typer(no_args_is_help=True)
22# Ensure examples panel is installed for this sub-typer
23from typer_examples import install
25install(ssl_renew_command)
28@ssl_renew_command.command()
29@example(
30 "Renew all certificates for a bench",
31 "{benchname}",
32 detail="Renews all TLS certificates associated with the specified bench.",
33 benchname="mybench",
34)
35@example(
36 "Renew certificate for specific domain",
37 "{benchname} example.com",
38 detail="Renews a single domain certificate for the given bench.",
39 benchname="mybench",
40)
41@example(
42 "Renew all certificates for all benches",
43 "--all",
44 detail="Renews certificates across all benches managed by FM when run by an administrator.",
45)
46@example(
47 "Test renewal with Let's Encrypt staging (dry-run)",
48 "{benchname} --dry-run",
49 detail="Simulates the renewal using Let's Encrypt staging environment to validate configuration.",
50 benchname="mybench",
51)
52@example(
53 "Renew specific external (standalone) domain",
54 "--standalone example.com",
55 detail="Renews a standalone external domain certificate managed outside benches.",
56)
57@example(
58 "Renew all external (standalone) domains",
59 "--standalone --all",
60 detail="Renews all external standalone certificates managed by FM.",
61)
62def renew(
63 ctx: typer.Context,
64 benchname: Annotated[
65 str | None,
66 typer.Argument(
67 help="Name of the bench (omit for standalone mode).",
68 autocompletion=sites_autocompletion_callback,
69 ),
70 ] = None,
71 domain: Annotated[
72 str | None,
73 typer.Argument(help="Specific domain to renew. If omitted, renews all certificates for the bench/standalone."),
74 ] = None,
75 all: Annotated[bool, typer.Option(help="Renew ssl cert for all benches.")] = False,
76 standalone: Annotated[bool, typer.Option("--standalone", help="Renew certificates for external domains")] = False,
77 dry_run: Annotated[
78 bool,
79 typer.Option("--dry-run", help="Test renewal using Let's Encrypt staging server without modifying the system."),
80 ] = False,
81 force: Annotated[
82 bool,
83 typer.Option("--force", "-f", help="Force renewal even if certificate is not due for renewal."),
84 ] = False,
85):
86 """
87 Renew SSL certificates.
89 Supports bench and standalone modes. Use --dry-run to validate against Let's Encrypt staging; --force forces renewal.
90 """
92 if standalone:
93 if all:
94 _renew_all_external_certificates(ctx, dry_run, force)
95 else:
96 actual_domain = domain if domain else benchname
97 if not actual_domain:
98 context = LoggerContext(operation="ssl-renew-external")
99 output = get_output_handler(ctx, context=context)
100 output.display_error("Domain required for standalone renewal")
101 with temporary_stop(output):
102 typer.echo(ctx.get_help())
103 raise typer.Exit(1)
104 _renew_external_certificate(ctx, actual_domain, dry_run, force)
105 else:
106 # Existing bench renewal logic
107 services_manager = ctx.obj["services"]
108 bench_service = BenchService(CLI_BENCHES_DIRECTORY, services_manager)
110 if all:
111 sites_list = bench_service.get_bench_names()
112 else:
113 benchname = prompt_for_bench_selection(benchname)
115 if not benchname:
116 context = LoggerContext(operation="ssl-renew")
117 output = get_output_handler(ctx, context=context)
118 output.display_error("Benchname required in bench mode")
119 with temporary_stop(output):
120 typer.echo(ctx.get_help())
121 raise typer.Exit(1)
122 sites_list = [benchname]
124 for benchname in sites_list:
125 context = LoggerContext(bench=benchname, operation="ssl-renew")
126 output = get_output_handler(ctx, context=context)
127 logger = ctx.obj.get("logger")
128 bench = Bench.get_object(benchname, services_manager, logger=logger, output_handler=output)
130 output.change_head(f"Renew certificate for {benchname}")
131 try:
132 if domain:
133 cert_domains = [cert.domain for cert in bench.certificate_manager.certificates]
134 if domain not in cert_domains:
135 output.display_error(
136 f"No SSL certificate found for domain '{domain}'.\n"
137 f"Configured certificates: {', '.join(cert_domains) if cert_domains else 'None'}\n"
138 f"To add a certificate, use: fm ssl add {benchname} {domain}",
139 )
140 raise typer.Exit(1)
142 # Renew specific domain certificate
143 with spinner(output, f"Renewing certificate for {domain}"):
144 bench.ssl.renew_certificate(domain, dry_run=dry_run, force=force)
145 if not dry_run:
146 output.print(f"Certificate renewed for {domain}", emoji_code=":white_check_mark:")
147 else:
148 # Renew all certificates for the bench
149 with spinner(output, f"Renewing certificates for {benchname}"):
150 bench.ssl.renew_all_certificates(dry_run=dry_run, force=force)
151 except (BenchSSLCertificateNotIssued, SSLCertificateNotDueForRenewalError) as e:
152 output.warning(e.message)
154 except Exception as e:
155 output.display_error(str(e))
156 raise typer.Exit(1)