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

1"""Renew SSL certificates command.""" 

2 

3from typing import Annotated 

4 

5import typer 

6from typer_examples import example 

7 

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 

16 

17from .external_helpers import _renew_all_external_certificates, _renew_external_certificate 

18from .helpers import get_output_handler 

19 

20ssl_renew_command = typer.Typer(no_args_is_help=True) 

21 

22# Ensure examples panel is installed for this sub-typer 

23from typer_examples import install 

24 

25install(ssl_renew_command) 

26 

27 

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. 

88 

89 Supports bench and standalone modes. Use --dry-run to validate against Let's Encrypt staging; --force forces renewal. 

90 """ 

91 

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) 

109 

110 if all: 

111 sites_list = bench_service.get_bench_names() 

112 else: 

113 benchname = prompt_for_bench_selection(benchname) 

114 

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] 

123 

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) 

129 

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) 

141 

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) 

153 

154 except Exception as e: 

155 output.display_error(str(e)) 

156 raise typer.Exit(1)