Coverage for frappe_manager / commands / ssl / acme_sh.py: 24%
49 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"""acme.sh passthrough command."""
3import os
5import typer
6from typer_examples import example
8from frappe_manager.utils.subprocess import stream_command_output
10from .helpers import get_output_handler
13@example(
14 "Show acme.sh help and available commands",
15 "",
16 detail="Displays acme.sh help. Use for learning available subcommands and flags.",
17)
18@example(
19 "List all certificates managed by acme.sh",
20 "--list",
21 detail="Lists certificates that acme.sh currently manages in its home directory.",
22)
23@example(
24 "Show certificate information for a domain",
25 "--info -d example.com",
26 detail="Shows detailed information for a managed certificate for the domain.",
27)
28@example(
29 "Check acme.sh version",
30 "--version",
31 detail="Prints the installed acme.sh version used by FM.",
32)
33@example(
34 "Upgrade acme.sh to latest version",
35 "--upgrade",
36 detail="Upgrades the bundled acme.sh installation to the latest release.",
37)
38@example(
39 "Force renew certificate for a domain",
40 "--renew -d example.com --force",
41 detail="Forces a renewal for a certificate using acme.sh; advanced option for recovery and testing.",
42)
43def acmesh_passthrough(
44 ctx: typer.Context,
45):
46 """
47 Run acme.sh commands directly with FM's environment (advanced users).
49 Advanced users only: this bypasses FM's certificate management. Prefer 'fm ssl add/remove/renew' for normal workflows.
50 """
51 args = ctx.args
53 services_manager = ctx.obj["services"]
54 output = get_output_handler(ctx)
56 global_proxy_storage = services_manager.proxy_storage
57 ssl_dir = global_proxy_storage.dirs.ssl.host
58 acmesh_home = ssl_dir / "acmesh" / ".acme.sh"
59 acmesh_bin = acmesh_home / "acme.sh"
61 if not acmesh_bin.exists():
62 output.display_error("acme.sh is not installed yet")
63 output.info("Run 'fm ssl add <benchname> <domain>' to install acme.sh first")
64 raise typer.Exit(1)
66 cmd = [str(acmesh_bin), "--home", str(acmesh_home)]
68 # If no args provided, show help
69 if not args:
70 cmd.append("--help")
71 else:
72 cmd.extend(args)
74 env = os.environ.copy()
75 env["LE_WORKING_DIR"] = str(acmesh_home)
77 output.change_head("Running acme.sh")
78 output.info(f"Command: acme.sh {' '.join(args or ['--help'])}")
79 output.info(f"Home: {acmesh_home}")
80 output.print("")
82 # Stream output directly to user
83 exit_code_holder = [0]
85 def stream_with_exit_tracking():
86 """Generator that tracks exit code while yielding output."""
87 for source, line in stream_command_output(cmd, env=env, cwd=None):
88 if source == "exit_code":
89 exit_code_holder[0] = int(line.decode())
90 yield source, line
92 # Display all output (print directly for raw acme.sh output)
93 for source, line in stream_with_exit_tracking():
94 if source in ("stdout", "stderr"):
95 # Print directly without prefix for raw acme.sh output
96 decoded = line.decode()
97 print(decoded, flush=True)
99 # Exit with acme.sh's exit code
100 if exit_code_holder[0] != 0:
101 output.print("")
102 output.display_error(f"acme.sh exited with code {exit_code_holder[0]}")
103 raise typer.Exit(exit_code_holder[0])
104 output.print("")
105 output.print("Command completed successfully", emoji_code=":white_check_mark:")