registry.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. """Host-side enterprise -> pinned Gogs checkout mapping and post-initialization entry."""
  2. import argparse
  3. import json
  4. import os
  5. from pathlib import Path
  6. import re
  7. import subprocess
  8. import sys
  9. import uuid
  10. sys.dont_write_bytecode = True
  11. from bootstrap import COMPANY, Stopped, atomic_create, encode, need, plain, private_json, sealed, sha
  12. from host_bootstrap import initialize, run
  13. def register(registry, checkout, commit, apply=False, runner=subprocess.run):
  14. registry, checkout = plain(registry), plain(checkout)
  15. cfg, _ = sealed(checkout)
  16. need(re.fullmatch('[a-f0-9]{40}', commit), 'PINNED_COMMIT_REQUIRED')
  17. need(run(['git', '-C', str(checkout), 'rev-parse', 'HEAD'], runner).decode().strip() == commit,
  18. 'CONFIGURATION_COMMIT_MISMATCH')
  19. remote = run(['git', '-C', str(checkout), 'remote', 'get-url', 'origin'], runner).decode().strip()
  20. expected = 'https://git.fmode.cn/' + cfg['organization'] + '/' + cfg['repository'] + '.git'
  21. need(remote == expected, 'CONFIGURATION_REPOSITORY_MISMATCH')
  22. need(not run(['git', '-C', str(checkout), 'status', '--porcelain'], runner).strip(),
  23. 'CONFIGURATION_CHECKOUT_CHANGED')
  24. before = registry.read_bytes() if registry.exists() else None
  25. mapping = private_json(registry) if before is not None else {'schema': 'ksf.enterprise-registry.v1', 'companies': {}}
  26. need(set(mapping) == {'schema', 'companies'} and mapping['schema'] == 'ksf.enterprise-registry.v1'
  27. and isinstance(mapping['companies'], dict), 'INVALID_ENTERPRISE_REGISTRY')
  28. need(checkout != registry and checkout not in registry.parents, 'REGISTRY_INSIDE_CHECKOUT')
  29. entry = {'repositoryURL': expected, 'checkout': str(checkout), 'commit': commit,
  30. 'packageManifestSha256': sha((checkout / 'FILES.sha256').read_bytes())}
  31. changed = mapping['companies'].get(COMPANY) != entry
  32. mapping['companies'][COMPANY] = entry
  33. if changed and apply:
  34. if before is not None:
  35. atomic_create(registry.with_name(registry.name + '.before-' + uuid.uuid4().hex), before)
  36. temp = registry.with_name('.' + registry.name + '.' + uuid.uuid4().hex)
  37. try:
  38. atomic_create(temp, encode(mapping))
  39. need((registry.read_bytes() if registry.exists() else None) == before, 'REGISTRY_CONCURRENT_CHANGE')
  40. os.replace(temp, registry)
  41. finally:
  42. if temp.exists():
  43. temp.unlink()
  44. return {'ok': True, 'planOnly': not apply, 'mappingChanged': changed and apply,
  45. 'companyObjectId': COMPANY, 'configuration': entry, 'businessWrites': 0}
  46. def after_twin(registry, container, apply=False, runner=subprocess.run):
  47. need(re.fullmatch('[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}', container), 'INVALID_CONTAINER_NAME')
  48. current = json.loads(run(['docker', 'exec', '--user', '10000:10000', container,
  49. 'cat', '/opt/data/.fmode/config/enterprise/current.json'], runner))
  50. mapping = private_json(registry)
  51. need(mapping.get('schema') == 'ksf.enterprise-registry.v1', 'INVALID_ENTERPRISE_REGISTRY')
  52. entry = mapping.get('companies', {}).get(current.get('companyObjectId'))
  53. if entry is None:
  54. return {'ok': True, 'skipped': True, 'reason': 'NO_ENTERPRISE_WORKSHOP_MAPPING', 'businessWrites': 0}
  55. need(current.get('companyObjectId') == COMPANY, 'UNSUPPORTED_ENTERPRISE')
  56. root = plain(entry['checkout'])
  57. need(sha((root / 'FILES.sha256').read_bytes()) == entry['packageManifestSha256'], 'REGISTERED_PACKAGE_CHANGED')
  58. return initialize(root, entry['commit'], container, apply, runner)
  59. def main():
  60. parser = argparse.ArgumentParser(description=__doc__)
  61. sub = parser.add_subparsers(dest='command', required=True)
  62. bind = sub.add_parser('bind')
  63. bind.add_argument('--checkout', type=Path, required=True)
  64. bind.add_argument('--commit', required=True)
  65. hook = sub.add_parser('after-twin')
  66. hook.add_argument('--container', required=True)
  67. for p in (bind, hook):
  68. p.add_argument('--registry', type=Path, required=True)
  69. p.add_argument('--apply', action='store_true')
  70. args = parser.parse_args()
  71. need(args.registry.is_absolute(), 'ABSOLUTE_PATH_REQUIRED')
  72. if args.command == 'bind':
  73. need(args.checkout.is_absolute(), 'ABSOLUTE_PATH_REQUIRED')
  74. result = register(args.registry, args.checkout, args.commit, args.apply)
  75. else:
  76. result = after_twin(args.registry, args.container, args.apply)
  77. print(json.dumps(result, ensure_ascii=False, indent=2))
  78. if __name__ == '__main__':
  79. try:
  80. main()
  81. except (Stopped, OSError, ValueError, KeyError, subprocess.TimeoutExpired) as error:
  82. print(json.dumps({'ok': False, 'error': {'code': str(error) if isinstance(error, Stopped)
  83. else 'ENTERPRISE_REGISTRY_STOPPED'}}), file=sys.stderr)
  84. sys.exit(1)