Coverage for frappe_manager / commands / ssl / add.py: 38%

42 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-07-02 18:13 +0530

1"""Add SSL certificate command.""" 

2 

3from typing import Annotated 

4 

5import typer 

6from typer_examples import example 

7 

8from frappe_manager.logger.context import LoggerContext 

9from frappe_manager.output_manager import temporary_stop 

10from frappe_manager.ssl_manager import LETSENCRYPT_PREFERRED_CHALLENGE 

11from frappe_manager.utils.callbacks import prompt_for_bench_selection, sites_autocompletion_callback 

12 

13from .bench_helpers import _add_bench_certificate 

14from .external_helpers import _add_external_certificate 

15from .helpers import get_output_handler 

16 

17 

18@example( 

19 "Add SSL certificate with HTTP-01 challenge", 

20 "{benchname} example.com --challenge http01", 

21 detail="Requests a certificate using an HTTP-01 challenge and installs it into the bench's nginx configuration.", 

22 benchname="mybench", 

23) 

24@example( 

25 "Add SSL certificate with DNS-01 challenge (Cloudflare)", 

26 "{benchname} example.com --challenge dns01", 

27 detail="Requests a certificate using DNS-01 validation; configure DNS provider credentials first when required.", 

28 benchname="mybench", 

29) 

30@example( 

31 "Add for external Docker project (standalone mode)", 

32 "example.com --standalone", 

33 detail="Manage SSL for an external Docker project by using standalone mode and FM's nginx-proxy.", 

34) 

35@example( 

36 "Test with Let's Encrypt staging (dry-run)", 

37 "{benchname} example.com --dry-run", 

38 detail="Performs a dry-run against Let's Encrypt staging environment to validate configuration without issuing production certs.", 

39 benchname="mybench", 

40) 

41@example( 

42 "Add with CNAME delegation for DNS validation", 

43 "{benchname} example.com --challenge dns01 --cname delegated.example.com", 

44 detail="Uses a CNAME delegation target for DNS-01 validation when the DNS zone is delegated to another provider.", 

45 benchname="mybench", 

46) 

47def add_certificate( 

48 ctx: typer.Context, 

49 benchname: Annotated[ 

50 str | None, 

51 typer.Argument( 

52 help="Name of the bench (omit for standalone mode).", 

53 autocompletion=sites_autocompletion_callback, 

54 ), 

55 ] = None, 

56 domain: Annotated[str | None, typer.Argument(help="Domain name for the certificate")] = None, 

57 challenge: Annotated[ 

58 LETSENCRYPT_PREFERRED_CHALLENGE, 

59 typer.Option("--challenge", "-c", help="Challenge type"), 

60 ] = LETSENCRYPT_PREFERRED_CHALLENGE.http01, 

61 cname: Annotated[ 

62 str | None, 

63 typer.Option("--cname", help="CNAME delegation record for DNS-01 challenge (requires dns01)"), 

64 ] = None, 

65 dry_run: Annotated[ 

66 bool, 

67 typer.Option( 

68 "--dry-run", 

69 help="Test certificate generation using Let's Encrypt staging server without adding it to the system.", 

70 ), 

71 ] = False, 

72 standalone: Annotated[ 

73 bool, 

74 typer.Option( 

75 "--standalone", 

76 help="Manage SSL for external (non-bench) Docker project. Use with docker network 'fm-global-frontend-network'.", 

77 ), 

78 ] = False, 

79 skip_dns_check: Annotated[ 

80 bool, 

81 typer.Option( 

82 "--skip-dns-check", 

83 help="Skip DNS validation before certificate generation (use if DNS will be configured later).", 

84 ), 

85 ] = False, 

86 wait_for_dns: Annotated[ 

87 bool, 

88 typer.Option( 

89 "--wait-for-dns", 

90 help="Wait for DNS propagation (polls every 30s for up to 5 minutes).", 

91 ), 

92 ] = False, 

93): 

94 """ 

95 Add an SSL certificate for a domain. 

96 

97 Supports bench mode (adds certificate to a bench) and standalone mode for external Docker projects using FM nginx-proxy. 

98 Use --dry-run to validate issuance against Let's Encrypt staging first. 

99 """ 

100 

101 if standalone: 

102 # Standalone mode: domain can be first arg (as benchname) or second arg 

103 actual_domain = domain if domain else benchname 

104 

105 if not actual_domain: 

106 context = LoggerContext(operation="ssl-add-external") 

107 output = get_output_handler(ctx, context=context) 

108 output.display_error("Domain is required in standalone mode") 

109 with temporary_stop(output): 

110 typer.echo(ctx.get_help()) 

111 raise typer.Exit(1) 

112 

113 if benchname and domain: 

114 context = LoggerContext(operation="ssl-add-external") 

115 output = get_output_handler(ctx, context=context) 

116 output.display_error("Cannot specify both benchname and domain in standalone mode") 

117 with temporary_stop(output): 

118 typer.echo(ctx.get_help()) 

119 raise typer.Exit(1) 

120 

121 _add_external_certificate(ctx, actual_domain, challenge, cname, dry_run, skip_dns_check, wait_for_dns) 

122 else: 

123 benchname = prompt_for_bench_selection(benchname) 

124 

125 if not benchname or not domain: 

126 context = LoggerContext(operation="ssl-add") 

127 output = get_output_handler(ctx, context=context) 

128 output.display_error("Both benchname and domain are required in bench mode") 

129 with temporary_stop(output): 

130 typer.echo(ctx.get_help()) 

131 raise typer.Exit(1) 

132 

133 _add_bench_certificate(ctx, benchname, domain, challenge, cname, dry_run)