import re import os import signal import shutil import subprocess import zipfile import logging import requests from django.core.management import call_command, BaseCommand, CommandError from django.utils.functional import cached_property from filelock import Timeout, FileLock logger = logging.getLogger(__name__) class Command(BaseCommand): FRONTEND_DIR = 'account_frontend' ASSETS_DIR = FRONTEND_DIR + '/dist' LOCK_FILE = FRONTEND_DIR + '/account_frontend.lock' DOWNLOAD_ASSETS_FILE = FRONTEND_DIR + '/account_frontend_download' VERSION_FILE = ASSETS_DIR + '/static/sso/assets/version.txt' NESTED_STATIC_DIR = ASSETS_DIR + '/accounts/static' STATIC_DIR = ASSETS_DIR + '/static' GITHUB_TOKEN_ENV = 'ACCOUNTS_SITE_GITHUB_TOKEN' LARK_WEBHOOK_ENV = 'ACCOUNT_FRONTEND_LARK_WEBHOOK' def add_arguments(self, parser): parser.add_argument( '--lark_webhook', required=False, dest='notify_lark_webhook', help='A lark robot webhook. Prefer ACCOUNT_FRONTEND_LARK_WEBHOOK.', ) parser.add_argument( '--github_token', required=False, help='A GitHub token. Prefer ACCOUNTS_SITE_GITHUB_TOKEN.', ) parser.add_argument( '--prerelease', default=False, action='store_true', ) def handle(self, *args, **options): self.github_token = self._get_github_token(options.get('github_token')) self.prerelease = options['prerelease'] notify_lark_webhook = ( options.get('notify_lark_webhook') or os.environ.get(self.LARK_WEBHOOK_ENV) ) try: with FileLock(self.LOCK_FILE).acquire(timeout=10): if not self.frontend_latest_version: return if self.frontend_current_version == self.frontend_latest_version: return self.upgrade( self.frontend_latest_assets_download_url, self.frontend_latest_version ) if notify_lark_webhook: self.notify(notify_lark_webhook) except Timeout: raise Exception('Upgrade account-frontend: failed to acquire filelock') @cached_property def frontend_latest_version(self): resp = requests.get( 'https://api.github.com/repos/pingcap-inc/accounts-site/releases', headers={ 'Content-Type': 'application/json; charset=utf-8', 'Authorization': 'token {}'.format(self.github_token) }, timeout=30 ) if resp.status_code != 200: raise Exception('Get account-frontend releases failed, status_code: {}'.format( resp.status_code)) result = list(filter(lambda r: r['prerelease'] == self.prerelease, resp.json())) if not result: return None result = result[0] assets = result['assets'][0] assert assets['content_type'] == 'application/zip' self._frontend_latest_assets_download_url = f'https://api.github.com/repos/pingcap-inc/accounts-site/releases/assets/{assets["id"]}' return result['tag_name'] @cached_property def frontend_latest_assets_download_url(self): if not hasattr(self, '_frontend_latest_assets_download_url'): self.frontend_current_version() return self._frontend_latest_assets_download_url @cached_property def frontend_current_version(self): if not os.path.exists(self.VERSION_FILE): return None with open(self.VERSION_FILE, 'a+') as f: f.seek(0) return f.read() def upgrade(self, assets_url, version): shutil.rmtree(self.ASSETS_DIR, onerror=FileNotFoundError) resp = requests.get( assets_url, headers={ 'Accept': 'application/octet-stream', 'Authorization': 'token {}'.format(self.github_token) }, timeout=30, stream=True ) with open(self.DOWNLOAD_ASSETS_FILE, 'wb') as f: for chunk in resp.iter_content(chunk_size=1024): if chunk: f.write(chunk) if resp.status_code != 200: raise Exception('Download account-frontend assets failed, status_code: {}'.format( resp.status_code)) with zipfile.ZipFile(self.DOWNLOAD_ASSETS_FILE, 'r') as zip_ref: zip_ref.extractall(self.FRONTEND_DIR) self._normalize_static_assets_dir() self._assert_path_exists('{}/index.html'.format(self.ASSETS_DIR)) self._assert_path_exists('{}/sso/assets'.format(self.STATIC_DIR)) with open(self.VERSION_FILE, 'w+') as f: f.write(version) shutil.copyfile( '{}/index.html'.format(self.ASSETS_DIR), 'pingcap_sso/jinja2/pingcap_sso/index.html' ) call_command('collectstatic', '--clear', '--noinput') try: supervisor_output = subprocess.check_output( ['supervisorctl', 'status', 'account']).decode('utf8') except FileNotFoundError: logger.error( 'Please call this command on production, `supervisorctl` should be installed.') exit() pid = int(re.findall(r'.*pid ([0-9]+), .*', supervisor_output)[0]) os.kill(pid, signal.SIGHUP) def _assert_path_exists(self, file_path): assert os.path.exists(file_path), 'Upgrade account-frontend, {} not exists'.format(file_path) def _get_github_token(self, option_value=None): token = option_value or os.environ.get(self.GITHUB_TOKEN_ENV) if not token: raise CommandError( 'GitHub token is required. Set {} or pass --github_token.'.format( self.GITHUB_TOKEN_ENV ) ) return token def _normalize_static_assets_dir(self): """Accept both the canonical and the v1.5.2 nested artifact layouts.""" if os.path.exists(self.STATIC_DIR): return if not os.path.exists(self.NESTED_STATIC_DIR): return shutil.move(self.NESTED_STATIC_DIR, self.STATIC_DIR) try: os.rmdir(os.path.dirname(self.NESTED_STATIC_DIR)) except OSError: pass def notify(self, lark_webhook): if self.prerelease: title = 'Account 预览环境前端更新' online_version_check_text = '检查预览环境当前版本' online_version_check_url = 'https://accounts-preview.pingcap.cn/static/sso/assets/version.txt' else: title = 'Account 线上前端更新' online_version_check_text = '检查线上当前版本' online_version_check_url = 'https://accounts.pingcap.cn/static/sso/assets/version.txt' payload = { "msg_type": "interactive", "card": { "config": { "wide_screen_mode": True, "enable_forward": True }, "elements": [{ "tag": "div", "text": { "content": f"**Version:** {self.frontend_latest_version}", "tag": "lark_md" } }, { "actions": [{ "tag": "button", "text": { "content": online_version_check_text, "tag": "lark_md" }, "url": online_version_check_url, "type": "default", "value": {} }], "tag": "action" }], "header": { "title": { "content": title, "tag": "plain_text" } } } } try: resp = requests.post( lark_webhook, json=payload, headers={"Content-Type": "application/json"}, timeout=5 ) if resp.status_code != 200: logger.error('Send message to lark failed, status: {}'.format( resp.status_code)) if resp.json().get('StatusCode') != 0: logger.error('Send message to lark failed, msg: {}'.format( resp.content)) except Exception as e: logger.error(e)