66import re
77import subprocess
88import sys
9- import pkg_resources
9+ from importlib import metadata
1010from typing import List , Optional , Tuple
1111
1212# Add script directory to path to import dns_providers
@@ -48,143 +48,26 @@ def _detect_provider_type(self) -> str:
4848 """Detect provider type (reuse factory logic)."""
4949 return DNSProviderFactory ._detect_provider_type ()
5050
51+ @staticmethod
52+ def _cert_name (domain : str ) -> str :
53+ """Match certbot's stable lineage name for a domain."""
54+ return domain .lstrip ("*." ).replace ("*" , "wildcard-" )
55+
5156 def install_plugin (self ) -> bool :
5257 """Install certbot plugin for the current provider."""
5358 if not self .provider .CERTBOT_PACKAGE :
5459 print (f"No certbot package defined for { self .provider_type } " )
5560 return False
5661
57- # First ensure certbot is installed in the current environment
58- self ._ensure_certbot_in_env ()
59-
60- # Check if plugin is already installed
6162 try :
63+ self ._ensure_certbot_in_env ()
6264 __import__ (self .provider .CERTBOT_PLUGIN_MODULE )
63- print (
64- f"Plugin { self .provider .CERTBOT_PACKAGE } is already installed" )
65+ print (f"Plugin { self .provider .CERTBOT_PACKAGE } is available" )
6566 return True
66- except ImportError :
67- pass # Plugin not installed, continue with installation
68-
69- print (f"Installing certbot plugin: { self .provider .CERTBOT_PACKAGE } " )
70-
71- # Try multiple installation methods
72- install_methods = []
73-
74- # Method 1: Use the same python executable that's running this script
75- install_methods .append (
76- [sys .executable , "-m" , "pip" , "install" , self .provider .CERTBOT_PACKAGE ])
77-
78- # Method 2: Use virtual environment pip if available
79- if "VIRTUAL_ENV" in os .environ :
80- venv_pip = os .path .join (os .environ ["VIRTUAL_ENV" ], "bin" , "pip" )
81- if os .path .exists (venv_pip ):
82- install_methods .append (
83- [venv_pip , "install" , self .provider .CERTBOT_PACKAGE ])
84-
85- # Method 3: Use system pip
86- install_methods .append (
87- ["pip" , "install" , self .provider .CERTBOT_PACKAGE ])
88-
89- # Method 4: Use pip3
90- install_methods .append (
91- ["pip3" , "install" , self .provider .CERTBOT_PACKAGE ])
92-
93- success = False
94- for i , pip_cmd in enumerate (install_methods ):
95- print (f"Trying installation method { i + 1 } " )
96- print (f"Command: { ' ' .join (pip_cmd )} " )
97- try :
98- result = subprocess .run (
99- pip_cmd , capture_output = True , text = True )
100- if result .returncode == 0 :
101- print (f"Installation method { i + 1 } succeeded" )
102- success = True
103- break
104- else :
105- print (f"Installation method { i + 1 } failed: { result .stderr } " )
106- except Exception as e :
107- print (f"Installation method { i + 1 } exception: { e } " )
108-
109- if not success :
110- print (f"All installation methods failed" , file = sys .stderr )
67+ except (ImportError , RuntimeError ) as exc :
68+ print (f"Required certbot dependency is missing from the measured image: { exc } " , file = sys .stderr )
11169 return False
11270
113- print (f"Successfully installed { self .provider .CERTBOT_PACKAGE } " )
114-
115- # Diagnostic information for troubleshooting
116- try :
117- print (f"Installed to Python: { sys .executable } " )
118-
119- # Show certbot command
120- certbot_cmd = self ._get_certbot_command ()
121- print (f"Using certbot: { ' ' .join (certbot_cmd )} " )
122-
123- try :
124- dist = pkg_resources .get_distribution (
125- self .provider .CERTBOT_PACKAGE )
126- print (f"Package version: { dist .version } at { dist .location } " )
127- except pkg_resources .DistributionNotFound :
128- print ("Warning: Package not found in current environment" )
129- except Exception as diag_error :
130- print (f"Diagnostic error: { diag_error } " )
131-
132- # Verify plugin installation
133- try :
134- __import__ (self .provider .CERTBOT_PLUGIN_MODULE )
135- print (
136- f"Plugin { self .provider .CERTBOT_PLUGIN } successfully imported" )
137-
138- # Test if plugin is recognized by certbot
139- certbot_cmd = self ._get_certbot_command ()
140- test_cmd = certbot_cmd + ["plugins" ]
141- test_result = subprocess .run (
142- test_cmd , capture_output = True , text = True , timeout = 10 )
143-
144- if test_result .returncode == 0 and self .provider .CERTBOT_PLUGIN in test_result .stdout :
145- print (
146- f"✓ Plugin { self .provider .CERTBOT_PLUGIN } is available in certbot" )
147- return True
148- else :
149- print (
150- f"Warning: { self .provider .CERTBOT_PLUGIN } plugin not found in certbot plugins list" )
151- if test_result .stderr :
152- print (f"Plugin test stderr: { test_result .stderr } " )
153-
154- # Debug plugin registration
155- self ._debug_plugin_registration ()
156-
157- # Try force reinstall to fix plugin registration
158- print ("Attempting to fix plugin registration..." )
159- try :
160- force_cmd = [sys .executable , "-m" , "pip" , "install" , "--force-reinstall" ,
161- "--no-deps" , self .provider .CERTBOT_PACKAGE ]
162- print (f"Running: { ' ' .join (force_cmd )} " )
163- force_result = subprocess .run (
164- force_cmd , capture_output = True , text = True )
165-
166- if force_result .returncode == 0 :
167- # Test again after reinstall
168- retest_cmd = certbot_cmd + ["plugins" ]
169- retest_result = subprocess .run (
170- retest_cmd , capture_output = True , text = True , timeout = 10 )
171- if retest_result .returncode == 0 and self .provider .CERTBOT_PLUGIN in retest_result .stdout :
172- print (f"✓ Plugin registration fixed after reinstall" )
173- return True
174- else :
175- print (f"Plugin still not registered, may work anyway" )
176- else :
177- print (f"Force reinstall failed: { force_result .stderr } " )
178- except Exception as fix_error :
179- print (f"Plugin fix attempt failed: { fix_error } " )
180-
181- # Continue anyway - may work in Docker environments
182- return True
183-
184- except Exception as e :
185- print (f"Plugin verification warning: { e } " )
186- return True
187-
18871 def _ensure_certbot_in_env (self ) -> None :
18972 """Ensure certbot is installed in the current Python environment."""
19073
@@ -193,24 +76,8 @@ def _ensure_certbot_in_env(self) -> None:
19376 import certbot
19477 print (f"✓ Certbot module available in current environment" )
19578 return
196- except ImportError :
197- print (f"Certbot module not found, installing..." )
198-
199- # Install certbot if not available
200- try :
201- install_cmd = [sys .executable , "-m" , "pip" , "install" , "certbot" ]
202- print (f"Running: { ' ' .join (install_cmd )} " )
203- result = subprocess .run (
204- install_cmd , capture_output = True , text = True )
205-
206- if result .returncode == 0 :
207- print (f"✓ Certbot installed successfully in current environment" )
208- else :
209- print (f"Failed to install certbot: { result .stderr } " )
210- # Continue anyway - may still work
211- except Exception as e :
212- print (f"Error installing certbot: { e } " )
213- # Continue anyway - may still work
79+ except ImportError as exc :
80+ raise RuntimeError ("certbot is missing from the measured image" ) from exc
21481
21582 def _get_certbot_command (self ) -> List [str ]:
21683 """Get the correct certbot command that uses the same Python environment."""
@@ -234,7 +101,6 @@ def _get_certbot_command(self) -> List[str]:
234101 def _debug_plugin_registration (self ) -> None :
235102 """Debug why plugin is not being registered by certbot."""
236103 try :
237- import pkg_resources
238104 print ("=== Plugin Registration Debug ===" )
239105
240106 # Show which certbot we're using
@@ -243,11 +109,10 @@ def _debug_plugin_registration(self) -> None:
243109
244110 # Check entry points
245111 try :
246- entry_points = list (
247- pkg_resources .iter_entry_points ('certbot.plugins' ))
112+ entry_points = list (metadata .entry_points (group = 'certbot.plugins' ))
248113 print (f"Found { len (entry_points )} certbot plugins:" )
249114 for ep in entry_points :
250- print (f" - { ep .name } : { ep .module_name } " )
115+ print (f" - { ep .name } : { ep .value } " )
251116
252117 # Look specifically for our plugin
253118 plugin_eps = [ep for ep in entry_points if ep .name ==
@@ -289,6 +154,11 @@ def setup_credentials(self) -> bool:
289154 def _build_certbot_command (self , action : str , domain : str , email : str ) -> List [str ]:
290155 """Build certbot command using provider configuration."""
291156 certbot_cmd = self ._get_certbot_command ()
157+ deterministic = os .environ .get ("DETERMINISTIC_TLS_KEY" , "false" ).lower () == "true"
158+ if deterministic and action == "certonly" :
159+ key_path = f"/etc/letsencrypt/live/{ self ._cert_name (domain )} /privkey.pem"
160+ csr_path = f"/etc/letsencrypt/csr/{ self ._cert_name (domain )} .csr"
161+ subprocess .run (["python3" , "/scripts/deterministic_key.py" , domain , key_path , csr_path ], check = True )
292162
293163 # Challenge-delegation mode: when DELEGATION_ZONE is set, answer the
294164 # DNS-01 challenge in a delegated zone via a manual hook instead of the
@@ -343,6 +213,8 @@ def _build_certbot_command(self, action: str, domain: str, email: str) -> List[s
343213 f"Credentials file does not exist: { credentials_file } " )
344214
345215 if action == "certonly" :
216+ if deterministic :
217+ base_cmd .extend (["--csr" , csr_path , "--cert-path" , f"/etc/letsencrypt/live/{ self ._cert_name (domain )} /cert.pem" , "--fullchain-path" , f"/etc/letsencrypt/live/{ self ._cert_name (domain )} /fullchain.pem" , "--chain-path" , f"/etc/letsencrypt/live/{ self ._cert_name (domain )} /chain.pem" ])
346218 base_cmd .extend (["--agree-tos" , "--no-eff-email" ])
347219 # The ACME contact address is optional (RFC 8555 section 7.3), and
348220 # it is published: the account document is served as attestation
@@ -363,6 +235,11 @@ def _build_certbot_command(self, action: str, domain: str, email: str) -> List[s
363235 base_cmd .extend (["--cert-name" , self ._lineage_name (domain )])
364236 if staging_enabled ():
365237 base_cmd .extend (["--staging" ])
238+ # Allow local ACME test servers (for example Pebble) without changing
239+ # the production/staging defaults.
240+ acme_server = os .environ .get ("ACME_DIRECTORY_URL" , "" ).strip ()
241+ if acme_server :
242+ base_cmd .extend (["--server" , acme_server ])
366243
367244 if getattr (self .provider , 'CERTBOT_PROPAGATION_SECONDS' ):
368245 propagation_seconds = self .provider .CERTBOT_PROPAGATION_SECONDS
0 commit comments