A 添加灰度发布功能

4.0
vapao 2022-10-23 23:50:43 +08:00
parent 6709feffeb
commit 9889c95a42
14 changed files with 532 additions and 408 deletions

View File

@ -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')

View File

@ -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')

View File

@ -231,6 +231,8 @@ class Helper(NotifyMixin, KitMixin):
self.callback = [] self.callback = []
self.buffers = defaultdict(str) self.buffers = defaultdict(str)
self.flags = defaultdict(bool) self.flags = defaultdict(bool)
self.deploy_status = {}
self.deploy_host_ids = []
self.files = {} self.files = {}
self.already_clear = False self.already_clear = False
@ -242,6 +244,9 @@ class Helper(NotifyMixin, KitMixin):
rds.delete(rds_key) rds.delete(rds_key)
instance = cls(rds, rds_key) instance = cls(rds, rds_key)
for key in keys: for key in keys:
if key != 'local':
instance.deploy_host_ids.append(key)
instance.deploy_status[key] = '0'
instance.get_file(key) instance.get_file(key)
return instance return instance
@ -282,6 +287,15 @@ class Helper(NotifyMixin, KitMixin):
line = f.readline() line = f.readline()
return counter 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): def get_file(self, key):
if key in self.files: if key in self.files:
return self.files[key] return self.files[key]

View File

@ -19,6 +19,7 @@ class DeployRequest(models.Model, ModelMixin):
('1', '待发布'), ('1', '待发布'),
('2', '发布中'), ('2', '发布中'),
('3', '发布成功'), ('3', '发布成功'),
('4', '灰度成功'),
) )
TYPES = ( TYPES = (
('1', '正常发布'), ('1', '正常发布'),
@ -37,8 +38,7 @@ class DeployRequest(models.Model, ModelMixin):
version = models.CharField(max_length=100, null=True) version = models.CharField(max_length=100, null=True)
spug_version = models.CharField(max_length=50, null=True) spug_version = models.CharField(max_length=50, null=True)
plan = models.DateTimeField(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_at = models.CharField(max_length=20, default=human_datetime)
created_by = models.ForeignKey(User, models.PROTECT, related_name='+') created_by = models.ForeignKey(User, models.PROTECT, related_name='+')
approve_at = models.CharField(max_length=20, null=True) 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)) os.remove(os.path.join(settings.REPOS_DIR, str(self.deploy_id), self.spug_version))
except FileNotFoundError: except FileNotFoundError:
pass pass
#TODO: 清理日志文件, 删除自定义发布tar.gz文件
def __repr__(self): def __repr__(self):
return f'<DeployRequest name={self.name}>' return f'<DeployRequest name={self.name}>'

View File

@ -4,33 +4,22 @@
from django_redis import get_redis_connection from django_redis import get_redis_connection
from django.conf import settings from django.conf import settings
from django.db import close_old_connections from django.db import close_old_connections
from libs.utils import AttrDict, render_str, human_seconds_time from libs.utils import AttrDict
from apps.host.models import Host
from apps.config.utils import compose_configs 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.models import DeployRequest
from apps.deploy.helper import Helper, SpugError from apps.deploy.helper import Helper, SpugError
from concurrent import futures from apps.deploy.ext1 import ext1_deploy
from functools import partial from apps.deploy.ext2 import ext2_deploy
import json import json
import uuid import uuid
import time
import os
REPOS_DIR = settings.REPOS_DIR 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 = get_redis_connection()
rds_key = req.deploy_key rds_key = req.deploy_key
if fail_mode: keys = deploy_host_ids + ['local'] if with_local else deploy_host_ids
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']
helper = Helper.make(rds, rds_key, keys) helper = Helper.make(rds, rds_key, keys)
try: try:
@ -57,9 +46,9 @@ def dispatch(req, fail_mode=False):
env.update(configs_env) env.update(configs_env)
if req.deploy.extend == '1': if req.deploy.extend == '1':
_ext1_deploy(req, helper, env) ext1_deploy(req, helper, env)
else: else:
_ext2_deploy(req, helper, env) ext2_deploy(req, helper, env, with_local)
req.status = '3' req.status = '3'
except Exception as e: except Exception as e:
req.status = '-3' req.status = '-3'
@ -67,283 +56,19 @@ def dispatch(req, fail_mode=False):
raise e raise e
finally: finally:
close_old_connections() close_old_connections()
DeployRequest.objects.filter(pk=req.id).update( request = DeployRequest.objects.get(pk=req.id)
status=req.status, deploy_status = json.loads(request.deploy_status)
repository=req.repository, deploy_status.update({str(k): v for k, v in helper.deploy_status.items()})
fail_host_ids=json.dumps(req.fail_host_ids), 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.clear()
Helper.send_deploy_notify(req) 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')

View File

@ -5,7 +5,6 @@ from django.views.generic import View
from django.db.models import F from django.db.models import F
from django.conf import settings from django.conf import settings
from django.http.response import HttpResponseBadRequest 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 libs import json_response, JsonParser, Argument, human_datetime, human_time, auth, AttrDict
from apps.deploy.models import DeployRequest from apps.deploy.models import DeployRequest
from apps.app.models import Deploy, DeployExtend2 from apps.app.models import Deploy, DeployExtend2
@ -46,7 +45,7 @@ class RequestView(View):
tmp['app_name'] = item.app_name tmp['app_name'] = item.app_name
tmp['app_extend'] = item.app_extend tmp['app_extend'] = item.app_extend
tmp['host_ids'] = json.loads(item.host_ids) 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['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['rep_extra'] = json.loads(item.rep_extra) if item.rep_extra else None
tmp['app_host_ids'] = json.loads(item.app_host_ids) tmp['app_host_ids'] = json.loads(item.app_host_ids)
@ -125,49 +124,72 @@ class RequestDetailView(View):
@auth('deploy.request.do') @auth('deploy.request.do')
def post(self, request, r_id): def post(self, request, r_id):
form, _ = JsonParser(Argument('mode', default='all')).parse(request.body) form, error = JsonParser(
query, is_fail_mode = {'pk': r_id}, form.mode == 'fail' Argument('mode', filter=lambda x: x in ('fail', 'gray', 'all'), help='参数错误'),
if not request.user.is_supper: Argument('host_ids', type=list, required=False)
perms = request.user.deploy_perms ).parse(request.body)
query['deploy__app_id__in'] = perms['apps'] if error is None:
query['deploy__env_id__in'] = perms['envs'] query, is_fail_mode = {'pk': r_id}, form.mode == 'fail'
req = DeployRequest.objects.filter(**query).first() if not request.user.is_supper:
if not req: perms = request.user.deploy_perms
return json_response(error='未找到指定发布申请') query['deploy__app_id__in'] = perms['apps']
if req.status not in ('1', '-3'): query['deploy__env_id__in'] = perms['envs']
return json_response(error='该申请单当前状态还不能执行发布') req = DeployRequest.objects.filter(**query).first()
host_ids = req.fail_host_ids if is_fail_mode else req.host_ids if not req:
return json_response(error='未找到指定发布申请')
if req.status not in ('1', '-3', '4'):
return json_response(error='该申请单当前状态还不能执行发布')
req.status = '2' deploy_status = json.loads(req.deploy_status)
req.do_at = human_datetime() if form.mode == 'gray':
req.do_by = request.user if not form.host_ids:
req.save() return json_response(error='请选择灰度发布的主机')
Thread(target=dispatch, args=(req, is_fail_mode)).start() host_ids = form.host_ids
elif form.mode == 'fail':
hosts = Host.objects.filter(id__in=json.loads(host_ids)) host_ids = [int(k) for k, v in deploy_status.items() if v != '2' and k != 'local']
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')
}
else: else:
outputs['local'] = {'id': 'local', 'data': Helper.term_message('等待初始化... ')} host_ids = json.loads(req.host_ids)
if req.deploy.extend == '2':
message = Helper.term_message('等待初始化... ') with_local = False
if is_fail_mode: hosts = Host.objects.filter(id__in=host_ids)
message = Helper.term_message('已完成本地动作忽略执行', 'warn') message = Helper.term_message('等待调度... ')
outputs['local'] = {'id': 'local', 'data': message} outputs = {x.id: {'id': x.id, 'title': x.name, 'data': message} for x in hosts}
s_actions = json.loads(req.deploy.extend_obj.server_actions) if req.deploy.extend == '1':
h_actions = json.loads(req.deploy.extend_obj.host_actions) if req.repository_id:
if not s_actions: if req.is_quick_deploy:
outputs.pop('local') outputs['local'] = {
if not h_actions: 'id': 'local',
outputs = {'local': outputs['local']} 'status': 'success',
return json_response({'outputs': outputs, 'token': req.deploy_key}) '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') @auth('deploy.request.approve')
def patch(self, request, r_id): def patch(self, request, r_id):
@ -323,7 +345,7 @@ def get_request_info(request):
if error is None: if error is None:
req = DeployRequest.objects.get(pk=form.id) req = DeployRequest.objects.get(pk=form.id)
response = req.to_dict(selects=('status', 'reason')) 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() response['status_alias'] = req.get_status_display()
return json_response(response) return json_response(response)
return json_response(error=error) return json_response(error=error)

View File

@ -26,6 +26,7 @@ def dispatch(rep: Repository, helper=None):
helper = Helper.make(rds, rep.deploy_key, ['local']) helper = Helper.make(rds, rep.deploy_key, ['local'])
rep.save() rep.save()
try: try:
helper.set_deploy_process('local')
api_token = uuid.uuid4().hex api_token = uuid.uuid4().hex
helper.rds.setex(api_token, 60 * 60, f'{rep.app_id},{rep.env_id}') helper.rds.setex(api_token, 60 * 60, f'{rep.app_id},{rep.env_id}')
env = AttrDict( env = AttrDict(
@ -63,8 +64,10 @@ def dispatch(rep: Repository, helper=None):
_build(rep, helper, env) _build(rep, helper, env)
rep.status = '5' rep.status = '5'
helper.set_deploy_success('local')
except Exception as e: except Exception as e:
rep.status = '2' rep.status = '2'
helper.set_deploy_fail('local')
raise e raise e
finally: finally:
helper.local(f'cd {REPOS_DIR} && rm -rf {rep.spug_version}') helper.local(f'cd {REPOS_DIR} && rm -rf {rep.spug_version}')

View File

@ -78,7 +78,12 @@ function Console(props) {
function doDeploy() { function doDeploy() {
let socket; 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 => { .then(res => {
_handleResponse(res) _handleResponse(res)
socket = _makeSocket() socket = _makeSocket()

View File

@ -249,6 +249,7 @@ export default observer(function () {
)} )}
</Form> </Form>
{visible && <HostSelector {visible && <HostSelector
title="可选主机列表"
host_ids={host_ids} host_ids={host_ids}
app_host_ids={app_host_ids} app_host_ids={app_host_ids}
onCancel={() => setVisible(false)} onCancel={() => setVisible(false)}

View File

@ -120,6 +120,7 @@ export default observer(function () {
)} )}
</Form> </Form>
{visible && <HostSelector {visible && <HostSelector
title="可选主机列表"
host_ids={host_ids} host_ids={host_ids}
app_host_ids={app_host_ids} app_host_ids={app_host_ids}
onCancel={() => setVisible(false)} onCancel={() => setVisible(false)}

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-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'; import hostStore from 'pages/host/store';
export default observer(function (props) { 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 ( return (
<Modal <Modal
visible visible
width={600} width={800}
title='可选主机列表' title={props.title}
onOk={handleSubmit} onOk={handleSubmit}
okButtonProps={{disabled: selectedRowKeys.length === 0}}
onCancel={props.onCancel}> onCancel={props.onCancel}>
{selectedRowKeys.length > 0 && ( <Alert
<Alert style={{marginBottom: 12}}
style={{marginBottom: 12}} message={<span>已选择 <b style={{color: '#2563fc', fontSize: 18}}>{selectedRowKeys.length}</b> </span>}
message={`已选择 ${selectedRowKeys.length} 台主机`} action={<Button type="link" disabled={selectedRowKeys.length === 0}
action={<Button type="link" onClick={() => setSelectedRowKeys([])}>取消选择</Button>}/> onClick={() => setSelectedRowKeys([])}>取消选择</Button>}/>
)}
<Table <Table
rowKey="id" rowKey="id"
dataSource={hostStore.records.filter(x => props.app_host_ids.includes(x.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="name"/>
<Table.Column title="连接地址" dataIndex="hostname"/> <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> </Table>
</Modal> </Modal>
) )

View File

@ -39,7 +39,7 @@ export default observer(function () {
}, () => setLoading(false)) }, () => setLoading(false))
} }
const {app_host_ids, deploy_id} = store.record; const {app_host_ids, deploy_id, deploy_status} = store.record;
return ( return (
<Modal <Modal
visible visible
@ -79,8 +79,10 @@ export default observer(function () {
</Form.Item> </Form.Item>
</Form> </Form>
{visible && <HostSelector {visible && <HostSelector
title="选择回滚发布的主机"
host_ids={host_ids} host_ids={host_ids}
app_host_ids={app_host_ids} app_host_ids={app_host_ids}
deploy_status={deploy_status}
onCancel={() => setVisible(false)} onCancel={() => setVisible(false)}
onOk={ids => setHostIds(ids)}/>} onOk={ids => setHostIds(ids)}/>}
</Modal> </Modal>

View File

@ -3,27 +3,21 @@
* Copyright (c) <spug.dev@gmail.com> * Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License. * Released under the AGPL-3.0 License.
*/ */
import React from 'react'; import React, { useState } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { BranchesOutlined, BuildOutlined, TagOutlined, PlusOutlined, TagsOutlined } from '@ant-design/icons'; 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 { http, hasPermission } from 'libs';
import { Action, AuthButton, TableCard } from 'components'; import { Action, AuthButton, TableCard } from 'components';
import HostSelector from './HostSelector';
import S from './index.module.less'; import S from './index.module.less';
import store from './store'; import store from './store';
import moment from 'moment'; import moment from 'moment';
import lds from 'lodash';
function DeployConfirm() {
return (
<div>
<div>确认发布方式</div>
<div style={{color: '#999', fontSize: 12}}>补偿仅发布上次发布失败的主机</div>
<div style={{color: '#999', fontSize: 12}}>全量再次发布所有主机</div>
</div>
)
}
function ComTable() { function ComTable() {
const [request, setRequest] = useState()
const columns = [{ const columns = [{
title: '申请标题', title: '申请标题',
className: S.min180, className: S.min180,
@ -144,6 +138,14 @@ function ComTable() {
<Action.Button auth="deploy.request.do" onClick={() => store.rollback(info)}>回滚</Action.Button> <Action.Button auth="deploy.request.do" onClick={() => store.rollback(info)}>回滚</Action.Button>
)} )}
</Action>; </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': case '-1':
return <Action> return <Action>
<Action.Button auth="deploy.request.edit" onClick={() => store.showForm(info)}>编辑</Action.Button> <Action.Button auth="deploy.request.edit" onClick={() => store.showForm(info)}>编辑</Action.Button>
@ -171,17 +173,23 @@ function ComTable() {
}]; }];
function DoAction(props) { function DoAction(props) {
const {host_ids, fail_host_ids} = props.info; const {deploy_status} = props.info;
return ( return (
<Popconfirm <Popover trigger="click" zIndex={2} title="确认发布方式" content={(
title={<DeployConfirm/>} <div>
okText="全量" <div style={{color: '#999', fontSize: 12}}>全量发布所有主机包含已成功的</div>
cancelText="补偿" <div style={{color: '#999', fontSize: 12}}>补偿仅发布上次发布失败的主机</div>
cancelButtonProps={{disabled: [0, host_ids.length].includes(fail_host_ids.length)}} <div style={{color: '#999', fontSize: 12}}>灰度选择指定主机发布</div>
onConfirm={e => handleDeploy(e, props.info, 'all')} <Space style={{width: '100%', justifyContent: 'flex-end', marginTop: 16}}>
onCancel={e => handleDeploy(e, props.info, 'fail')}> <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> <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 info.mode = mode
store.showConsole(info); store.showConsole(info)
if (request) setRequest()
} }
return ( return (
<TableCard <React.Fragment>
tKey="dr" <TableCard
rowKey={row => row.key || row.id} tKey="dr"
title="申请列表" rowKey={row => row.key || row.id}
columns={columns} title="申请列表"
scroll={{x: 1500}} columns={columns}
tableLayout="auto" scroll={{x: 1500}}
loading={store.isFetching} tableLayout="auto"
dataSource={store.dataSource} loading={store.isFetching}
onReload={store.fetchRecords} dataSource={store.dataSource}
actions={[ onReload={store.fetchRecords}
<AuthButton actions={[
auth="deploy.request.add" <AuthButton
type="primary" auth="deploy.request.add"
icon={<PlusOutlined/>} type="primary"
onClick={() => store.addVisible = true}>新建申请</AuthButton>, icon={<PlusOutlined/>}
<Radio.Group value={store.f_status} onChange={e => store.f_status = e.target.value}> onClick={() => store.addVisible = true}>新建申请</AuthButton>,
<Radio.Button value="all">全部({store.counter['all'] || 0})</Radio.Button> <Radio.Group value={store.f_status} onChange={e => store.f_status = e.target.value}>
<Radio.Button value="0">待审核({store.counter['0'] || 0})</Radio.Button> <Radio.Button value="all">全部({store.counter['all'] || 0})</Radio.Button>
<Radio.Button value="1">待发布({store.counter['1'] || 0})</Radio.Button> <Radio.Button value="0">待审核({store.counter['0'] || 0})</Radio.Button>
<Radio.Button value="3">发布成功({store.counter['3'] || 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.Button value="-3">发布异常({store.counter['-3'] || 0})</Radio.Button>
</Radio.Group> <Radio.Button value="99">其他({store.counter['99'] || 0})</Radio.Button>
]} </Radio.Group>
pagination={{ ]}
showSizeChanger: true, pagination={{
showLessItems: true, showSizeChanger: true,
showTotal: total => `${total}`, showLessItems: true,
pageSizeOptions: ['10', '20', '50', '100'] 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>
) )
} }

View File

@ -108,7 +108,7 @@ class Store {
}; };
rollback = (info) => { 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.app_host_ids = info.host_ids;
this.record.name = `${info.name} - 回滚`; this.record.name = `${info.name} - 回滚`;
this.rollbackVisible = true this.rollbackVisible = true