| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- """Create the enterprise organization/config repository as its designated administrator."""
- import argparse
- import base64
- import json
- import os
- from pathlib import Path
- import subprocess
- import sys
- import tempfile
- import urllib.error
- import urllib.request
- sys.dont_write_bytecode = True
- from bootstrap import HERE, Stopped, need, plain, private_json, sealed, sha
- ORIGIN = 'https://git.fmode.cn'
- class Gogs:
- def __init__(self, token):
- self.token = token
- def call(self, method, route, body=None, missing=False):
- need(route.startswith('/api/v1/') and '..' not in route, 'INVALID_GOGS_ROUTE')
- request = urllib.request.Request(ORIGIN + route, method=method,
- headers={'Authorization': 'token ' + self.token,
- 'Content-Type': 'application/json'},
- data=None if body is None else json.dumps(body).encode())
- # Credentials may only go to this origin; Gogs API calls do not need redirects.
- class NoRedirect(urllib.request.HTTPRedirectHandler):
- def redirect_request(self, *args, **kwargs):
- return None
- try:
- with urllib.request.build_opener(NoRedirect()).open(request, timeout=30) as response:
- raw = response.read(2 * 1024 * 1024)
- return json.loads(raw) if raw else None
- except urllib.error.HTTPError as error:
- if error.code == 404 and missing:
- return None
- raise Stopped('GOGS_HTTP_' + str(error.code)) from None
- def lookup_organization(api, name):
- org = api.call('GET', '/api/v1/orgs/' + name, missing=True)
- if org:
- return org
- # A missing route is not evidence that an organization does not exist.
- mine = api.call('GET', '/api/v1/user/orgs')
- need(isinstance(mine, list), 'GOGS_ORGANIZATION_LIST_INVALID')
- matches = [row for row in mine if (row.get('username') or row.get('login')) == name]
- need(len(matches) <= 1, 'GOGS_ORGANIZATION_LIST_INVALID')
- if matches:
- return matches[0]
- # This also detects a same-name organization outside the administrator's memberships.
- account = api.call('GET', '/api/v1/users/' + name, missing=True)
- need(account is None, 'EXISTING_ORGANIZATION_MEMBERSHIP_UNVERIFIED')
- return None
- def inspect(api, cfg):
- who = api.call('GET', '/api/v1/user')
- need((who.get('username') or who.get('login')) == cfg['administrator'], 'GOGS_ADMINISTRATOR_MISMATCH')
- org = lookup_organization(api, cfg['organization'])
- owner_verified = False
- if org:
- teams = api.call('GET', '/api/v1/orgs/' + cfg['organization'] + '/teams', missing=True)
- if teams is not None:
- owners = [t for t in teams if t['name'] == 'Owners']
- need(len(owners) == 1, 'GOGS_OWNER_TEAM_MISSING')
- members = api.call('GET', '/api/v1/teams/' + str(owners[0]['id']) + '/members', missing=True)
- if members is None:
- members = api.call('GET', '/api/v1/admin/teams/' + str(owners[0]['id']) + '/members')
- need(any((m.get('username') or m.get('login')) == cfg['administrator'] for m in members),
- 'EXISTING_ORGANIZATION_OWNER_MISMATCH')
- owner_verified = True
- repo = api.call('GET', '/api/v1/repos/' + cfg['organization'] + '/' + cfg['repository'], missing=True)
- if repo:
- need(repo.get('private') is True and repo.get('owner', {}).get('username',
- repo.get('owner', {}).get('login')) == cfg['organization'], 'EXISTING_REPOSITORY_MISMATCH')
- return {'organizationExists': bool(org), 'repositoryExists': bool(repo),
- 'organizationOwnerVerified': owner_verified,
- 'ownerVerificationUnavailable': bool(org) and not owner_verified,
- 'administrator': cfg['administrator'], 'organization': cfg['organization'],
- 'repository': cfg['repository']}
- def git(args, cwd, env):
- result = subprocess.run(['git', *args], cwd=cwd, env=env, capture_output=True, timeout=120)
- need(result.returncode == 0, 'CONFIGURATION_GIT_OPERATION_FAILED')
- return result.stdout
- def repository_state(existing, root, files):
- expected = set(files) | {'FILES.sha256'}
- if set(existing) == expected and all(existing[n] == (root / n).read_bytes() for n in expected):
- return 'current'
- prior = json.loads((root / 'previous-release.json').read_bytes())['files']
- need(set(existing) == set(prior) and set(prior) <= expected
- and all(sha(existing[n]) == digest for n, digest in prior.items()),
- 'EXISTING_REPOSITORY_CONTENT_CHANGED')
- return 'known-r1'
- def create_organization(api, cfg):
- org = cfg['organization']
- description = '企业配置来源;companyObjectId=' + cfg['companyObjectId']
- try:
- return api.call('POST', '/api/v1/orgs',
- {'username': org, 'full_name': cfg['companyName'], 'description': description})
- except Stopped as error:
- if str(error) != 'GOGS_HTTP_404':
- raise
- # Gogs 0.13 exposes organization creation on the admin user route, with form field names.
- # An uncertain response or authorization failure never triggers a second write attempt.
- existing = lookup_organization(api, org)
- if existing is not None:
- return existing
- return api.call('POST', '/api/v1/admin/users/' + cfg['administrator'] + '/orgs',
- {'UserName': org, 'FullName': cfg['companyName'], 'Description': description})
- def setup(api, root, apply=False):
- root = plain(root)
- cfg, files = sealed(root)
- status = inspect(api, cfg)
- if not apply:
- return {'ok': True, 'planOnly': True, **status, 'productionConfigurationChanged': False}
- org, name = cfg['organization'], cfg['repository']
- if not status['organizationExists']:
- create_organization(api, cfg)
- # Revalidate ownership before creating a repository in an existing/new organization.
- status = inspect(api, cfg)
- need(status['organizationExists'] and status['organizationOwnerVerified'], 'GOGS_OWNER_VERIFICATION_UNAVAILABLE')
- if not status['repositoryExists']:
- api.call('POST', '/api/v1/org/' + org + '/repos',
- {'name': name, 'description': '康师傅工作坊配置与初始化组件;不存放凭据',
- 'private': True, 'auto_init': False})
- repo = api.call('GET', '/api/v1/repos/' + org + '/' + name)
- need(repo.get('private') is True, 'REPOSITORY_NOT_PRIVATE')
- env = dict(os.environ)
- env.update(GIT_TERMINAL_PROMPT='0', GIT_CONFIG_COUNT='2',
- GIT_CONFIG_KEY_0='http.' + ORIGIN + '/.extraheader',
- GIT_CONFIG_VALUE_0='Authorization: Basic ' + base64.b64encode(
- (cfg['administrator'] + ':' + api.token).encode()).decode(),
- GIT_CONFIG_KEY_1='http.followRedirects', GIT_CONFIG_VALUE_1='false')
- remote = ORIGIN + '/' + org + '/' + name + '.git'
- with tempfile.TemporaryDirectory(prefix='ksf-enterprise-git-') as temp:
- staging = Path(temp)
- git(['init', '--initial-branch=main'], staging, env)
- git(['config', 'core.autocrlf', 'false'], staging, env)
- git(['remote', 'add', 'origin', remote], staging, env)
- refs = git(['ls-remote', 'origin'], staging, env).decode().splitlines()
- had_content = bool(refs)
- heads = [x for x in refs if x.endswith('\trefs/heads/main')]
- commit = None
- if refs:
- need(len(heads) == 1, 'EXISTING_REPOSITORY_BASELINE_UNKNOWN')
- git(['fetch', '--depth=1', 'origin', 'main'], staging, env)
- tracked = git(['ls-tree', '-r', '--name-only', 'FETCH_HEAD'], staging, env).decode().splitlines()
- existing = {n: git(['show', 'FETCH_HEAD:' + n], staging, env) for n in tracked}
- if repository_state(existing, root, files) == 'current':
- commit = git(['rev-parse', 'FETCH_HEAD'], staging, env).decode().strip()
- else:
- git(['checkout', '-B', 'main', 'FETCH_HEAD'], staging, env)
- changed = commit is None
- if changed:
- for relative in [*files, 'FILES.sha256']:
- dest = staging / relative
- dest.parent.mkdir(parents=True, exist_ok=True)
- dest.write_bytes((root / relative).read_bytes())
- git(['config', 'user.name', 'Kangshifu configuration release'], staging, env)
- git(['config', 'user.email', 'ksf-config@fmode.cn'], staging, env)
- git(['add', '--', *sorted(files), 'FILES.sha256'], staging, env)
- git(['commit', '-m', 'Configure enterprise workshop and required VOC runtime'], staging, env)
- commit = git(['rev-parse', 'HEAD'], staging, env).decode().strip()
- git(['push', 'origin', 'HEAD:refs/heads/main'], staging, env)
- refs = git(['ls-remote', 'origin', 'refs/heads/main'], staging, env).decode()
- need(refs.split()[0] == commit, 'GOGS_PUSH_NOT_CONFIRMED')
- # Read every remote file back, including the private configuration manifest.
- git(['fetch', '--depth=1', 'origin', commit], staging, env)
- for relative in [*files, 'FILES.sha256']:
- need(git(['show', 'FETCH_HEAD:' + relative], staging, env) == (root / relative).read_bytes(),
- 'GOGS_READBACK_MISMATCH')
- return {'ok': True, 'planOnly': False, **inspect(api, cfg), 'repositoryURL': remote,
- 'commit': commit, 'repositoryContentCreated': changed and not had_content,
- 'repositoryContentUpdated': changed and had_content, 'private': True,
- 'filesReadBack': len(files) + 1, 'credentialsInRepository': False,
- 'enterprisePlatformBindingInstalled': False, 'businessWrites': 0}
- def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--token-file', required=True, type=Path)
- parser.add_argument('--apply', action='store_true')
- args = parser.parse_args()
- # The supplied private JSON contains only an existing Gogs access token, never a password reset.
- token = private_json(args.token_file).get('token')
- need(isinstance(token, str) and len(token) >= 20 and '\n' not in token, 'GOGS_TOKEN_MISSING')
- print(json.dumps(setup(Gogs(token), HERE, args.apply), ensure_ascii=False, indent=2))
- if __name__ == '__main__':
- try:
- main()
- except (Stopped, OSError, ValueError, KeyError) as error:
- print(json.dumps({'ok': False, 'error': {'code': str(error) if isinstance(error, Stopped)
- else 'GOGS_SETUP_STOPPED'}}), file=sys.stderr)
- sys.exit(1)
|