summaryrefslogtreecommitdiff
path: root/tools/sops-inventory
diff options
context:
space:
mode:
Diffstat (limited to 'tools/sops-inventory')
-rw-r--r--tools/sops-inventory/default.nix5
-rw-r--r--tools/sops-inventory/sops_inventory/__main__.py107
2 files changed, 63 insertions, 49 deletions
diff --git a/tools/sops-inventory/default.nix b/tools/sops-inventory/default.nix
index 94c455e5..2fc485a7 100644
--- a/tools/sops-inventory/default.nix
+++ b/tools/sops-inventory/default.nix
@@ -11,9 +11,4 @@ in mach-nix.lib.${system}.buildPythonPackage {
11 requirements = '' 11 requirements = ''
12 pyyaml 12 pyyaml
13 ''; 13 '';
14
15 postInstall = ''
16 wrapProgram $out/bin/sops-inventory \
17 --set-default SOPS_INVENTORY_BASE ${self}
18 '';
19} 14}
diff --git a/tools/sops-inventory/sops_inventory/__main__.py b/tools/sops-inventory/sops_inventory/__main__.py
index 68f72b60..47100c17 100644
--- a/tools/sops-inventory/sops_inventory/__main__.py
+++ b/tools/sops-inventory/sops_inventory/__main__.py
@@ -5,16 +5,39 @@ from collections import deque, defaultdict
5 5
6import argparse 6import argparse
7 7
8import subprocess
9
10from operator import attrgetter, itemgetter
11
8from yaml import load, YAMLError 12from yaml import load, YAMLError
9try: 13try:
10 from yaml import CLoader as Loader 14 from yaml import CLoader as Loader
11except ImportError: 15except ImportError:
12 from yaml import Loader 16 from yaml import Loader
13 17
14
15SOPS_TYPES = frozenset({'kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'age', 'pgp'}) 18SOPS_TYPES = frozenset({'kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'age', 'pgp'})
16 19
17 20
21def readnull(fh):
22 buffer = b''
23
24 while True:
25 chunk = fh.read(4096)
26 buffer += chunk
27 if not buffer:
28 break
29
30 while True:
31 lines = buffer.split(b'\0', maxsplit=1)
32 match lines:
33 case [l, r]:
34 buffer = r
35 yield l
36 case _:
37 if not chunk:
38 yield buffer
39 break
40
18class BooleanAction(argparse.Action): 41class BooleanAction(argparse.Action):
19 def __init__(self, option_strings, dest, nargs=None, **kwargs): 42 def __init__(self, option_strings, dest, nargs=None, **kwargs):
20 super(BooleanAction, self).__init__(option_strings, dest, nargs=0, **kwargs) 43 super(BooleanAction, self).__init__(option_strings, dest, nargs=0, **kwargs)
@@ -24,55 +47,51 @@ class BooleanAction(argparse.Action):
24 47
25 48
26def main(): 49def main():
27 default_base = os.getenv('SOPS_INVENTORY_BASE', default=[])
28 if default_base:
29 default_base = Path(default_base)
30
31 parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 50 parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
32 parser.add_argument('--list-files', '--no-list-files', action=BooleanAction, default=False, help='Only list sops files') 51 parser.add_argument('--list-files', '--no-list-files', action=BooleanAction, default=False, help='Only list sops files')
33 parser.add_argument('path', metavar='PATH', nargs='?' if default_base else None, type=Path, default=default_base, help='Base directory to take inventory of') 52 parser.add_argument('path', metavar='PATH', nargs='?', type=Path, default=Path('.'), help='Base directory to take inventory of')
34 args = parser.parse_args() 53 args = parser.parse_args()
35 54
36 inventory = defaultdict(set) 55 inventory = defaultdict(list)
37 56
38 queue = deque([args.path]) 57 with subprocess.Popen(['git', '-C', args.path, 'ls-files', '-z'], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE) as proc:
39 while queue: 58 files = sorted(map(lambda child: args.path / child.decode('utf-8').strip(), readnull(proc.stdout)), key=attrgetter('parts'))
40 baseDir = queue.popleft() 59 for child in files:
41 for child in baseDir.iterdir(): 60 try:
42 if child.is_dir(): 61 with child.open(mode='r') as fh:
43 queue.append(child) 62 yaml = load(fh, Loader=Loader)
44 else: 63 if not yaml:
45 try: 64 raise ValueError('Could not parse YAML')
46 with child.open(mode='r') as fh: 65 if not isinstance(yaml, dict) or not 'sops' in yaml:
47 yaml = load(fh, Loader=Loader) 66 raise ValueError('Did not find "sops" key')
48 if not yaml: 67 sops = yaml['sops']
49 raise ValueError('Could not parse YAML') 68
50 if not isinstance(yaml, dict) or not 'sops' in yaml: 69 key_info = set()
51 raise ValueError('Did not find "sops" key') 70 for k in SOPS_TYPES:
52 sops = yaml['sops'] 71 if k in sops:
53 72 v = sops[k]
54 key_info = set() 73 if not v:
55 for k in SOPS_TYPES: 74 continue
56 if k in sops: 75
57 v = sops[k] 76 match k:
58 if not v: 77 case 'pgp':
59 continue 78 for r in v:
60 79 key_info.add(r['fp'])
61 match k: 80 case 'age':
62 case 'pgp': 81 for r in v:
63 for r in v: 82 key_info.add(r['recipient'])
64 key_info.add(r['fp']) 83 case _:
65 case 'age': 84 raise NotImplementedError
66 for r in v: 85 inventory[frozenset(key_info)].append(child.relative_to(args.path))
67 key_info.add(r['recipient']) 86 except (YAMLError, ValueError) as e:
68 case _: 87 pass
69 raise NotImplementedError 88
70 inventory[frozenset(key_info)].add(child.relative_to(args.path)) 89 proc.wait(timeout=1)
71 except (YAMLError, ValueError) as e: 90 if proc.returncode != 0:
72 pass 91 raise RuntimeError(f'git ls-files returned with {proc.returncode}')
73 92
74 if not args.list_files: 93 if not args.list_files:
75 for keys, files in inventory.items(): 94 for keys, files in sorted(inventory.items(), key=itemgetter(0)):
76 print(','.join(keys) + ':') 95 print(','.join(keys) + ':')
77 for file in files: 96 for file in files:
78 print(' - ' + str(file)) 97 print(' - ' + str(file))