96 lines
4.8 KiB
Python
96 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Create a local project from the engineering recipe. Never creates remote repositories."""
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
|
|
HOME = Path(__file__).resolve().parent
|
|
|
|
|
|
def create(destination, name, components, repository, tag, runner, landscape):
|
|
destination = Path(destination).resolve()
|
|
if not re.fullmatch('[a-z][a-z0-9-]*', name):
|
|
raise ValueError('Use a lowercase project name with optional digits and hyphens')
|
|
if destination.exists():
|
|
raise ValueError('Destination must not exist; existing project files are never overwritten')
|
|
commit = subprocess.check_output(['git', '-C', str(HOME), 'rev-parse', tag + '^{commit}'], text=True).strip()
|
|
replacements = {'@PROJECT@': name, '@RUNNER@': runner, '@RUNNER_NAME@': runner, '@LANDSCAPE@': landscape,
|
|
'@ENGINEERING@': repository.removesuffix('.git'),
|
|
'@MODULES@': '<module>backend</module>' if 'backend' in components else ''}
|
|
try:
|
|
destination.mkdir(parents=True)
|
|
for part in ['common', *components]:
|
|
for source in (HOME / 'templates' / part).rglob('*'):
|
|
if source.is_file():
|
|
target = destination / source.relative_to(HOME / 'templates' / part)
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
text = source.read_text()
|
|
for before, after in replacements.items():
|
|
text = text.replace(before, after)
|
|
target.write_text(text)
|
|
lock = {'repository': repository, 'tag': tag, 'commit': commit}
|
|
(destination / 'engineering.lock.json').write_text(json.dumps(lock, indent=2) + '\n')
|
|
(destination / 'scripts').mkdir()
|
|
shutil.copy2(HOME / 'bootstrap.py', destination / 'scripts/engineering.py')
|
|
settings = {'PROJECT_NAME': name, 'BACKEND_MODULE': 'backend' if 'backend' in components else '',
|
|
'FRONTEND_MODULE': 'frontend' if 'frontend' in components else '',
|
|
'FORMAT_DIRECTORIES': 'backend/src', 'COMPOSE_PROJECT': name + '-production',
|
|
'BROWSER_TESTS': 'false', 'CI_DATABASE': 'false',
|
|
'TARGET_PLATFORM': 'linux/amd64', 'RUNNER_LABEL': runner,
|
|
'RUNNER_NAME': runner, 'RUNNER_SCOPE': 'user',
|
|
'FRONTEND_TEST_COMMAND': '["npm","test"]', 'DEPENDENCY_SERVICES': '',
|
|
'REQUIRED_DEPLOY_SECRETS': ''}
|
|
(destination / 'engineering.properties').write_text(''.join(k + '=' + v + '\n' for k, v in settings.items()))
|
|
services = []
|
|
published_role = 'frontend' if 'frontend' in components else 'backend'
|
|
for role in components:
|
|
services.append(f''' {role}:
|
|
image: ${{{role.upper()}_IMAGE:?Required verified digest}}
|
|
platform: linux/amd64
|
|
restart: unless-stopped
|
|
labels:
|
|
nl.agilecc.release: ${{RELEASE_VERSION:?Required version}}
|
|
nl.agilecc.revision: ${{RELEASE_SHA:?Required SHA}}
|
|
''')
|
|
if role == published_role:
|
|
services.append(" ports:\n - '${BIND_IP:?Required private IP}:${FRONTEND_PORT:?Required port}:8080'\n")
|
|
(destination / 'deploy/compose.yaml').write_text('services:\n' + ''.join(services))
|
|
if published_role == 'backend':
|
|
p = destination / 'deploy/health.sh'
|
|
p.write_text(p.read_text().replace('$FRONTEND_PORT/', '$FRONTEND_PORT/hello'))
|
|
spec = importlib.util.spec_from_file_location('bootstrap', HOME / 'bootstrap.py')
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
module.materialize(destination)
|
|
except BaseException:
|
|
shutil.rmtree(destination)
|
|
raise
|
|
return destination
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('name')
|
|
parser.add_argument('destination', type=Path)
|
|
parser.add_argument('--components', choices=['backend', 'frontend', 'both'], default='both')
|
|
parser.add_argument('--repository', default='http://192.168.2.28:3000/wanjakrah/engineering.git')
|
|
parser.add_argument('--version', required=True)
|
|
parser.add_argument('--runner', default='mini-pc', help='Shared Gitea host runner label (default: mini-pc)')
|
|
parser.add_argument('--landscape', default='http://192.168.2.28:3000/wanjakrah/landscape')
|
|
args = parser.parse_args()
|
|
try:
|
|
path = create(args.destination, args.name,
|
|
['backend', 'frontend'] if args.components == 'both' else [args.components],
|
|
args.repository, args.version, args.runner, args.landscape)
|
|
print('Created ' + str(path))
|
|
except (ValueError, OSError, subprocess.CalledProcessError) as error:
|
|
parser.exit(1, str(error) + '\n')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|