doran/scripts/hetzner/forgejo-bootstrap.py

136 lines
5.9 KiB
Python
Executable file

#!/usr/bin/env python3
import argparse
import base64
import json
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
import yaml
from nacl import encoding, public
class ForgejoClient:
def __init__(self, base_url: str, username: str, password: str):
self.base_url = base_url.rstrip('/')
credentials = base64.b64encode(f'{username}:{password}'.encode()).decode()
self.headers = {
'Authorization': f'Basic {credentials}',
'Accept': 'application/json',
'Content-Type': 'application/json',
}
self.ssl_context = ssl.create_default_context()
def request(self, method: str, path: str, payload=None, expected=(200, 201, 204)):
url = f'{self.base_url}{path}'
data = None
if payload is not None:
data = json.dumps(payload).encode()
req = urllib.request.Request(url, data=data, method=method)
for key, value in self.headers.items():
req.add_header(key, value)
try:
with urllib.request.urlopen(req, context=self.ssl_context) as response:
body = response.read().decode() if response.length != 0 else ''
if response.status not in expected:
raise RuntimeError(f'{method} {path} returned {response.status}: {body}')
if not body:
return None
return json.loads(body)
except urllib.error.HTTPError as exc:
body = exc.read().decode()
if exc.code not in expected:
raise RuntimeError(f'{method} {path} returned {exc.code}: {body}') from exc
return json.loads(body) if body else None
def get_repo(self, owner: str, repo: str):
try:
return self.request('GET', f'/api/v1/repos/{owner}/{repo}')
except RuntimeError as exc:
if ' returned 404:' in str(exc):
return None
raise
def create_repo(self, name: str, private: bool):
return self.request('POST', '/api/v1/user/repos', {
'name': name,
'private': private,
'auto_init': False,
'default_branch': 'main',
}, expected=(201,))
def upsert_variable(self, owner: str, repo: str, name: str, value: str):
try:
self.request('POST', f'/api/v1/repos/{owner}/{repo}/actions/variables/{urllib.parse.quote(name)}', {
'value': value,
}, expected=(201,))
except RuntimeError as exc:
if ' returned 409:' not in str(exc) and ' returned 422:' not in str(exc):
raise
self.request('PUT', f'/api/v1/repos/{owner}/{repo}/actions/variables/{urllib.parse.quote(name)}', {
'value': value,
}, expected=(201, 204))
def upsert_secret(self, owner: str, repo: str, name: str, value: str):
key = self.request('GET', f'/api/v1/repos/{owner}/{repo}/actions/secrets/public-key')
public_key = public.PublicKey(key['key'].encode(), encoder=encoding.Base64Encoder)
sealed_box = public.SealedBox(public_key)
encrypted_value = base64.b64encode(sealed_box.encrypt(value.encode())).decode()
self.request('PUT', f'/api/v1/repos/{owner}/{repo}/actions/secrets/{urllib.parse.quote(name)}', {
'encrypted_value': encrypted_value,
'key_id': key['key_id'],
}, expected=(201, 204))
def render_ci_kubeconfig(source: Path) -> str:
config = yaml.safe_load(source.read_text())
clusters = config.get('clusters') or []
if not clusters:
raise SystemExit('kubeconfig does not contain any clusters')
clusters[0]['cluster']['server'] = 'https://kubernetes.default.svc:443'
return yaml.safe_dump(config, sort_keys=False)
def main():
parser = argparse.ArgumentParser(description='Bootstrap Forgejo repo secrets/variables for CI/CD')
parser.add_argument('--forgejo-url', required=True)
parser.add_argument('--admin-username', required=True)
parser.add_argument('--admin-password', required=True)
parser.add_argument('--repo-owner', required=True)
parser.add_argument('--repo-name', required=True)
parser.add_argument('--repo-private', action='store_true', default=False)
parser.add_argument('--kubeconfig', required=True)
parser.add_argument('--registry-username', required=True)
parser.add_argument('--registry-password', required=True)
parser.add_argument('--registry-host', required=True)
parser.add_argument('--project-name', required=True)
parser.add_argument('--project-namespace', required=True)
parser.add_argument('--project-deployments', required=True)
args = parser.parse_args()
client = ForgejoClient(args.forgejo_url, args.admin_username, args.admin_password)
repo = client.get_repo(args.repo_owner, args.repo_name)
if repo is None:
created = client.create_repo(args.repo_name, args.repo_private)
print(f'created repo {created["full_name"]}')
else:
print(f'repo already exists: {repo["full_name"]}')
ci_kubeconfig = render_ci_kubeconfig(Path(args.kubeconfig))
client.upsert_secret(args.repo_owner, args.repo_name, 'KUBECONFIG_B64', base64.b64encode(ci_kubeconfig.encode()).decode())
client.upsert_secret(args.repo_owner, args.repo_name, 'REGISTRY_USERNAME', args.registry_username)
client.upsert_secret(args.repo_owner, args.repo_name, 'REGISTRY_PASSWORD', args.registry_password)
print('upserted repo action secrets')
client.upsert_variable(args.repo_owner, args.repo_name, 'REGISTRY_HOST', args.registry_host)
client.upsert_variable(args.repo_owner, args.repo_name, 'PROJECT_NAME', args.project_name)
client.upsert_variable(args.repo_owner, args.repo_name, 'PROJECT_NAMESPACE', args.project_namespace)
client.upsert_variable(args.repo_owner, args.repo_name, 'PROJECT_DEPLOYMENTS', args.project_deployments)
print('upserted repo action variables')
if __name__ == '__main__':
main()