mirror of https://github.com/openspug/spug
A 添加灰度发布功能
parent
6709feffeb
commit
9889c95a42
|
@ -0,0 +1,138 @@
|
|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
# Copyright: (c) <spug.dev@gmail.com>
|
||||
# Released under the AGPL-3.0 License.
|
||||
from django.conf import settings
|
||||
from libs.utils import AttrDict, render_str, human_seconds_time
|
||||
from apps.host.models import Host
|
||||
from apps.repository.models import Repository
|
||||
from apps.repository.utils import dispatch as build_repository
|
||||
from apps.deploy.helper import SpugError
|
||||
from concurrent import futures
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
|
||||
BUILD_DIR = settings.BUILD_DIR
|
||||
|
||||
|
||||
def ext1_deploy(req, helper, env):
|
||||
if not req.repository_id:
|
||||
rep = Repository(
|
||||
app_id=req.deploy.app_id,
|
||||
env_id=req.deploy.env_id,
|
||||
deploy_id=req.deploy_id,
|
||||
version=req.version,
|
||||
spug_version=req.spug_version,
|
||||
extra=req.extra,
|
||||
remarks='SPUG AUTO MAKE',
|
||||
created_by_id=req.created_by_id
|
||||
)
|
||||
build_repository(rep, helper)
|
||||
req.repository = rep
|
||||
extras = json.loads(req.extra)
|
||||
if extras[0] == 'repository':
|
||||
extras = extras[1:]
|
||||
if extras[0] == 'branch':
|
||||
env.update(SPUG_GIT_BRANCH=extras[1], SPUG_GIT_COMMIT_ID=extras[2])
|
||||
else:
|
||||
env.update(SPUG_GIT_TAG=extras[1])
|
||||
if req.deploy.is_parallel:
|
||||
threads, latest_exception = [], None
|
||||
max_workers = max(10, os.cpu_count() * 5)
|
||||
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
for h_id in helper.deploy_host_ids:
|
||||
new_env = AttrDict(env.items())
|
||||
t = executor.submit(_deploy_ext1_host, req, helper, h_id, new_env)
|
||||
t.h_id = h_id
|
||||
threads.append(t)
|
||||
for t in futures.as_completed(threads):
|
||||
exception = t.exception()
|
||||
if exception:
|
||||
helper.set_deploy_fail(t.h_id)
|
||||
latest_exception = exception
|
||||
if not isinstance(exception, SpugError):
|
||||
helper.send_error(t.h_id, f'Exception: {exception}', with_break=False)
|
||||
else:
|
||||
helper.set_deploy_success(t.h_id)
|
||||
if latest_exception:
|
||||
raise latest_exception
|
||||
else:
|
||||
host_ids = sorted(helper.deploy_host_ids, reverse=True)
|
||||
while host_ids:
|
||||
h_id = host_ids.pop()
|
||||
new_env = AttrDict(env.items())
|
||||
try:
|
||||
_deploy_ext1_host(req, helper, h_id, new_env)
|
||||
helper.set_deploy_success(h_id)
|
||||
except Exception as e:
|
||||
helper.set_deploy_fail(h_id)
|
||||
helper.send_error(h_id, f'Exception: {e}', with_break=False)
|
||||
for h_id in host_ids:
|
||||
helper.set_deploy_fail(h_id)
|
||||
helper.send_error(h_id, '终止发布', with_break=False)
|
||||
raise e
|
||||
|
||||
|
||||
def _deploy_ext1_host(req, helper, h_id, env):
|
||||
flag = time.time()
|
||||
helper.set_deploy_process(h_id)
|
||||
helper.send_clear(h_id)
|
||||
helper.send_info(h_id, '数据准备... ', status='doing')
|
||||
host = Host.objects.filter(pk=h_id).first()
|
||||
if not host:
|
||||
helper.send_error(h_id, 'no such host')
|
||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||
extend = req.deploy.extend_obj
|
||||
extend.dst_dir = render_str(extend.dst_dir, env)
|
||||
extend.dst_repo = render_str(extend.dst_repo, env)
|
||||
env.update(SPUG_DST_DIR=extend.dst_dir)
|
||||
with host.get_ssh(default_env=env) as ssh:
|
||||
helper.save_pid(ssh.get_pid(), h_id)
|
||||
base_dst_dir = os.path.dirname(extend.dst_dir)
|
||||
code, _ = ssh.exec_command_raw(
|
||||
f'mkdir -p {extend.dst_repo} {base_dst_dir} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
|
||||
if code == 0:
|
||||
helper.send_error(host.id,
|
||||
f'\r\n检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。')
|
||||
if req.type == '2':
|
||||
helper.send_warn(h_id, '跳过√\r\n')
|
||||
else:
|
||||
# clean
|
||||
clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf'
|
||||
helper.remote_raw(host.id, ssh, f'cd {extend.dst_repo} && {clean_command}')
|
||||
# transfer files
|
||||
tar_gz_file = f'{req.spug_version}.tar.gz'
|
||||
try:
|
||||
callback = helper.progress_callback(host.id)
|
||||
ssh.put_file(
|
||||
os.path.join(BUILD_DIR, tar_gz_file),
|
||||
os.path.join(extend.dst_repo, tar_gz_file),
|
||||
callback
|
||||
)
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'Exception: {e}')
|
||||
|
||||
command = f'cd {extend.dst_repo} && rm -rf {req.spug_version} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
|
||||
helper.remote_raw(host.id, ssh, command)
|
||||
helper.send_success(h_id, '完成√\r\n')
|
||||
|
||||
# pre host
|
||||
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
|
||||
if extend.hook_pre_host:
|
||||
helper.send_info(h_id, '发布前任务... \r\n')
|
||||
command = f'cd {repo_dir} && {extend.hook_pre_host}'
|
||||
helper.remote(host.id, ssh, command)
|
||||
|
||||
# do deploy
|
||||
helper.send_info(h_id, '执行发布... ')
|
||||
helper.remote_raw(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
|
||||
helper.send_success(h_id, '完成√\r\n')
|
||||
|
||||
# post host
|
||||
if extend.hook_post_host:
|
||||
helper.send_info(h_id, '发布后任务... \r\n')
|
||||
command = f'cd {extend.dst_dir} && {extend.hook_post_host}'
|
||||
helper.remote(host.id, ssh, command)
|
||||
|
||||
human_time = human_seconds_time(time.time() - flag)
|
||||
helper.send_success(h_id, f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
|
@ -0,0 +1,170 @@
|
|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
# Copyright: (c) <spug.dev@gmail.com>
|
||||
# Released under the AGPL-3.0 License.
|
||||
from django.conf import settings
|
||||
from libs.utils import AttrDict, render_str, human_seconds_time
|
||||
from apps.host.models import Host
|
||||
from apps.deploy.helper import SpugError
|
||||
from concurrent import futures
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
|
||||
REPOS_DIR = settings.REPOS_DIR
|
||||
|
||||
|
||||
def ext2_deploy(req, helper, env, with_local):
|
||||
flag = time.time()
|
||||
extend, step = req.deploy.extend_obj, 1
|
||||
host_actions = json.loads(extend.host_actions)
|
||||
server_actions = json.loads(extend.server_actions)
|
||||
env.update({'SPUG_RELEASE': req.version})
|
||||
if req.version:
|
||||
for index, value in enumerate(req.version.split()):
|
||||
env.update({f'SPUG_RELEASE_{index + 1}': value})
|
||||
|
||||
transfer_action = None
|
||||
for action in host_actions:
|
||||
if action.get('type') == 'transfer':
|
||||
action['src'] = render_str(action.get('src', '').strip().rstrip('/'), env)
|
||||
action['dst'] = render_str(action['dst'].strip().rstrip('/'), env)
|
||||
if action.get('src_mode') == '1': # upload when publish
|
||||
if not req.extra:
|
||||
helper.send_error('local', '\r\n未找到上传的文件信息,请尝试新建发布申请')
|
||||
extra = json.loads(req.extra)
|
||||
if 'name' in extra:
|
||||
action['name'] = extra['name']
|
||||
else:
|
||||
transfer_action = action
|
||||
break
|
||||
|
||||
if with_local:
|
||||
helper.set_deploy_process('local')
|
||||
helper.send_success('local', '', status='doing')
|
||||
if server_actions or transfer_action:
|
||||
helper.send_clear('local')
|
||||
for action in server_actions:
|
||||
helper.send_info('local', f'{action["title"]}...\r\n')
|
||||
helper.local(f'cd /tmp && {action["data"]}', env)
|
||||
step += 1
|
||||
if transfer_action:
|
||||
action = transfer_action
|
||||
helper.send_info('local', '检测到来源为本地路径的数据传输动作,执行打包... \r\n')
|
||||
action['src'] = action['src'].rstrip('/ ')
|
||||
action['dst'] = action['dst'].rstrip('/ ')
|
||||
if not action['src'] or not action['dst']:
|
||||
helper.send_error('local', f'Invalid path for transfer, src: {action["src"]} dst: {action["dst"]}')
|
||||
if not os.path.exists(action['src']):
|
||||
helper.send_error('local', f'No such file or directory: {action["src"]}')
|
||||
is_dir, exclude = os.path.isdir(action['src']), ''
|
||||
sp_dir, sd_dst = os.path.split(action['src'])
|
||||
contain = sd_dst
|
||||
if action['mode'] != '0' and is_dir:
|
||||
files = helper.parse_filter_rule(action['rule'], ',', env)
|
||||
if files:
|
||||
if action['mode'] == '1':
|
||||
contain = ' '.join(f'{sd_dst}/{x}' for x in files)
|
||||
else:
|
||||
excludes = []
|
||||
for x in files:
|
||||
if x.startswith('/'):
|
||||
excludes.append(f'--exclude={sd_dst}{x}')
|
||||
else:
|
||||
excludes.append(f'--exclude={x}')
|
||||
exclude = ' '.join(excludes)
|
||||
tar_gz_file = os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, f'{req.spug_version}.tar.gz')
|
||||
helper.local(f'cd {sp_dir} && tar -zcf {tar_gz_file} {exclude} {contain}')
|
||||
helper.send_info('local', '打包完成\r\n')
|
||||
helper.set_deploy_success('local')
|
||||
human_time = human_seconds_time(time.time() - flag)
|
||||
helper.send_success('local', f'\r\n** 执行完成,耗时:{human_time} **', status='success')
|
||||
|
||||
if host_actions:
|
||||
if req.deploy.is_parallel:
|
||||
threads, latest_exception = [], None
|
||||
max_workers = max(10, os.cpu_count() * 5)
|
||||
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
for h_id in sorted(helper.deploy_host_ids, reverse=True):
|
||||
new_env = AttrDict(env.items())
|
||||
t = executor.submit(_deploy_ext2_host, helper, h_id, host_actions, new_env, req.spug_version)
|
||||
t.h_id = h_id
|
||||
threads.append(t)
|
||||
for t in futures.as_completed(threads):
|
||||
exception = t.exception()
|
||||
if exception:
|
||||
helper.set_deploy_fail(t.h_id)
|
||||
latest_exception = exception
|
||||
if not isinstance(exception, SpugError):
|
||||
helper.send_error(t.h_id, f'Exception: {exception}', with_break=False)
|
||||
else:
|
||||
helper.set_deploy_success(t.h_id)
|
||||
if latest_exception:
|
||||
raise latest_exception
|
||||
else:
|
||||
host_ids = sorted(helper.deploy_host_ids)
|
||||
while host_ids:
|
||||
h_id = host_ids.pop()
|
||||
new_env = AttrDict(env.items())
|
||||
try:
|
||||
_deploy_ext2_host(helper, h_id, host_actions, new_env, req.spug_version)
|
||||
helper.set_deploy_success(h_id)
|
||||
except Exception as e:
|
||||
helper.set_deploy_fail(h_id)
|
||||
if not isinstance(e, SpugError):
|
||||
helper.send_error(h_id, f'Exception: {e}', with_break=False)
|
||||
for h_id in host_ids:
|
||||
helper.set_deploy_fail(h_id)
|
||||
helper.send_clear(h_id)
|
||||
helper.send_error(h_id, '串行模式,终止发布', with_break=False)
|
||||
raise e
|
||||
|
||||
|
||||
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
||||
flag = time.time()
|
||||
helper.set_deploy_process(h_id)
|
||||
host = Host.objects.filter(pk=h_id).first()
|
||||
if not host:
|
||||
helper.send_error(h_id, 'no such host')
|
||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||
with host.get_ssh(default_env=env) as ssh:
|
||||
helper.send_clear(h_id)
|
||||
helper.save_pid(ssh.get_pid(), h_id)
|
||||
helper.send_success(h_id, '', status='doing')
|
||||
for index, action in enumerate(actions, start=1):
|
||||
if action.get('type') == 'transfer':
|
||||
helper.send_info(h_id, f'{action["title"]}...')
|
||||
if action.get('src_mode') == '1':
|
||||
try:
|
||||
dst = action['dst']
|
||||
command = f'[ -e {dst} ] || mkdir -p $(dirname {dst}); [ -d {dst} ]'
|
||||
code, _ = ssh.exec_command_raw(command)
|
||||
if code == 0: # is dir
|
||||
if not action.get('name'):
|
||||
raise RuntimeError('internal error 1002')
|
||||
dst = dst.rstrip('/') + '/' + action['name']
|
||||
callback = helper.progress_callback(host.id)
|
||||
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, spug_version), dst, callback)
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'\r\nException: {e}')
|
||||
helper.send_success(host.id, '完成√\r\n')
|
||||
else:
|
||||
_, sd_dst = os.path.split(action['src'])
|
||||
tar_gz_file = f'{spug_version}.tar.gz'
|
||||
src_file = os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, tar_gz_file)
|
||||
try:
|
||||
callback = helper.progress_callback(host.id)
|
||||
ssh.put_file(src_file, f'/tmp/{tar_gz_file}', callback)
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'\r\nException: {e}')
|
||||
helper.send_success(host.id, '完成√\r\n')
|
||||
command = f'mkdir -p /tmp/{spug_version} '
|
||||
command += f'&& tar xf /tmp/{tar_gz_file} -C /tmp/{spug_version}/ 2> /dev/null '
|
||||
command += f'&& rm -rf {action["dst"]} && mv /tmp/{spug_version}/{sd_dst} {action["dst"]} '
|
||||
command += f'&& rm -rf /tmp/{spug_version}*'
|
||||
helper.remote(host.id, ssh, command)
|
||||
else:
|
||||
helper.send_info(h_id, f'{action["title"]}...\r\n')
|
||||
command = f'cd /tmp && {action["data"]}'
|
||||
helper.remote(host.id, ssh, command)
|
||||
human_time = human_seconds_time(time.time() - flag)
|
||||
helper.send_success(h_id, f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
|
@ -231,6 +231,8 @@ class Helper(NotifyMixin, KitMixin):
|
|||
self.callback = []
|
||||
self.buffers = defaultdict(str)
|
||||
self.flags = defaultdict(bool)
|
||||
self.deploy_status = {}
|
||||
self.deploy_host_ids = []
|
||||
self.files = {}
|
||||
self.already_clear = False
|
||||
|
||||
|
@ -242,6 +244,9 @@ class Helper(NotifyMixin, KitMixin):
|
|||
rds.delete(rds_key)
|
||||
instance = cls(rds, rds_key)
|
||||
for key in keys:
|
||||
if key != 'local':
|
||||
instance.deploy_host_ids.append(key)
|
||||
instance.deploy_status[key] = '0'
|
||||
instance.get_file(key)
|
||||
return instance
|
||||
|
||||
|
@ -282,6 +287,15 @@ class Helper(NotifyMixin, KitMixin):
|
|||
line = f.readline()
|
||||
return counter
|
||||
|
||||
def set_deploy_process(self, key):
|
||||
self.deploy_status[key] = '1'
|
||||
|
||||
def set_deploy_success(self, key):
|
||||
self.deploy_status[key] = '2'
|
||||
|
||||
def set_deploy_fail(self, key):
|
||||
self.deploy_status[key] = '3'
|
||||
|
||||
def get_file(self, key):
|
||||
if key in self.files:
|
||||
return self.files[key]
|
||||
|
|
|
@ -19,6 +19,7 @@ class DeployRequest(models.Model, ModelMixin):
|
|||
('1', '待发布'),
|
||||
('2', '发布中'),
|
||||
('3', '发布成功'),
|
||||
('4', '灰度成功'),
|
||||
)
|
||||
TYPES = (
|
||||
('1', '正常发布'),
|
||||
|
@ -37,8 +38,7 @@ class DeployRequest(models.Model, ModelMixin):
|
|||
version = models.CharField(max_length=100, null=True)
|
||||
spug_version = models.CharField(max_length=50, null=True)
|
||||
plan = models.DateTimeField(null=True)
|
||||
fail_host_ids = models.TextField(default='[]')
|
||||
|
||||
deploy_status = models.TextField(default='{}')
|
||||
created_at = models.CharField(max_length=20, default=human_datetime)
|
||||
created_by = models.ForeignKey(User, models.PROTECT, related_name='+')
|
||||
approve_at = models.CharField(max_length=20, null=True)
|
||||
|
@ -67,6 +67,7 @@ class DeployRequest(models.Model, ModelMixin):
|
|||
os.remove(os.path.join(settings.REPOS_DIR, str(self.deploy_id), self.spug_version))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
#TODO: 清理日志文件, 删除自定义发布tar.gz文件
|
||||
|
||||
def __repr__(self):
|
||||
return f'<DeployRequest name={self.name}>'
|
||||
|
|
|
@ -4,33 +4,22 @@
|
|||
from django_redis import get_redis_connection
|
||||
from django.conf import settings
|
||||
from django.db import close_old_connections
|
||||
from libs.utils import AttrDict, render_str, human_seconds_time
|
||||
from apps.host.models import Host
|
||||
from libs.utils import AttrDict
|
||||
from apps.config.utils import compose_configs
|
||||
from apps.repository.models import Repository
|
||||
from apps.repository.utils import dispatch as build_repository
|
||||
from apps.deploy.models import DeployRequest
|
||||
from apps.deploy.helper import Helper, SpugError
|
||||
from concurrent import futures
|
||||
from functools import partial
|
||||
from apps.deploy.ext1 import ext1_deploy
|
||||
from apps.deploy.ext2 import ext2_deploy
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
import os
|
||||
|
||||
REPOS_DIR = settings.REPOS_DIR
|
||||
BUILD_DIR = settings.BUILD_DIR
|
||||
|
||||
|
||||
def dispatch(req, fail_mode=False):
|
||||
def dispatch(req, deploy_host_ids, with_local):
|
||||
rds = get_redis_connection()
|
||||
rds_key = req.deploy_key
|
||||
if fail_mode:
|
||||
req.host_ids = req.fail_host_ids
|
||||
req.fail_mode = fail_mode
|
||||
req.host_ids = json.loads(req.host_ids)
|
||||
req.fail_host_ids = req.host_ids[:]
|
||||
keys = req.host_ids if fail_mode else req.host_ids + ['local']
|
||||
keys = deploy_host_ids + ['local'] if with_local else deploy_host_ids
|
||||
helper = Helper.make(rds, rds_key, keys)
|
||||
|
||||
try:
|
||||
|
@ -57,9 +46,9 @@ def dispatch(req, fail_mode=False):
|
|||
env.update(configs_env)
|
||||
|
||||
if req.deploy.extend == '1':
|
||||
_ext1_deploy(req, helper, env)
|
||||
ext1_deploy(req, helper, env)
|
||||
else:
|
||||
_ext2_deploy(req, helper, env)
|
||||
ext2_deploy(req, helper, env, with_local)
|
||||
req.status = '3'
|
||||
except Exception as e:
|
||||
req.status = '-3'
|
||||
|
@ -67,283 +56,19 @@ def dispatch(req, fail_mode=False):
|
|||
raise e
|
||||
finally:
|
||||
close_old_connections()
|
||||
DeployRequest.objects.filter(pk=req.id).update(
|
||||
status=req.status,
|
||||
repository=req.repository,
|
||||
fail_host_ids=json.dumps(req.fail_host_ids),
|
||||
)
|
||||
request = DeployRequest.objects.get(pk=req.id)
|
||||
deploy_status = json.loads(request.deploy_status)
|
||||
deploy_status.update({str(k): v for k, v in helper.deploy_status.items()})
|
||||
values = [v for k, v in deploy_status.items() if k != 'local']
|
||||
if all([x == '2' for x in values]):
|
||||
if len(values) == len(json.loads(request.host_ids)):
|
||||
request.status = '3'
|
||||
else:
|
||||
request.status = '4'
|
||||
else:
|
||||
request.status = '-3'
|
||||
request.repository = req.repository
|
||||
request.deploy_status = json.dumps(deploy_status)
|
||||
request.save()
|
||||
helper.clear()
|
||||
Helper.send_deploy_notify(req)
|
||||
|
||||
|
||||
def _ext1_deploy(req, helper, env):
|
||||
if not req.repository_id:
|
||||
rep = Repository(
|
||||
app_id=req.deploy.app_id,
|
||||
env_id=req.deploy.env_id,
|
||||
deploy_id=req.deploy_id,
|
||||
version=req.version,
|
||||
spug_version=req.spug_version,
|
||||
extra=req.extra,
|
||||
remarks='SPUG AUTO MAKE',
|
||||
created_by_id=req.created_by_id
|
||||
)
|
||||
build_repository(rep, helper)
|
||||
req.repository = rep
|
||||
extras = json.loads(req.extra)
|
||||
if extras[0] == 'repository':
|
||||
extras = extras[1:]
|
||||
if extras[0] == 'branch':
|
||||
env.update(SPUG_GIT_BRANCH=extras[1], SPUG_GIT_COMMIT_ID=extras[2])
|
||||
else:
|
||||
env.update(SPUG_GIT_TAG=extras[1])
|
||||
if req.deploy.is_parallel:
|
||||
threads, latest_exception = [], None
|
||||
max_workers = max(10, os.cpu_count() * 5)
|
||||
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
for h_id in req.host_ids:
|
||||
new_env = AttrDict(env.items())
|
||||
t = executor.submit(_deploy_ext1_host, req, helper, h_id, new_env)
|
||||
t.h_id = h_id
|
||||
threads.append(t)
|
||||
for t in futures.as_completed(threads):
|
||||
exception = t.exception()
|
||||
if exception:
|
||||
latest_exception = exception
|
||||
if not isinstance(exception, SpugError):
|
||||
helper.send_error(t.h_id, f'Exception: {exception}', with_break=False)
|
||||
else:
|
||||
req.fail_host_ids.remove(t.h_id)
|
||||
if latest_exception:
|
||||
raise latest_exception
|
||||
else:
|
||||
host_ids = sorted(req.host_ids, reverse=True)
|
||||
while host_ids:
|
||||
h_id = host_ids.pop()
|
||||
new_env = AttrDict(env.items())
|
||||
try:
|
||||
_deploy_ext1_host(req, helper, h_id, new_env)
|
||||
req.fail_host_ids.remove(h_id)
|
||||
except Exception as e:
|
||||
helper.send_error(h_id, f'Exception: {e}', with_break=False)
|
||||
for h_id in host_ids:
|
||||
helper.send_error(h_id, '终止发布', with_break=False)
|
||||
raise e
|
||||
|
||||
|
||||
def _ext2_deploy(req, helper, env):
|
||||
flag = time.time()
|
||||
extend, step = req.deploy.extend_obj, 1
|
||||
host_actions = json.loads(extend.host_actions)
|
||||
server_actions = json.loads(extend.server_actions)
|
||||
env.update({'SPUG_RELEASE': req.version})
|
||||
if req.version:
|
||||
for index, value in enumerate(req.version.split()):
|
||||
env.update({f'SPUG_RELEASE_{index + 1}': value})
|
||||
|
||||
transfer_action = None
|
||||
for action in host_actions:
|
||||
if action.get('type') == 'transfer':
|
||||
action['src'] = render_str(action.get('src', '').strip().rstrip('/'), env)
|
||||
action['dst'] = render_str(action['dst'].strip().rstrip('/'), env)
|
||||
if action.get('src_mode') == '1': # upload when publish
|
||||
if not req.extra:
|
||||
helper.send_error('local', '\r\n未找到上传的文件信息,请尝试新建发布申请')
|
||||
extra = json.loads(req.extra)
|
||||
if 'name' in extra:
|
||||
action['name'] = extra['name']
|
||||
else:
|
||||
transfer_action = action
|
||||
break
|
||||
|
||||
if not req.fail_mode:
|
||||
helper.send_success('local', '', status='doing')
|
||||
if server_actions or transfer_action:
|
||||
helper.send_clear('local')
|
||||
for action in server_actions:
|
||||
helper.send_info('local', f'{action["title"]}...\r\n')
|
||||
helper.local(f'cd /tmp && {action["data"]}', env)
|
||||
step += 1
|
||||
if transfer_action:
|
||||
action = transfer_action
|
||||
helper.send_info('local', '检测到来源为本地路径的数据传输动作,执行打包... \r\n')
|
||||
action['src'] = action['src'].rstrip('/ ')
|
||||
action['dst'] = action['dst'].rstrip('/ ')
|
||||
if not action['src'] or not action['dst']:
|
||||
helper.send_error('local', f'Invalid path for transfer, src: {action["src"]} dst: {action["dst"]}')
|
||||
if not os.path.exists(action['src']):
|
||||
helper.send_error('local', f'No such file or directory: {action["src"]}')
|
||||
is_dir, exclude = os.path.isdir(action['src']), ''
|
||||
sp_dir, sd_dst = os.path.split(action['src'])
|
||||
contain = sd_dst
|
||||
if action['mode'] != '0' and is_dir:
|
||||
files = helper.parse_filter_rule(action['rule'], ',', env)
|
||||
if files:
|
||||
if action['mode'] == '1':
|
||||
contain = ' '.join(f'{sd_dst}/{x}' for x in files)
|
||||
else:
|
||||
excludes = []
|
||||
for x in files:
|
||||
if x.startswith('/'):
|
||||
excludes.append(f'--exclude={sd_dst}{x}')
|
||||
else:
|
||||
excludes.append(f'--exclude={x}')
|
||||
exclude = ' '.join(excludes)
|
||||
tar_gz_file = f'{req.spug_version}.tar.gz'
|
||||
helper.local(f'cd {sp_dir} && tar -zcf {tar_gz_file} {exclude} {contain}')
|
||||
helper.send_info('local', '打包完成\r\n')
|
||||
helper.add_callback(partial(os.remove, os.path.join(sp_dir, tar_gz_file)))
|
||||
|
||||
human_time = human_seconds_time(time.time() - flag)
|
||||
if host_actions:
|
||||
helper.send_success('local', f'\r\n** 执行完成,耗时:{human_time} **', status='success')
|
||||
if req.deploy.is_parallel:
|
||||
threads, latest_exception = [], None
|
||||
max_workers = max(10, os.cpu_count() * 5)
|
||||
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
for h_id in sorted(req.host_ids, reverse=True):
|
||||
new_env = AttrDict(env.items())
|
||||
t = executor.submit(_deploy_ext2_host, helper, h_id, host_actions, new_env, req.spug_version)
|
||||
t.h_id = h_id
|
||||
threads.append(t)
|
||||
for t in futures.as_completed(threads):
|
||||
exception = t.exception()
|
||||
if exception:
|
||||
latest_exception = exception
|
||||
if not isinstance(exception, SpugError):
|
||||
helper.send_error(t.h_id, f'Exception: {exception}', with_break=False)
|
||||
else:
|
||||
req.fail_host_ids.remove(t.h_id)
|
||||
if latest_exception:
|
||||
raise latest_exception
|
||||
else:
|
||||
host_ids = sorted(req.host_ids)
|
||||
while host_ids:
|
||||
h_id = host_ids.pop()
|
||||
new_env = AttrDict(env.items())
|
||||
try:
|
||||
_deploy_ext2_host(helper, h_id, host_actions, new_env, req.spug_version)
|
||||
req.fail_host_ids.remove(h_id)
|
||||
except Exception as e:
|
||||
if not isinstance(e, SpugError):
|
||||
helper.send_error(h_id, f'Exception: {e}', with_break=False)
|
||||
for h_id in host_ids:
|
||||
helper.send_clear(h_id)
|
||||
helper.send_error(h_id, '串行模式,终止发布', with_break=False)
|
||||
raise e
|
||||
else:
|
||||
req.fail_host_ids = []
|
||||
helper.send_success('local', f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
||||
|
||||
|
||||
def _deploy_ext1_host(req, helper, h_id, env):
|
||||
flag = time.time()
|
||||
helper.send_clear(h_id)
|
||||
helper.send_info(h_id, '数据准备... ', status='doing')
|
||||
host = Host.objects.filter(pk=h_id).first()
|
||||
if not host:
|
||||
helper.send_error(h_id, 'no such host')
|
||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||
extend = req.deploy.extend_obj
|
||||
extend.dst_dir = render_str(extend.dst_dir, env)
|
||||
extend.dst_repo = render_str(extend.dst_repo, env)
|
||||
env.update(SPUG_DST_DIR=extend.dst_dir)
|
||||
with host.get_ssh(default_env=env) as ssh:
|
||||
helper.save_pid(ssh.get_pid(), h_id)
|
||||
base_dst_dir = os.path.dirname(extend.dst_dir)
|
||||
code, _ = ssh.exec_command_raw(
|
||||
f'mkdir -p {extend.dst_repo} {base_dst_dir} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
|
||||
if code == 0:
|
||||
helper.send_error(host.id,
|
||||
f'\r\n检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。')
|
||||
if req.type == '2':
|
||||
helper.send_warn(h_id, '跳过√\r\n')
|
||||
else:
|
||||
# clean
|
||||
clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf'
|
||||
helper.remote_raw(host.id, ssh, f'cd {extend.dst_repo} && {clean_command}')
|
||||
# transfer files
|
||||
tar_gz_file = f'{req.spug_version}.tar.gz'
|
||||
try:
|
||||
callback = helper.progress_callback(host.id)
|
||||
ssh.put_file(
|
||||
os.path.join(BUILD_DIR, tar_gz_file),
|
||||
os.path.join(extend.dst_repo, tar_gz_file),
|
||||
callback
|
||||
)
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'Exception: {e}')
|
||||
|
||||
command = f'cd {extend.dst_repo} && rm -rf {req.spug_version} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
|
||||
helper.remote_raw(host.id, ssh, command)
|
||||
helper.send_success(h_id, '完成√\r\n')
|
||||
|
||||
# pre host
|
||||
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
|
||||
if extend.hook_pre_host:
|
||||
helper.send_info(h_id, '发布前任务... \r\n')
|
||||
command = f'cd {repo_dir} && {extend.hook_pre_host}'
|
||||
helper.remote(host.id, ssh, command)
|
||||
|
||||
# do deploy
|
||||
helper.send_info(h_id, '执行发布... ')
|
||||
helper.remote_raw(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
|
||||
helper.send_success(h_id, '完成√\r\n')
|
||||
|
||||
# post host
|
||||
if extend.hook_post_host:
|
||||
helper.send_info(h_id, '发布后任务... \r\n')
|
||||
command = f'cd {extend.dst_dir} && {extend.hook_post_host}'
|
||||
helper.remote(host.id, ssh, command)
|
||||
|
||||
human_time = human_seconds_time(time.time() - flag)
|
||||
helper.send_success(h_id, f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
||||
|
||||
|
||||
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
||||
flag = time.time()
|
||||
host = Host.objects.filter(pk=h_id).first()
|
||||
if not host:
|
||||
helper.send_error(h_id, 'no such host')
|
||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||
with host.get_ssh(default_env=env) as ssh:
|
||||
helper.send_clear(h_id)
|
||||
helper.save_pid(ssh.get_pid(), h_id)
|
||||
helper.send_success(h_id, '', status='doing')
|
||||
for index, action in enumerate(actions, start=1):
|
||||
if action.get('type') == 'transfer':
|
||||
helper.send_info(h_id, f'{action["title"]}...')
|
||||
if action.get('src_mode') == '1':
|
||||
try:
|
||||
dst = action['dst']
|
||||
command = f'[ -e {dst} ] || mkdir -p $(dirname {dst}); [ -d {dst} ]'
|
||||
code, _ = ssh.exec_command_raw(command)
|
||||
if code == 0: # is dir
|
||||
if not action.get('name'):
|
||||
raise RuntimeError('internal error 1002')
|
||||
dst = dst.rstrip('/') + '/' + action['name']
|
||||
callback = helper.progress_callback(host.id)
|
||||
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, spug_version), dst, callback)
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'\r\nException: {e}')
|
||||
helper.send_success(host.id, '完成√\r\n')
|
||||
continue
|
||||
else:
|
||||
sp_dir, sd_dst = os.path.split(action['src'])
|
||||
tar_gz_file = f'{spug_version}.tar.gz'
|
||||
try:
|
||||
callback = helper.progress_callback(host.id)
|
||||
ssh.put_file(os.path.join(sp_dir, tar_gz_file), f'/tmp/{tar_gz_file}', callback)
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'\r\nException: {e}')
|
||||
|
||||
command = f'mkdir -p /tmp/{spug_version} '
|
||||
command += f'&& tar xf /tmp/{tar_gz_file} -C /tmp/{spug_version}/ 2> /dev/null '
|
||||
command += f'&& rm -rf {action["dst"]} && mv /tmp/{spug_version}/{sd_dst} {action["dst"]} '
|
||||
command += f'&& rm -rf /tmp/{spug_version}* && echo "\033[32m完成√\033[0m"'
|
||||
else:
|
||||
helper.send_info(h_id, f'{action["title"]}...\r\n')
|
||||
command = f'cd /tmp && {action["data"]}'
|
||||
helper.remote(host.id, ssh, command)
|
||||
human_time = human_seconds_time(time.time() - flag)
|
||||
helper.send_success(h_id, f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
||||
|
|
|
@ -5,7 +5,6 @@ from django.views.generic import View
|
|||
from django.db.models import F
|
||||
from django.conf import settings
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django_redis import get_redis_connection
|
||||
from libs import json_response, JsonParser, Argument, human_datetime, human_time, auth, AttrDict
|
||||
from apps.deploy.models import DeployRequest
|
||||
from apps.app.models import Deploy, DeployExtend2
|
||||
|
@ -46,7 +45,7 @@ class RequestView(View):
|
|||
tmp['app_name'] = item.app_name
|
||||
tmp['app_extend'] = item.app_extend
|
||||
tmp['host_ids'] = json.loads(item.host_ids)
|
||||
tmp['fail_host_ids'] = json.loads(item.fail_host_ids)
|
||||
tmp['deploy_status'] = json.loads(item.deploy_status)
|
||||
tmp['extra'] = json.loads(item.extra) if item.extra else None
|
||||
tmp['rep_extra'] = json.loads(item.rep_extra) if item.rep_extra else None
|
||||
tmp['app_host_ids'] = json.loads(item.app_host_ids)
|
||||
|
@ -125,49 +124,72 @@ class RequestDetailView(View):
|
|||
|
||||
@auth('deploy.request.do')
|
||||
def post(self, request, r_id):
|
||||
form, _ = JsonParser(Argument('mode', default='all')).parse(request.body)
|
||||
query, is_fail_mode = {'pk': r_id}, form.mode == 'fail'
|
||||
if not request.user.is_supper:
|
||||
perms = request.user.deploy_perms
|
||||
query['deploy__app_id__in'] = perms['apps']
|
||||
query['deploy__env_id__in'] = perms['envs']
|
||||
req = DeployRequest.objects.filter(**query).first()
|
||||
if not req:
|
||||
return json_response(error='未找到指定发布申请')
|
||||
if req.status not in ('1', '-3'):
|
||||
return json_response(error='该申请单当前状态还不能执行发布')
|
||||
host_ids = req.fail_host_ids if is_fail_mode else req.host_ids
|
||||
form, error = JsonParser(
|
||||
Argument('mode', filter=lambda x: x in ('fail', 'gray', 'all'), help='参数错误'),
|
||||
Argument('host_ids', type=list, required=False)
|
||||
).parse(request.body)
|
||||
if error is None:
|
||||
query, is_fail_mode = {'pk': r_id}, form.mode == 'fail'
|
||||
if not request.user.is_supper:
|
||||
perms = request.user.deploy_perms
|
||||
query['deploy__app_id__in'] = perms['apps']
|
||||
query['deploy__env_id__in'] = perms['envs']
|
||||
req = DeployRequest.objects.filter(**query).first()
|
||||
if not req:
|
||||
return json_response(error='未找到指定发布申请')
|
||||
if req.status not in ('1', '-3', '4'):
|
||||
return json_response(error='该申请单当前状态还不能执行发布')
|
||||
|
||||
req.status = '2'
|
||||
req.do_at = human_datetime()
|
||||
req.do_by = request.user
|
||||
req.save()
|
||||
Thread(target=dispatch, args=(req, is_fail_mode)).start()
|
||||
|
||||
hosts = Host.objects.filter(id__in=json.loads(host_ids))
|
||||
message = Helper.term_message('等待调度... ')
|
||||
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': message} for x in hosts}
|
||||
if req.is_quick_deploy:
|
||||
if req.repository_id:
|
||||
outputs['local'] = {
|
||||
'id': 'local',
|
||||
'status': 'success',
|
||||
'data': Helper.term_message('已构建完成忽略执行', 'warn')
|
||||
}
|
||||
deploy_status = json.loads(req.deploy_status)
|
||||
if form.mode == 'gray':
|
||||
if not form.host_ids:
|
||||
return json_response(error='请选择灰度发布的主机')
|
||||
host_ids = form.host_ids
|
||||
elif form.mode == 'fail':
|
||||
host_ids = [int(k) for k, v in deploy_status.items() if v != '2' and k != 'local']
|
||||
else:
|
||||
outputs['local'] = {'id': 'local', 'data': Helper.term_message('等待初始化... ')}
|
||||
if req.deploy.extend == '2':
|
||||
message = Helper.term_message('等待初始化... ')
|
||||
if is_fail_mode:
|
||||
message = Helper.term_message('已完成本地动作忽略执行', 'warn')
|
||||
outputs['local'] = {'id': 'local', 'data': message}
|
||||
s_actions = json.loads(req.deploy.extend_obj.server_actions)
|
||||
h_actions = json.loads(req.deploy.extend_obj.host_actions)
|
||||
if not s_actions:
|
||||
outputs.pop('local')
|
||||
if not h_actions:
|
||||
outputs = {'local': outputs['local']}
|
||||
return json_response({'outputs': outputs, 'token': req.deploy_key})
|
||||
host_ids = json.loads(req.host_ids)
|
||||
|
||||
with_local = False
|
||||
hosts = Host.objects.filter(id__in=host_ids)
|
||||
message = Helper.term_message('等待调度... ')
|
||||
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': message} for x in hosts}
|
||||
if req.deploy.extend == '1':
|
||||
if req.repository_id:
|
||||
if req.is_quick_deploy:
|
||||
outputs['local'] = {
|
||||
'id': 'local',
|
||||
'status': 'success',
|
||||
'data': Helper.term_message('已构建完成忽略执行', 'warn')
|
||||
}
|
||||
else:
|
||||
with_local = True
|
||||
outputs['local'] = {'id': 'local', 'data': Helper.term_message('等待初始化... ')}
|
||||
elif req.deploy.extend == '2':
|
||||
s_actions = json.loads(req.deploy.extend_obj.server_actions)
|
||||
h_actions = json.loads(req.deploy.extend_obj.host_actions)
|
||||
if s_actions:
|
||||
if deploy_status.get('local') == '2':
|
||||
outputs['local'] = {
|
||||
'id': 'local',
|
||||
'status': 'success',
|
||||
'data': Helper.term_message('已完成本地动作忽略执行', 'warn')
|
||||
}
|
||||
else:
|
||||
with_local = True
|
||||
outputs['local'] = {'id': 'local', 'data': Helper.term_message('等待初始化... ')}
|
||||
if not h_actions:
|
||||
outputs = {'local': outputs['local']}
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
req.status = '2'
|
||||
req.do_at = human_datetime()
|
||||
req.do_by = request.user
|
||||
req.save()
|
||||
Thread(target=dispatch, args=(req, host_ids, with_local)).start()
|
||||
return json_response({'outputs': outputs, 'token': req.deploy_key})
|
||||
return json_response(error=error)
|
||||
|
||||
@auth('deploy.request.approve')
|
||||
def patch(self, request, r_id):
|
||||
|
@ -323,7 +345,7 @@ def get_request_info(request):
|
|||
if error is None:
|
||||
req = DeployRequest.objects.get(pk=form.id)
|
||||
response = req.to_dict(selects=('status', 'reason'))
|
||||
response['fail_host_ids'] = json.loads(req.fail_host_ids)
|
||||
response['deploy_status'] = json.loads(req.deploy_status)
|
||||
response['status_alias'] = req.get_status_display()
|
||||
return json_response(response)
|
||||
return json_response(error=error)
|
||||
|
|
|
@ -26,6 +26,7 @@ def dispatch(rep: Repository, helper=None):
|
|||
helper = Helper.make(rds, rep.deploy_key, ['local'])
|
||||
rep.save()
|
||||
try:
|
||||
helper.set_deploy_process('local')
|
||||
api_token = uuid.uuid4().hex
|
||||
helper.rds.setex(api_token, 60 * 60, f'{rep.app_id},{rep.env_id}')
|
||||
env = AttrDict(
|
||||
|
@ -63,8 +64,10 @@ def dispatch(rep: Repository, helper=None):
|
|||
|
||||
_build(rep, helper, env)
|
||||
rep.status = '5'
|
||||
helper.set_deploy_success('local')
|
||||
except Exception as e:
|
||||
rep.status = '2'
|
||||
helper.set_deploy_fail('local')
|
||||
raise e
|
||||
finally:
|
||||
helper.local(f'cd {REPOS_DIR} && rm -rf {rep.spug_version}')
|
||||
|
|
|
@ -78,7 +78,12 @@ function Console(props) {
|
|||
|
||||
function doDeploy() {
|
||||
let socket;
|
||||
http.post(`/api/deploy/request/${props.request.id}/`, {mode: props.request.mode})
|
||||
const formData = {mode: props.request.mode}
|
||||
if (Array.isArray(props.request.mode)) {
|
||||
formData.mode = 'gray'
|
||||
formData.host_ids = props.request.mode
|
||||
}
|
||||
http.post(`/api/deploy/request/${props.request.id}/`, formData)
|
||||
.then(res => {
|
||||
_handleResponse(res)
|
||||
socket = _makeSocket()
|
||||
|
|
|
@ -249,6 +249,7 @@ export default observer(function () {
|
|||
)}
|
||||
</Form>
|
||||
{visible && <HostSelector
|
||||
title="可选主机列表"
|
||||
host_ids={host_ids}
|
||||
app_host_ids={app_host_ids}
|
||||
onCancel={() => setVisible(false)}
|
||||
|
|
|
@ -120,6 +120,7 @@ export default observer(function () {
|
|||
)}
|
||||
</Form>
|
||||
{visible && <HostSelector
|
||||
title="可选主机列表"
|
||||
host_ids={host_ids}
|
||||
app_host_ids={app_host_ids}
|
||||
onCancel={() => setVisible(false)}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Modal, Table, Button, Alert } from 'antd';
|
||||
import { Modal, Table, Button, Alert, Tag } from 'antd';
|
||||
import hostStore from 'pages/host/store';
|
||||
|
||||
export default observer(function (props) {
|
||||
|
@ -31,19 +31,34 @@ export default observer(function (props) {
|
|||
}
|
||||
}
|
||||
|
||||
function DeployStatus(props) {
|
||||
switch (props.status) {
|
||||
case '0':
|
||||
return <Tag color="blue">待调度</Tag>
|
||||
case '1':
|
||||
return <Tag color="orange">发布中</Tag>
|
||||
case '2':
|
||||
return <Tag color="green">发布成功</Tag>
|
||||
case '3':
|
||||
return <Tag color="red">发布失败</Tag>
|
||||
default:
|
||||
return <Tag color="blue">待发布</Tag>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible
|
||||
width={600}
|
||||
title='可选主机列表'
|
||||
width={800}
|
||||
title={props.title}
|
||||
onOk={handleSubmit}
|
||||
okButtonProps={{disabled: selectedRowKeys.length === 0}}
|
||||
onCancel={props.onCancel}>
|
||||
{selectedRowKeys.length > 0 && (
|
||||
<Alert
|
||||
style={{marginBottom: 12}}
|
||||
message={`已选择 ${selectedRowKeys.length} 台主机`}
|
||||
action={<Button type="link" onClick={() => setSelectedRowKeys([])}>取消选择</Button>}/>
|
||||
)}
|
||||
<Alert
|
||||
style={{marginBottom: 12}}
|
||||
message={<span>已选择 <b style={{color: '#2563fc', fontSize: 18}}>{selectedRowKeys.length}</b> 台主机</span>}
|
||||
action={<Button type="link" disabled={selectedRowKeys.length === 0}
|
||||
onClick={() => setSelectedRowKeys([])}>取消选择</Button>}/>
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={hostStore.records.filter(x => props.app_host_ids.includes(x.id))}
|
||||
|
@ -61,6 +76,10 @@ export default observer(function (props) {
|
|||
}}>
|
||||
<Table.Column title="主机名称" dataIndex="name"/>
|
||||
<Table.Column title="连接地址" dataIndex="hostname"/>
|
||||
<Table.Column title="备注信息" dataIndex="desc"/>
|
||||
{props.deploy_status ? (
|
||||
<Table.Column title="发布状态" render={v => <DeployStatus status={props.deploy_status[v.id]}/>}/>
|
||||
) : null}
|
||||
</Table>
|
||||
</Modal>
|
||||
)
|
||||
|
|
|
@ -39,7 +39,7 @@ export default observer(function () {
|
|||
}, () => setLoading(false))
|
||||
}
|
||||
|
||||
const {app_host_ids, deploy_id} = store.record;
|
||||
const {app_host_ids, deploy_id, deploy_status} = store.record;
|
||||
return (
|
||||
<Modal
|
||||
visible
|
||||
|
@ -79,8 +79,10 @@ export default observer(function () {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
{visible && <HostSelector
|
||||
title="选择回滚发布的主机"
|
||||
host_ids={host_ids}
|
||||
app_host_ids={app_host_ids}
|
||||
deploy_status={deploy_status}
|
||||
onCancel={() => setVisible(false)}
|
||||
onOk={ids => setHostIds(ids)}/>}
|
||||
</Modal>
|
||||
|
|
|
@ -3,27 +3,21 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { BranchesOutlined, BuildOutlined, TagOutlined, PlusOutlined, TagsOutlined } from '@ant-design/icons';
|
||||
import { Radio, Modal, Popover, Tag, Popconfirm, Tooltip, message } from 'antd';
|
||||
import { Radio, Modal, Popover, Tag, Tooltip, Button, Space, message } from 'antd';
|
||||
import { http, hasPermission } from 'libs';
|
||||
import { Action, AuthButton, TableCard } from 'components';
|
||||
import HostSelector from './HostSelector';
|
||||
import S from './index.module.less';
|
||||
import store from './store';
|
||||
import moment from 'moment';
|
||||
|
||||
function DeployConfirm() {
|
||||
return (
|
||||
<div>
|
||||
<div>确认发布方式</div>
|
||||
<div style={{color: '#999', fontSize: 12}}>补偿:仅发布上次发布失败的主机。</div>
|
||||
<div style={{color: '#999', fontSize: 12}}>全量:再次发布所有主机。</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import lds from 'lodash';
|
||||
|
||||
function ComTable() {
|
||||
const [request, setRequest] = useState()
|
||||
|
||||
const columns = [{
|
||||
title: '申请标题',
|
||||
className: S.min180,
|
||||
|
@ -144,6 +138,14 @@ function ComTable() {
|
|||
<Action.Button auth="deploy.request.do" onClick={() => store.rollback(info)}>回滚</Action.Button>
|
||||
)}
|
||||
</Action>;
|
||||
case '4':
|
||||
return <Action>
|
||||
<Action.Button auth="deploy.request.do" onClick={() => store.readConsole(info)}>查看</Action.Button>
|
||||
<DoAction info={info}/>
|
||||
{info.visible_rollback && (
|
||||
<Action.Button auth="deploy.request.do" onClick={() => store.rollback(info)}>回滚</Action.Button>
|
||||
)}
|
||||
</Action>;
|
||||
case '-1':
|
||||
return <Action>
|
||||
<Action.Button auth="deploy.request.edit" onClick={() => store.showForm(info)}>编辑</Action.Button>
|
||||
|
@ -171,17 +173,23 @@ function ComTable() {
|
|||
}];
|
||||
|
||||
function DoAction(props) {
|
||||
const {host_ids, fail_host_ids} = props.info;
|
||||
const {deploy_status} = props.info;
|
||||
return (
|
||||
<Popconfirm
|
||||
title={<DeployConfirm/>}
|
||||
okText="全量"
|
||||
cancelText="补偿"
|
||||
cancelButtonProps={{disabled: [0, host_ids.length].includes(fail_host_ids.length)}}
|
||||
onConfirm={e => handleDeploy(e, props.info, 'all')}
|
||||
onCancel={e => handleDeploy(e, props.info, 'fail')}>
|
||||
<Popover trigger="click" zIndex={2} title="确认发布方式" content={(
|
||||
<div>
|
||||
<div style={{color: '#999', fontSize: 12}}>全量:发布所有主机(包含已成功的)。</div>
|
||||
<div style={{color: '#999', fontSize: 12}}>补偿:仅发布上次发布失败的主机。</div>
|
||||
<div style={{color: '#999', fontSize: 12}}>灰度:选择指定主机发布。</div>
|
||||
<Space style={{width: '100%', justifyContent: 'flex-end', marginTop: 16}}>
|
||||
<Button size="small" disabled={!lds.findKey(deploy_status, x => x !== '2')}
|
||||
onClick={() => handleDeploy(props.info, 'fail')}>补偿</Button>
|
||||
<Button ghost size="small" type="primary" onClick={() => setRequest(props.info)}>灰度</Button>
|
||||
<Button size="small" type="primary" onClick={() => handleDeploy(props.info, 'all')}>全量</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}>
|
||||
<Action.Button auth="deploy.request.do">发布</Action.Button>
|
||||
</Popconfirm>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -199,43 +207,58 @@ function ComTable() {
|
|||
})
|
||||
}
|
||||
|
||||
function handleDeploy(e, info, mode) {
|
||||
function handleDeploy(info, mode) {
|
||||
if (request && mode.length === 0) {
|
||||
return message.error('请选择灰度发布的主机')
|
||||
}
|
||||
info.mode = mode
|
||||
store.showConsole(info);
|
||||
store.showConsole(info)
|
||||
if (request) setRequest()
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCard
|
||||
tKey="dr"
|
||||
rowKey={row => row.key || row.id}
|
||||
title="申请列表"
|
||||
columns={columns}
|
||||
scroll={{x: 1500}}
|
||||
tableLayout="auto"
|
||||
loading={store.isFetching}
|
||||
dataSource={store.dataSource}
|
||||
onReload={store.fetchRecords}
|
||||
actions={[
|
||||
<AuthButton
|
||||
auth="deploy.request.add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined/>}
|
||||
onClick={() => store.addVisible = true}>新建申请</AuthButton>,
|
||||
<Radio.Group value={store.f_status} onChange={e => store.f_status = e.target.value}>
|
||||
<Radio.Button value="all">全部({store.counter['all'] || 0})</Radio.Button>
|
||||
<Radio.Button value="0">待审核({store.counter['0'] || 0})</Radio.Button>
|
||||
<Radio.Button value="1">待发布({store.counter['1'] || 0})</Radio.Button>
|
||||
<Radio.Button value="3">发布成功({store.counter['3'] || 0})</Radio.Button>
|
||||
<Radio.Button value="-3">发布异常({store.counter['-3'] || 0})</Radio.Button>
|
||||
<Radio.Button value="99">其他({store.counter['99'] || 0})</Radio.Button>
|
||||
</Radio.Group>
|
||||
]}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showLessItems: true,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
pageSizeOptions: ['10', '20', '50', '100']
|
||||
}}/>
|
||||
<React.Fragment>
|
||||
<TableCard
|
||||
tKey="dr"
|
||||
rowKey={row => row.key || row.id}
|
||||
title="申请列表"
|
||||
columns={columns}
|
||||
scroll={{x: 1500}}
|
||||
tableLayout="auto"
|
||||
loading={store.isFetching}
|
||||
dataSource={store.dataSource}
|
||||
onReload={store.fetchRecords}
|
||||
actions={[
|
||||
<AuthButton
|
||||
auth="deploy.request.add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined/>}
|
||||
onClick={() => store.addVisible = true}>新建申请</AuthButton>,
|
||||
<Radio.Group value={store.f_status} onChange={e => store.f_status = e.target.value}>
|
||||
<Radio.Button value="all">全部({store.counter['all'] || 0})</Radio.Button>
|
||||
<Radio.Button value="0">待审核({store.counter['0'] || 0})</Radio.Button>
|
||||
<Radio.Button value="1">待发布({store.counter['1'] || 0})</Radio.Button>
|
||||
<Radio.Button value="3">发布成功({store.counter['3'] || 0})</Radio.Button>
|
||||
<Radio.Button value="-3">发布异常({store.counter['-3'] || 0})</Radio.Button>
|
||||
<Radio.Button value="99">其他({store.counter['99'] || 0})</Radio.Button>
|
||||
</Radio.Group>
|
||||
]}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showLessItems: true,
|
||||
showTotal: total => `共 ${total} 条`,
|
||||
pageSizeOptions: ['10', '20', '50', '100']
|
||||
}}/>
|
||||
|
||||
{request ? (
|
||||
<HostSelector
|
||||
title="选择灰度发布的主机"
|
||||
app_host_ids={request.host_ids}
|
||||
onCancel={() => setRequest()}
|
||||
deploy_status={request.deploy_status}
|
||||
onOk={ids => handleDeploy(request, ids)}/>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ class Store {
|
|||
};
|
||||
|
||||
rollback = (info) => {
|
||||
this.record = lds.pick(info, ['deploy_id', 'host_ids']);
|
||||
this.record = lds.pick(info, ['deploy_id', 'host_ids', 'deploy_status']);
|
||||
this.record.app_host_ids = info.host_ids;
|
||||
this.record.name = `${info.name} - 回滚`;
|
||||
this.rollbackVisible = true
|
||||
|
|
Loading…
Reference in New Issue