77
88from pyinfra import host
99from pyinfra .api import OperationError , operation
10+ from pyinfra .facts .gpg import GpgKeyrings
1011
1112from . import files
1213
@@ -85,58 +86,73 @@ def key(
8586 # For removal, handle different scenarios
8687 if present is False :
8788 if not dest and keyid :
88- # Remove key(s) from all APT keyrings
89+ # Remove key(s) from all keyrings found in APT directories
8990 if isinstance (keyid , str ):
9091 keyid = [keyid ]
9192
92- # Define all APT keyring locations
93- # Not sure this is the best way to do this
94- # Cannot find a more generic way to get gpg keyring locations
95- keyring_patterns = [
96- "/etc/apt/trusted.gpg.d/*.gpg" ,
97- "/etc/apt/keyrings/*.gpg" ,
98- "/usr/share/keyrings/*.gpg" ,
99- ]
100-
101- for pattern in keyring_patterns :
93+ # Use the GpgKeyrings fact to find all keyrings with default APT directories
94+ apt_directories = [ "/etc/apt/trusted.gpg.d" , "/etc/apt/keyrings" , "/usr/share/keyrings" ]
95+ keyrings_info = host . get_fact ( GpgKeyrings , directories = apt_directories )
96+
97+ for keyring_path , keyring_data in keyrings_info . items ():
98+ # Get the keys from the GpgKeyrings fact data
99+ keys_in_keyring = keyring_data . get ( "keys" , {})
100+
101+ # Check if any of the target keys exist in this keyring
102+ keys_to_remove = []
102103 for kid in keyid :
103- # Remove key from all matching keyrings
104- yield (
105- f'for keyring in { pattern } ; do [ -e "$keyring" ] && '
106- f'gpg --batch --no-default-keyring --keyring "$keyring" '
107- f"--delete-keys { kid } 2>/dev/null || true; done"
104+ # Handle different key ID formats (short, long, with/without 0x prefix)
105+ clean_key = kid .replace ("0x" , "" ).replace ("0X" , "" ).upper ()
106+
107+ # Check for exact match or if the key ID is a suffix/prefix of any key in the keyring
108+ for existing_key_id in keys_in_keyring .keys ():
109+ if (clean_key == existing_key_id .upper () or
110+ existing_key_id .upper ().endswith (clean_key ) or
111+ existing_key_id .upper ().startswith (clean_key )):
112+ keys_to_remove .append (existing_key_id )
113+
114+ if keys_to_remove :
115+ # For APT keyrings, remove the entire keyring file if any target keys are found
116+ # This is the safest approach for APT key management
117+ yield from files .file ._inner (
118+ path = keyring_path ,
119+ present = False ,
108120 )
109121
110- # Clean up empty keyrings
111- yield (
112- f'for keyring in { pattern } ; do [ -e "$keyring" ] && '
113- f'! gpg --batch --no-default-keyring --keyring "$keyring" '
114- f'--list-keys 2>/dev/null | grep -q "pub" && rm -f "$keyring" || true; done'
115- )
116-
117122 return
118123
119124 elif dest and keyid :
120- # For APT keyring files, use a simpler approach:
121- # Check if keys exist in file, and if so, remove the entire file
122- # This is appropriate for most APT use cases
125+ # Remove specific key(s) from a specific keyring file
123126 if isinstance (keyid , str ):
124127 keyid = [keyid ]
125128
126- # Build a condition to check if any of the keys exist in the file
127- key_checks = []
128- for kid in keyid :
129- # Strip 0x prefix if present and handle both short and long key formats
130- clean_key = kid .replace ("0x" , "" ).replace ("0X" , "" )
131- key_checks .append (
132- f'gpg --batch --no-default-keyring --keyring "{ dest } " '
133- f'--list-keys 2>/dev/null | grep -qi "{ clean_key } "'
134- )
135-
136- condition = " || " .join (key_checks )
137-
138- # If any of the keys exist in the file, remove the entire file
139- yield (f'if [ -f "{ dest } " ] && ({ condition } ); then ' f'rm -f "{ dest } "; fi' )
129+ # Check if the destination keyring exists and contains the target keys
130+ keyrings_info = host .get_fact (GpgKeyrings , directories = [str (PurePosixPath (dest ).parent )])
131+
132+ if dest in keyrings_info :
133+ keyring_data = keyrings_info [dest ]
134+ keys_in_keyring = keyring_data .get ("keys" , {})
135+
136+ # Check if any of the target keys exist in this keyring
137+ keys_found = False
138+ for kid in keyid :
139+ clean_key = kid .replace ("0x" , "" ).replace ("0X" , "" ).upper ()
140+ for existing_key_id in keys_in_keyring .keys ():
141+ # Check for exact match, suffix (short key ID), or prefix match
142+ if (clean_key == existing_key_id .upper () or
143+ existing_key_id .upper ().endswith (clean_key ) or
144+ existing_key_id .upper ().startswith (clean_key )):
145+ keys_found = True
146+ break
147+ if keys_found :
148+ break
149+
150+ if keys_found :
151+ # Remove the entire keyring file - safest approach for APT
152+ yield from files .file ._inner (
153+ path = dest ,
154+ present = False ,
155+ )
140156 return
141157
142158 elif dest and not keyid :
@@ -147,6 +163,9 @@ def key(
147163 )
148164 return
149165
166+ else :
167+ raise OperationError ("Invalid parameters for removal operation" )
168+
150169 # For installation, validate required parameters
151170 if not src and not keyserver :
152171 raise OperationError ("Either `src` or `keyserver` must be provided for installation" )
0 commit comments