|  | 
| 8 | 8 | from pyinfra import host | 
| 9 | 9 | from pyinfra.api import OperationError, operation | 
| 10 | 10 | from pyinfra.facts.gpg import GpgKeyrings | 
|  | 11 | +from pyinfra.facts import files as file_facts | 
| 11 | 12 | 
 | 
| 12 | 13 | from . import files | 
| 13 | 14 | 
 | 
| @@ -277,6 +278,64 @@ def key( | 
| 277 | 278 |     # After validation, we know dest is not None for installation | 
| 278 | 279 |     assert dest is not None, "dest should not be None after validation" | 
| 279 | 280 | 
 | 
|  | 281 | +    # Check if key already exists (for idempotence) | 
|  | 282 | +    if keyid: | 
|  | 283 | +        # If we have keyid(s), check if they exist in the destination keyring | 
|  | 284 | +        try: | 
|  | 285 | +            dest_dir = str(PurePosixPath(dest).parent) | 
|  | 286 | +            keyring_fact = host.get_fact(GpgKeyrings, [dest_dir]) | 
|  | 287 | + | 
|  | 288 | +            # keyring_fact contains keyring paths as keys | 
|  | 289 | +            # Check if our destination keyring exists in the fact | 
|  | 290 | +            keyring_info = keyring_fact.get(dest) | 
|  | 291 | + | 
|  | 292 | +            if keyring_info: | 
|  | 293 | +                existing_keys = keyring_info["keys"] | 
|  | 294 | +                keyids_to_check = keyid if isinstance(keyid, list) else [keyid] | 
|  | 295 | + | 
|  | 296 | +                # Check if all requested keys already exist | 
|  | 297 | +                all_keys_exist = True | 
|  | 298 | +                for kid in keyids_to_check: | 
|  | 299 | +                    # Remove 0x prefix if present for comparison | 
|  | 300 | +                    clean_keyid = kid.replace("0x", "").replace("0X", "").upper() | 
|  | 301 | +                    key_exists = any( | 
|  | 302 | +                        clean_keyid in existing_key_id.upper() | 
|  | 303 | +                        or existing_key_id.upper().endswith(clean_keyid) | 
|  | 304 | +                        for existing_key_id in existing_keys.keys() | 
|  | 305 | +                    ) | 
|  | 306 | +                    if not key_exists: | 
|  | 307 | +                        all_keys_exist = False | 
|  | 308 | +                        break | 
|  | 309 | + | 
|  | 310 | +                if all_keys_exist: | 
|  | 311 | +                    # All keys already exist, ensure file permissions are correct | 
|  | 312 | +                    yield from files.file._inner( | 
|  | 313 | +                        path=dest, | 
|  | 314 | +                        mode=mode, | 
|  | 315 | +                        present=True, | 
|  | 316 | +                    ) | 
|  | 317 | +                    host.noop(f"GPG keys {keyid} already exist in {dest}") | 
|  | 318 | +                    return | 
|  | 319 | +        except (KeyError, AttributeError): | 
|  | 320 | +            # Fact not available or incomplete, proceed with installation | 
|  | 321 | +            pass | 
|  | 322 | +    else: | 
|  | 323 | +        # If no keyid specified, check if destination file exists (for file/URL sources) | 
|  | 324 | +        try: | 
|  | 325 | +            file_fact = host.get_fact(file_facts.File, dest) | 
|  | 326 | +            if file_fact: | 
|  | 327 | +                # File exists, ensure permissions are correct | 
|  | 328 | +                yield from files.file._inner( | 
|  | 329 | +                    path=dest, | 
|  | 330 | +                    mode=mode, | 
|  | 331 | +                    present=True, | 
|  | 332 | +                ) | 
|  | 333 | +                host.noop(f"GPG keyring {dest} already exists") | 
|  | 334 | +                return | 
|  | 335 | +        except (KeyError, AttributeError): | 
|  | 336 | +            # Fact not available, proceed with installation | 
|  | 337 | +            pass | 
|  | 338 | + | 
| 280 | 339 |     # Ensure destination directory exists | 
| 281 | 340 |     dest_dir = str(PurePosixPath(dest).parent) | 
| 282 | 341 |     yield from files.directory._inner( | 
|  | 
0 commit comments