mirror of https://github.com/openspug/spug
improve app deploy
parent
402250f7b1
commit
b81b1f66ac
|
@ -5,6 +5,7 @@ from django.db import models
|
||||||
from libs import ModelMixin, human_datetime
|
from libs import ModelMixin, human_datetime
|
||||||
from apps.account.models import User
|
from apps.account.models import User
|
||||||
from apps.app.models import Deploy
|
from apps.app.models import Deploy
|
||||||
|
from apps.repository.models import Repository
|
||||||
|
|
||||||
|
|
||||||
class DeployRequest(models.Model, ModelMixin):
|
class DeployRequest(models.Model, ModelMixin):
|
||||||
|
@ -21,6 +22,7 @@ class DeployRequest(models.Model, ModelMixin):
|
||||||
('2', '回滚')
|
('2', '回滚')
|
||||||
)
|
)
|
||||||
deploy = models.ForeignKey(Deploy, on_delete=models.CASCADE)
|
deploy = models.ForeignKey(Deploy, on_delete=models.CASCADE)
|
||||||
|
repository = models.ForeignKey(Repository, null=True, on_delete=models.SET_NULL)
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
type = models.CharField(max_length=2, choices=TYPES, default='1')
|
type = models.CharField(max_length=2, choices=TYPES, default='1')
|
||||||
extra = models.TextField()
|
extra = models.TextField()
|
||||||
|
@ -29,6 +31,7 @@ class DeployRequest(models.Model, ModelMixin):
|
||||||
status = models.CharField(max_length=2, choices=STATUS)
|
status = models.CharField(max_length=2, choices=STATUS)
|
||||||
reason = models.CharField(max_length=255, null=True)
|
reason = models.CharField(max_length=255, null=True)
|
||||||
version = models.CharField(max_length=50, null=True)
|
version = models.CharField(max_length=50, null=True)
|
||||||
|
spug_version = models.CharField(max_length=50, null=True)
|
||||||
|
|
||||||
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='+')
|
||||||
|
|
|
@ -7,6 +7,7 @@ from .views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('request/', RequestView.as_view()),
|
path('request/', RequestView.as_view()),
|
||||||
|
path('request/1/', post_request_1),
|
||||||
path('request/upload/', do_upload),
|
path('request/upload/', do_upload),
|
||||||
path('request/<int:r_id>/', RequestDetailView.as_view()),
|
path('request/<int:r_id>/', RequestDetailView.as_view()),
|
||||||
]
|
]
|
||||||
|
|
|
@ -22,13 +22,14 @@ class SpugError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def deploy_dispatch(request, req, token):
|
def dispatch(req):
|
||||||
rds = get_redis_connection()
|
rds = get_redis_connection()
|
||||||
|
rds_key = f'{settings.REQUEST_KEY}:{req.id}'
|
||||||
try:
|
try:
|
||||||
api_token = uuid.uuid4().hex
|
api_token = uuid.uuid4().hex
|
||||||
rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}')
|
rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}')
|
||||||
helper = Helper(rds, token, req.id)
|
helper = Helper(rds, rds_key)
|
||||||
helper.send_step('local', 1, f'完成\r\n{human_time()} 发布准备... ')
|
# helper.send_step('local', 1, f'完成\r\n{human_time()} 发布准备... ')
|
||||||
env = AttrDict(
|
env = AttrDict(
|
||||||
SPUG_APP_NAME=req.deploy.app.name,
|
SPUG_APP_NAME=req.deploy.app.name,
|
||||||
SPUG_APP_ID=str(req.deploy.app_id),
|
SPUG_APP_ID=str(req.deploy.app_id),
|
||||||
|
@ -43,7 +44,6 @@ def deploy_dispatch(request, req, token):
|
||||||
SPUG_REPOS_DIR=REPOS_DIR,
|
SPUG_REPOS_DIR=REPOS_DIR,
|
||||||
)
|
)
|
||||||
if req.deploy.extend == '1':
|
if req.deploy.extend == '1':
|
||||||
env.update(json.loads(req.deploy.extend_obj.custom_envs))
|
|
||||||
_ext1_deploy(req, helper, env)
|
_ext1_deploy(req, helper, env)
|
||||||
else:
|
else:
|
||||||
_ext2_deploy(req, helper, env)
|
_ext2_deploy(req, helper, env)
|
||||||
|
@ -52,7 +52,7 @@ def deploy_dispatch(request, req, token):
|
||||||
req.status = '-3'
|
req.status = '-3'
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
rds.expire(token, 5 * 60)
|
rds.expire(rds_key, 14 * 24 * 60 * 60)
|
||||||
rds.close()
|
rds.close()
|
||||||
req.save()
|
req.save()
|
||||||
Helper.send_deploy_notify(req)
|
Helper.send_deploy_notify(req)
|
||||||
|
@ -60,55 +60,12 @@ def deploy_dispatch(request, req, token):
|
||||||
|
|
||||||
def _ext1_deploy(req, helper, env):
|
def _ext1_deploy(req, helper, env):
|
||||||
extend = req.deploy.extend_obj
|
extend = req.deploy.extend_obj
|
||||||
extras = json.loads(req.extra)
|
|
||||||
env.update(SPUG_DST_DIR=extend.dst_dir)
|
env.update(SPUG_DST_DIR=extend.dst_dir)
|
||||||
if extras[0] == 'branch':
|
|
||||||
tree_ish = extras[2]
|
|
||||||
env.update(SPUG_GIT_BRANCH=extras[1], SPUG_GIT_COMMIT_ID=extras[2])
|
|
||||||
else:
|
|
||||||
tree_ish = extras[1]
|
|
||||||
env.update(SPUG_GIT_TAG=extras[1])
|
|
||||||
if req.type == '2':
|
|
||||||
helper.send_step('local', 6, f'完成\r\n{human_time()} 回滚发布... 跳过')
|
|
||||||
else:
|
|
||||||
helper.local(f'cd {REPOS_DIR} && rm -rf {req.deploy_id}_*')
|
|
||||||
helper.send_step('local', 1, '完成\r\n')
|
|
||||||
|
|
||||||
if extend.hook_pre_server:
|
|
||||||
helper.send_step('local', 2, f'{human_time()} 检出前任务...\r\n')
|
|
||||||
helper.local(f'cd /tmp && {extend.hook_pre_server}', env)
|
|
||||||
|
|
||||||
helper.send_step('local', 3, f'{human_time()} 执行检出... ')
|
|
||||||
git_dir = os.path.join(REPOS_DIR, str(req.deploy.id))
|
|
||||||
command = f'cd {git_dir} && git archive --prefix={env.SPUG_VERSION}/ {tree_ish} | (cd .. && tar xf -)'
|
|
||||||
helper.local(command)
|
|
||||||
helper.send_step('local', 3, '完成\r\n')
|
|
||||||
|
|
||||||
if extend.hook_post_server:
|
|
||||||
helper.send_step('local', 4, f'{human_time()} 检出后任务...\r\n')
|
|
||||||
helper.local(f'cd {os.path.join(REPOS_DIR, env.SPUG_VERSION)} && {extend.hook_post_server}', env)
|
|
||||||
|
|
||||||
helper.send_step('local', 5, f'\r\n{human_time()} 执行打包... ')
|
|
||||||
filter_rule, exclude, contain = json.loads(extend.filter_rule), '', env.SPUG_VERSION
|
|
||||||
files = helper.parse_filter_rule(filter_rule['data'])
|
|
||||||
if files:
|
|
||||||
if filter_rule['type'] == 'exclude':
|
|
||||||
excludes = []
|
|
||||||
for x in files:
|
|
||||||
if x.startswith('/'):
|
|
||||||
excludes.append(f'--exclude={env.SPUG_VERSION}{x}')
|
|
||||||
else:
|
|
||||||
excludes.append(f'--exclude={x}')
|
|
||||||
exclude = ' '.join(excludes)
|
|
||||||
else:
|
|
||||||
contain = ' '.join(f'{env.SPUG_VERSION}/{x}' for x in files)
|
|
||||||
helper.local(f'cd {REPOS_DIR} && tar zcf {env.SPUG_VERSION}.tar.gz {exclude} {contain}')
|
|
||||||
helper.send_step('local', 6, f'完成')
|
|
||||||
threads, latest_exception = [], None
|
threads, latest_exception = [], None
|
||||||
with futures.ThreadPoolExecutor(max_workers=min(10, os.cpu_count() + 5)) as executor:
|
with futures.ThreadPoolExecutor(max_workers=min(10, os.cpu_count() + 5)) as executor:
|
||||||
for h_id in json.loads(req.host_ids):
|
for h_id in json.loads(req.host_ids):
|
||||||
env = AttrDict(env.items())
|
env = AttrDict(env.items())
|
||||||
t = executor.submit(_deploy_ext1_host, helper, h_id, extend, env)
|
t = executor.submit(_deploy_ext1_host, req, helper, h_id, env)
|
||||||
t.h_id = h_id
|
t.h_id = h_id
|
||||||
threads.append(t)
|
threads.append(t)
|
||||||
for t in futures.as_completed(threads):
|
for t in futures.as_completed(threads):
|
||||||
|
@ -188,34 +145,34 @@ def _ext2_deploy(req, helper, env):
|
||||||
helper.send_step('local', 100, f'\r\n{human_time()} ** 发布成功 **')
|
helper.send_step('local', 100, f'\r\n{human_time()} ** 发布成功 **')
|
||||||
|
|
||||||
|
|
||||||
def _deploy_ext1_host(helper, h_id, extend, env):
|
def _deploy_ext1_host(req, helper, h_id, env):
|
||||||
helper.send_step(h_id, 1, f'{human_time()} 数据准备... ')
|
extend = req.deploy.extend_obj
|
||||||
|
helper.send_step(h_id, 1, f'就绪\r\n{human_time()} 数据准备... ')
|
||||||
host = Host.objects.filter(pk=h_id).first()
|
host = Host.objects.filter(pk=h_id).first()
|
||||||
if not host:
|
if not host:
|
||||||
helper.send_error(h_id, 'no such host')
|
helper.send_error(h_id, 'no such host')
|
||||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||||
ssh = host.get_ssh()
|
ssh = host.get_ssh()
|
||||||
if env.SPUG_DEPLOY_TYPE != '2':
|
|
||||||
code, _ = ssh.exec_command(
|
code, _ = ssh.exec_command(
|
||||||
f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
|
f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
|
||||||
if code == 0:
|
if code == 0:
|
||||||
helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。')
|
helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。')
|
||||||
# clean
|
# clean
|
||||||
clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf'
|
clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf'
|
||||||
helper.remote(host.id, ssh, f'cd {extend.dst_repo} && rm -rf {env.SPUG_VERSION} && {clean_command}')
|
helper.remote(host.id, ssh, f'cd {extend.dst_repo} && rm -rf {req.spug_version} && {clean_command}')
|
||||||
# transfer files
|
# transfer files
|
||||||
tar_gz_file = f'{env.SPUG_VERSION}.tar.gz'
|
tar_gz_file = f'{req.spug_version}.tar.gz'
|
||||||
try:
|
try:
|
||||||
ssh.put_file(os.path.join(REPOS_DIR, tar_gz_file), os.path.join(extend.dst_repo, tar_gz_file))
|
ssh.put_file(os.path.join(REPOS_DIR, 'build', tar_gz_file), os.path.join(extend.dst_repo, tar_gz_file))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
helper.send_error(host.id, f'exception: {e}')
|
helper.send_error(host.id, f'exception: {e}')
|
||||||
|
|
||||||
command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {env.SPUG_APP_ID}_*.tar.gz'
|
command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
|
||||||
helper.remote(host.id, ssh, command)
|
helper.remote(host.id, ssh, command)
|
||||||
helper.send_step(h_id, 1, '完成\r\n')
|
helper.send_step(h_id, 1, '完成\r\n')
|
||||||
|
|
||||||
# pre host
|
# pre host
|
||||||
repo_dir = os.path.join(extend.dst_repo, env.SPUG_VERSION)
|
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
|
||||||
if extend.hook_pre_host:
|
if extend.hook_pre_host:
|
||||||
helper.send_step(h_id, 2, f'{human_time()} 发布前任务... \r\n')
|
helper.send_step(h_id, 2, f'{human_time()} 发布前任务... \r\n')
|
||||||
command = f'cd {repo_dir} ; {extend.hook_pre_host}'
|
command = f'cd {repo_dir} ; {extend.hook_pre_host}'
|
||||||
|
@ -271,11 +228,10 @@ def _deploy_ext2_host(helper, h_id, actions, env):
|
||||||
|
|
||||||
|
|
||||||
class Helper:
|
class Helper:
|
||||||
def __init__(self, rds, token, r_id):
|
def __init__(self, rds, key):
|
||||||
self.rds = rds
|
self.rds = rds
|
||||||
self.token = token
|
self.key = key
|
||||||
self.log_key = f'{settings.REQUEST_KEY}:{r_id}'
|
self.rds.delete(self.key)
|
||||||
self.rds.delete(self.log_key)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _make_dd_notify(cls, action, req, version, host_str):
|
def _make_dd_notify(cls, action, req, version, host_str):
|
||||||
|
@ -425,11 +381,11 @@ class Helper:
|
||||||
return files
|
return files
|
||||||
|
|
||||||
def _send(self, message):
|
def _send(self, message):
|
||||||
self.rds.lpush(self.token, json.dumps(message))
|
self.rds.rpush(self.key, json.dumps(message))
|
||||||
self.rds.lpush(self.log_key, json.dumps(message))
|
|
||||||
|
|
||||||
def send_info(self, key, message):
|
def send_info(self, key, message):
|
||||||
self._send({'key': key, 'status': 'info', 'data': message})
|
if message:
|
||||||
|
self._send({'key': key, 'data': message})
|
||||||
|
|
||||||
def send_error(self, key, message, with_break=True):
|
def send_error(self, key, message, with_break=True):
|
||||||
message = '\r\n' + message
|
message = '\r\n' + message
|
||||||
|
|
|
@ -9,14 +9,14 @@ from django_redis import get_redis_connection
|
||||||
from libs import json_response, JsonParser, Argument, human_datetime, human_time
|
from libs import json_response, JsonParser, Argument, human_datetime, human_time
|
||||||
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
|
||||||
from apps.deploy.utils import deploy_dispatch, Helper
|
from apps.repository.models import Repository
|
||||||
|
from apps.deploy.utils import dispatch, Helper
|
||||||
from apps.host.models import Host
|
from apps.host.models import Host
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
import uuid
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ class RequestView(View):
|
||||||
app_name=F('deploy__app__name'),
|
app_name=F('deploy__app__name'),
|
||||||
app_host_ids=F('deploy__host_ids'),
|
app_host_ids=F('deploy__host_ids'),
|
||||||
app_extend=F('deploy__extend'),
|
app_extend=F('deploy__extend'),
|
||||||
|
rep_extra=F('repository__extra'),
|
||||||
created_by_user=F('created_by__nickname')):
|
created_by_user=F('created_by__nickname')):
|
||||||
tmp = item.to_dict()
|
tmp = item.to_dict()
|
||||||
tmp['env_id'] = item.env_id
|
tmp['env_id'] = item.env_id
|
||||||
|
@ -41,8 +42,8 @@ class RequestView(View):
|
||||||
tmp['app_id'] = item.app_id
|
tmp['app_id'] = item.app_id
|
||||||
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['extra'] = json.loads(item.extra)
|
|
||||||
tmp['host_ids'] = json.loads(item.host_ids)
|
tmp['host_ids'] = json.loads(item.host_ids)
|
||||||
|
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)
|
||||||
tmp['status_alias'] = item.get_status_display()
|
tmp['status_alias'] = item.get_status_display()
|
||||||
tmp['created_by_user'] = item.created_by_user
|
tmp['created_by_user'] = item.created_by_user
|
||||||
|
@ -62,10 +63,6 @@ class RequestView(View):
|
||||||
deploy = Deploy.objects.filter(pk=form.deploy_id).first()
|
deploy = Deploy.objects.filter(pk=form.deploy_id).first()
|
||||||
if not deploy:
|
if not deploy:
|
||||||
return json_response(error='未找到该发布配置')
|
return json_response(error='未找到该发布配置')
|
||||||
if form.extra[0] == 'tag' and not form.extra[1]:
|
|
||||||
return json_response(error='请选择要发布的Tag')
|
|
||||||
if form.extra[0] == 'branch' and not form.extra[2]:
|
|
||||||
return json_response(error='请选择要发布的分支及Commit ID')
|
|
||||||
if deploy.extend == '2':
|
if deploy.extend == '2':
|
||||||
if form.extra[0]:
|
if form.extra[0]:
|
||||||
form.extra[0] = form.extra[0].replace("'", '')
|
form.extra[0] = form.extra[0].replace("'", '')
|
||||||
|
@ -165,28 +162,29 @@ class RequestDetailView(View):
|
||||||
if not req:
|
if not req:
|
||||||
return json_response(error='未找到指定发布申请')
|
return json_response(error='未找到指定发布申请')
|
||||||
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
|
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
|
||||||
targets = [{'id': x.id, 'title': f'{x.name}({x.hostname}:{x.port})'} for x in hosts]
|
server_actions, host_actions = [], []
|
||||||
server_actions, host_actions, outputs = [], [], []
|
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': []} for x in hosts}
|
||||||
if req.deploy.extend == '2':
|
if req.deploy.extend == '2':
|
||||||
server_actions = json.loads(req.deploy.extend_obj.server_actions)
|
server_actions = json.loads(req.deploy.extend_obj.server_actions)
|
||||||
host_actions = json.loads(req.deploy.extend_obj.host_actions)
|
host_actions = json.loads(req.deploy.extend_obj.host_actions)
|
||||||
if request.GET.get('log'):
|
|
||||||
rds, key, counter = get_redis_connection(), f'{settings.REQUEST_KEY}:{r_id}', 0
|
rds, key, counter = get_redis_connection(), f'{settings.REQUEST_KEY}:{r_id}', 0
|
||||||
data = rds.lrange(key, counter, counter + 9)
|
data = rds.lrange(key, counter, counter + 9)
|
||||||
while data:
|
while data:
|
||||||
counter += 10
|
counter += 10
|
||||||
outputs.extend(x.decode() for x in data)
|
for item in data:
|
||||||
|
item = json.loads(item.decode())
|
||||||
|
if 'data' in item:
|
||||||
|
outputs[item['key']]['data'].append(item['data'])
|
||||||
|
if 'step' in item:
|
||||||
|
outputs[item['key']]['step'] = item['step']
|
||||||
|
if 'status' in item:
|
||||||
|
outputs[item['key']]['status'] = item['status']
|
||||||
data = rds.lrange(key, counter, counter + 9)
|
data = rds.lrange(key, counter, counter + 9)
|
||||||
return json_response({
|
return json_response({
|
||||||
'app_name': req.deploy.app.name,
|
|
||||||
'env_name': req.deploy.env.name,
|
|
||||||
'status': req.status,
|
|
||||||
'type': req.type,
|
|
||||||
'status_alias': req.get_status_display(),
|
|
||||||
'targets': targets,
|
|
||||||
'server_actions': server_actions,
|
'server_actions': server_actions,
|
||||||
'host_actions': host_actions,
|
'host_actions': host_actions,
|
||||||
'outputs': outputs
|
'outputs': outputs,
|
||||||
|
'status': req.status
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, r_id):
|
def post(self, request, r_id):
|
||||||
|
@ -201,17 +199,14 @@ class RequestDetailView(View):
|
||||||
if req.status not in ('1', '-3'):
|
if req.status not in ('1', '-3'):
|
||||||
return json_response(error='该申请单当前状态还不能执行发布')
|
return json_response(error='该申请单当前状态还不能执行发布')
|
||||||
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
|
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
|
||||||
token = uuid.uuid4().hex
|
message = f'{human_time()} 等待调度... '
|
||||||
outputs = {str(x.id): {'data': []} for x in hosts}
|
outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': [message]} for x in hosts}
|
||||||
outputs.update(local={'data': [f'{human_time()} 建立接连... ']})
|
|
||||||
req.status = '2'
|
req.status = '2'
|
||||||
req.do_at = human_datetime()
|
req.do_at = human_datetime()
|
||||||
req.do_by = request.user
|
req.do_by = request.user
|
||||||
if not req.version:
|
|
||||||
req.version = f'{req.deploy_id}_{req.id}_{datetime.now().strftime("%Y%m%d%H%M%S")}'
|
|
||||||
req.save()
|
req.save()
|
||||||
Thread(target=deploy_dispatch, args=(request, req, token)).start()
|
Thread(target=dispatch, args=(req,)).start()
|
||||||
return json_response({'token': token, 'type': req.type, 'outputs': outputs})
|
return json_response({'type': req.type, 'outputs': outputs})
|
||||||
|
|
||||||
def patch(self, request, r_id):
|
def patch(self, request, r_id):
|
||||||
form, error = JsonParser(
|
form, error = JsonParser(
|
||||||
|
@ -235,6 +230,36 @@ class RequestDetailView(View):
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
||||||
|
|
||||||
|
def post_request_1(request):
|
||||||
|
form, error = JsonParser(
|
||||||
|
Argument('id', type=int, required=False),
|
||||||
|
Argument('name', help='请输申请标题'),
|
||||||
|
Argument('repository_id', type=int, help='请选择发布版本'),
|
||||||
|
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'),
|
||||||
|
Argument('desc', required=False),
|
||||||
|
).parse(request.body)
|
||||||
|
if error is None:
|
||||||
|
repository = Repository.objects.filter(pk=form.repository_id).first()
|
||||||
|
if not repository:
|
||||||
|
return json_response(error='未找到指定构建版本记录')
|
||||||
|
form.name = form.name.replace("'", '')
|
||||||
|
form.status = '0' if repository.deploy.is_audit else '1'
|
||||||
|
form.version = repository.version
|
||||||
|
form.spug_version = repository.spug_version
|
||||||
|
form.deploy_id = repository.deploy_id
|
||||||
|
form.host_ids = json.dumps(form.host_ids)
|
||||||
|
if form.id:
|
||||||
|
req = DeployRequest.objects.get(pk=form.id)
|
||||||
|
is_required_notify = repository.deploy.is_audit and req.status == '-1'
|
||||||
|
DeployRequest.objects.filter(pk=form.id).update(created_by=request.user, reason=None, **form)
|
||||||
|
else:
|
||||||
|
req = DeployRequest.objects.create(created_by=request.user, **form)
|
||||||
|
is_required_notify = repository.deploy.is_audit
|
||||||
|
if is_required_notify:
|
||||||
|
Thread(target=Helper.send_deploy_notify, args=(req, 'approve_req')).start()
|
||||||
|
return json_response(error=error)
|
||||||
|
|
||||||
|
|
||||||
def do_upload(request):
|
def do_upload(request):
|
||||||
repos_dir = settings.REPOS_DIR
|
repos_dir = settings.REPOS_DIR
|
||||||
file = request.FILES['file']
|
file = request.FILES['file']
|
||||||
|
|
|
@ -13,10 +13,13 @@ import json
|
||||||
|
|
||||||
class RepositoryView(View):
|
class RepositoryView(View):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
deploy_id = request.GET.get('deploy_id')
|
||||||
data = Repository.objects.annotate(
|
data = Repository.objects.annotate(
|
||||||
app_name=F('app__name'),
|
app_name=F('app__name'),
|
||||||
env_name=F('env__name'),
|
env_name=F('env__name'),
|
||||||
created_by_user=F('created_by__nickname'))
|
created_by_user=F('created_by__nickname'))
|
||||||
|
if deploy_id:
|
||||||
|
data = data.filter(deploy_id=deploy_id, status='5')
|
||||||
return json_response([x.to_view() for x in data])
|
return json_response([x.to_view() for x in data])
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
@ -41,6 +44,20 @@ class RepositoryView(View):
|
||||||
return json_response(rep.to_view())
|
return json_response(rep.to_view())
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
form, error = JsonParser(
|
||||||
|
Argument('id', type=int, help='参数错误'),
|
||||||
|
Argument('action', help='参数错误')
|
||||||
|
).parse(request.body)
|
||||||
|
if error is None:
|
||||||
|
rep = Repository.objects.filter(pk=form.id).first()
|
||||||
|
if not rep:
|
||||||
|
return json_response(error='未找到指定构建记录')
|
||||||
|
if form.action == 'rebuild':
|
||||||
|
Thread(target=dispatch, args=(rep,)).start()
|
||||||
|
return json_response(rep.to_view())
|
||||||
|
return json_response(error=error)
|
||||||
|
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
form, error = JsonParser(
|
form, error = JsonParser(
|
||||||
Argument('id', type=int, help='请指定操作对象')
|
Argument('id', type=int, help='请指定操作对象')
|
||||||
|
|
|
@ -43,6 +43,8 @@ class ComConsumer(WebsocketConsumer):
|
||||||
module = self.scope['url_route']['kwargs']['module']
|
module = self.scope['url_route']['kwargs']['module']
|
||||||
if module == 'build':
|
if module == 'build':
|
||||||
self.key = f'{settings.BUILD_KEY}:{token}'
|
self.key = f'{settings.BUILD_KEY}:{token}'
|
||||||
|
elif module == 'request':
|
||||||
|
self.key = f'{settings.REQUEST_KEY}:{token}'
|
||||||
else:
|
else:
|
||||||
raise TypeError(f'unknown module for {module}')
|
raise TypeError(f'unknown module for {module}')
|
||||||
self.rds = get_redis_connection()
|
self.rds = get_redis_connection()
|
||||||
|
@ -69,7 +71,6 @@ class ComConsumer(WebsocketConsumer):
|
||||||
while response:
|
while response:
|
||||||
index += 1
|
index += 1
|
||||||
self.send(text_data=response)
|
self.send(text_data=response)
|
||||||
time.sleep(1)
|
|
||||||
response = self.get_response(index)
|
response = self.get_response(index)
|
||||||
self.send(text_data='pong')
|
self.send(text_data='pong')
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default function () {
|
||||||
rel="noopener noreferrer">文档</a>
|
rel="noopener noreferrer">文档</a>
|
||||||
</div>
|
</div>
|
||||||
<div style={{color: 'rgba(0, 0, 0, .45)'}}>
|
<div style={{color: 'rgba(0, 0, 0, .45)'}}>
|
||||||
Copyright <CopyrightOutlined/> 2020 By OpenSpug
|
Copyright <CopyrightOutlined/> 2021 By OpenSpug
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout.Footer>
|
</Layout.Footer>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* 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 { Table, Modal, Tag, message } from 'antd';
|
import { Table, Modal, Tag, message } from 'antd';
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
@ -12,6 +12,8 @@ import { http, hasPermission } from 'libs';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
function ComTable() {
|
function ComTable() {
|
||||||
|
const [loading, setLoading] = useState();
|
||||||
|
|
||||||
function handleDelete(info) {
|
function handleDelete(info) {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '删除确认',
|
title: '删除确认',
|
||||||
|
@ -26,6 +28,27 @@ function ComTable() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRebuild(info) {
|
||||||
|
if (info.status === '5') {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '重新构建提示',
|
||||||
|
content: `当前选择版本 ${info.version} 已完成构建,再次构建将覆盖已有的数据,要再次重新构建吗?`,
|
||||||
|
onOk: () => _rebuild(info)
|
||||||
|
})
|
||||||
|
} else if (info.status === '1') {
|
||||||
|
return message.error('已在构建中,请点击日志查看详情')
|
||||||
|
} else {
|
||||||
|
_rebuild(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _rebuild(info) {
|
||||||
|
setLoading(info.id);
|
||||||
|
http.patch('/api/repository/', {id: info.id, action: 'rebuild'})
|
||||||
|
.then(() => store.showConsole(info))
|
||||||
|
.finally(() => setLoading(null))
|
||||||
|
}
|
||||||
|
|
||||||
const statusColorMap = {'0': 'cyan', '1': 'blue', '2': 'red', '5': 'green'};
|
const statusColorMap = {'0': 'cyan', '1': 'blue', '2': 'red', '5': 'green'};
|
||||||
return (
|
return (
|
||||||
<TableCard
|
<TableCard
|
||||||
|
@ -54,11 +77,16 @@ function ComTable() {
|
||||||
<Table.Column ellipsis title="备注" dataIndex="remarks"/>
|
<Table.Column ellipsis title="备注" dataIndex="remarks"/>
|
||||||
<Table.Column hide title="构建时间" dataIndex="created_at"/>
|
<Table.Column hide title="构建时间" dataIndex="created_at"/>
|
||||||
<Table.Column hide title="构建人" dataIndex="created_by_user"/>
|
<Table.Column hide title="构建人" dataIndex="created_by_user"/>
|
||||||
<Table.Column width={100} title="状态" render={info => <Tag color={statusColorMap[info.status]}>{info.status_alias}</Tag>}/>
|
<Table.Column width={100} title="状态"
|
||||||
|
render={info => <Tag color={statusColorMap[info.status]}>{info.status_alias}</Tag>}/>
|
||||||
{hasPermission('config.env.edit|config.env.del') && (
|
{hasPermission('config.env.edit|config.env.del') && (
|
||||||
<Table.Column width={150} title="操作" render={info => (
|
<Table.Column width={180} title="操作" render={info => (
|
||||||
<Action>
|
<Action>
|
||||||
<Action.Button auth="config.env.edit" onClick={() => store.showDetail(info)}>详情</Action.Button>
|
<Action.Button auth="config.env.edit" onClick={() => store.showDetail(info)}>详情</Action.Button>
|
||||||
|
<Action.Button
|
||||||
|
auth="config.env.del"
|
||||||
|
loading={loading === info.id}
|
||||||
|
onClick={() => handleRebuild(info)}>构建</Action.Button>
|
||||||
<Action.Button auth="config.env.del" onClick={() => store.showConsole(info)}>日志</Action.Button>
|
<Action.Button auth="config.env.del" onClick={() => store.showConsole(info)}>日志</Action.Button>
|
||||||
</Action>
|
</Action>
|
||||||
)}/>
|
)}/>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import http from 'libs/http';
|
||||||
class Store {
|
class Store {
|
||||||
@observable records = [];
|
@observable records = [];
|
||||||
@observable record = {};
|
@observable record = {};
|
||||||
@observable idMap = {};
|
@observable deploy = {};
|
||||||
@observable outputs = [];
|
@observable outputs = [];
|
||||||
@observable isFetching = false;
|
@observable isFetching = false;
|
||||||
@observable formVisible = false;
|
@observable formVisible = false;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { observer } from 'mobx-react';
|
||||||
import { Modal, Form, Input, Switch, message } from 'antd';
|
import { Modal, Form, Input, Switch, message } from 'antd';
|
||||||
import http from 'libs/http';
|
import http from 'libs/http';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
import styles from './index.module.less';
|
||||||
|
|
||||||
export default observer(function () {
|
export default observer(function () {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
@ -38,6 +39,7 @@ export default observer(function () {
|
||||||
title="审核发布申请"
|
title="审核发布申请"
|
||||||
onCancel={() => store.approveVisible = false}
|
onCancel={() => store.approveVisible = false}
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
|
className={styles.approve}
|
||||||
onOk={handleSubmit}>
|
onOk={handleSubmit}>
|
||||||
<Form form={form} labelCol={{span: 6}} wrapperCol={{span: 14}} onValuesChange={handleChange}>
|
<Form form={form} labelCol={{span: 6}} wrapperCol={{span: 14}} onValuesChange={handleChange}>
|
||||||
<Form.Item required name="is_pass" initialValue={true} valuePropName="checked" label="审批结果">
|
<Form.Item required name="is_pass" initialValue={true} valuePropName="checked" label="审批结果">
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
* Copyright (c) <spug.dev@gmail.com>
|
||||||
|
* Released under the AGPL-3.0 License.
|
||||||
|
*/
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { observer, useLocalStore } from 'mobx-react';
|
||||||
|
import { Card, Progress, Modal, Collapse, Steps } from 'antd';
|
||||||
|
import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, } from '@ant-design/icons';
|
||||||
|
import OutView from './OutView';
|
||||||
|
import { http, X_TOKEN } from 'libs';
|
||||||
|
import styles from './index.module.less';
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
|
function Ext1Console(props) {
|
||||||
|
const outputs = useLocalStore(() => ({}));
|
||||||
|
|
||||||
|
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
|
||||||
|
|
||||||
|
function readDeploy() {
|
||||||
|
let socket;
|
||||||
|
http.get(`/api/deploy/request/${props.request.id}/`)
|
||||||
|
.then(res => {
|
||||||
|
Object.assign(outputs, res.outputs)
|
||||||
|
if (res.status === '2') {
|
||||||
|
socket = _makeSocket()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => socket && socket.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
function doDeploy() {
|
||||||
|
let socket;
|
||||||
|
http.post(`/api/deploy/request/${props.request.id}/`)
|
||||||
|
.then(res => {
|
||||||
|
Object.assign(outputs, res.outputs)
|
||||||
|
socket = _makeSocket()
|
||||||
|
})
|
||||||
|
return () => socket && socket.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeSocket() {
|
||||||
|
let index = 0;
|
||||||
|
const token = props.request.id;
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/request/${token}/?x-token=${X_TOKEN}`);
|
||||||
|
socket.onopen = () => socket.send(String(index));
|
||||||
|
socket.onmessage = e => {
|
||||||
|
if (e.data === 'pong') {
|
||||||
|
socket.send(String(index))
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
const {key, data, step, status} = JSON.parse(e.data);
|
||||||
|
if (data !== undefined) outputs[key].data.push(data);
|
||||||
|
if (step !== undefined) outputs[key].step = step;
|
||||||
|
if (status !== undefined) outputs[key].status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return socket
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepItem(props) {
|
||||||
|
let icon = null;
|
||||||
|
if (props.step === props.item.step && props.item.status !== 'error') {
|
||||||
|
icon = <LoadingOutlined/>
|
||||||
|
}
|
||||||
|
return <Steps.Step {...props} icon={icon}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchMiniMode() {
|
||||||
|
const value = store.tabModes[props.request.id];
|
||||||
|
store.tabModes[props.request.id] = !value
|
||||||
|
}
|
||||||
|
|
||||||
|
return store.tabModes[props.request.id] ? (
|
||||||
|
<Card className={styles.item} bodyStyle={{padding: '8px 12px'}} onClick={switchMiniMode}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.title}>{props.request.name}</div>
|
||||||
|
</div>
|
||||||
|
{Object.values(outputs).map(item => (
|
||||||
|
<Progress
|
||||||
|
key={item.id}
|
||||||
|
percent={(item.step + 1) * 18}
|
||||||
|
status={item.status === 'error' ? 'exception' : 'active'}/>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={1000}
|
||||||
|
footer={null}
|
||||||
|
maskClosable={false}
|
||||||
|
className={styles.console}
|
||||||
|
onCancel={() => store.showConsole(props.request, true)}
|
||||||
|
title={[
|
||||||
|
<span key="1">{props.request.name}</span>,
|
||||||
|
<div key="2" className={styles.miniIcon} onClick={switchMiniMode}>
|
||||||
|
<ShrinkOutlined/>
|
||||||
|
</div>
|
||||||
|
]}>
|
||||||
|
<Collapse
|
||||||
|
defaultActiveKey={'0'}
|
||||||
|
className={styles.collapse}
|
||||||
|
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
|
||||||
|
{Object.values(outputs).map((item, index) => (
|
||||||
|
<Collapse.Panel
|
||||||
|
key={index}
|
||||||
|
header={
|
||||||
|
<div className={styles.header}>
|
||||||
|
<b className={styles.title}>{item.title}</b>
|
||||||
|
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
|
||||||
|
<StepItem title="等待调度" item={item} step={0}/>
|
||||||
|
<StepItem title="数据准备" item={item} step={1}/>
|
||||||
|
<StepItem title="发布前任务" item={item} step={2}/>
|
||||||
|
<StepItem title="执行发布" item={item} step={3}/>
|
||||||
|
<StepItem title="发布后任务" item={item} step={4}/>
|
||||||
|
</Steps>
|
||||||
|
</div>}>
|
||||||
|
<OutView records={item.data}/>
|
||||||
|
</Collapse.Panel>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</Collapse>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(Ext1Console)
|
|
@ -5,8 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
|
import { Modal, Form, Input, Select, Tag, message } from 'antd';
|
||||||
import { Modal, Form, Input, Select, Button, Tag, message } from 'antd';
|
|
||||||
import hostStore from 'pages/host/store';
|
import hostStore from 'pages/host/store';
|
||||||
import http from 'libs/http';
|
import http from 'libs/http';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
@ -15,84 +14,29 @@ import lds from 'lodash';
|
||||||
export default observer(function () {
|
export default observer(function () {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [fetching, setFetching] = useState(true);
|
const [versions, setVersions] = useState([]);
|
||||||
const [git_type, setGitType] = useState(lds.get(store.record, 'extra.0', 'branch'));
|
const [host_ids, setHostIds] = useState([]);
|
||||||
const [extra1, setExtra1] = useState(lds.get(store.record, 'extra.1'));
|
|
||||||
const [extra2, setExtra2] = useState(lds.get(store.record, 'extra.2'));
|
|
||||||
const [versions, setVersions] = useState({});
|
|
||||||
const [host_ids, setHostIds] = useState(lds.clone(store.record.app_host_ids));
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchVersions();
|
const {deploy_id, app_host_ids, host_ids} = store.record;
|
||||||
|
setHostIds(lds.clone(host_ids || app_host_ids));
|
||||||
|
http.get('/api/repository/', {params: {deploy_id}})
|
||||||
|
.then(res => setVersions(res))
|
||||||
if (hostStore.records.length === 0) {
|
if (hostStore.records.length === 0) {
|
||||||
hostStore.fetchRecords()
|
hostStore.fetchRecords()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (extra1 === undefined) {
|
|
||||||
const {branches, tags} = versions;
|
|
||||||
let [extra1, extra2] = [undefined, undefined];
|
|
||||||
if (git_type === 'branch') {
|
|
||||||
if (branches) {
|
|
||||||
extra1 = _getDefaultBranch(branches);
|
|
||||||
extra2 = lds.get(branches[extra1], '0.id')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (tags) {
|
|
||||||
extra1 = lds.get(Object.keys(tags), 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setExtra1(extra1)
|
|
||||||
setExtra2(extra2)
|
|
||||||
}
|
|
||||||
}, [versions, git_type, extra1])
|
|
||||||
|
|
||||||
function fetchVersions() {
|
|
||||||
setFetching(true);
|
|
||||||
http.get(`/api/app/deploy/${store.record.deploy_id}/versions/`, {timeout: 120000})
|
|
||||||
.then(res => setVersions(res))
|
|
||||||
.finally(() => setFetching(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getDefaultBranch(branches) {
|
|
||||||
branches = Object.keys(branches);
|
|
||||||
let branch = branches[0];
|
|
||||||
for (let item of store.records) {
|
|
||||||
if (item['deploy_id'] === store.record['deploy_id']) {
|
|
||||||
const b = lds.get(item, 'extra.1');
|
|
||||||
if (branches.includes(b)) {
|
|
||||||
branch = b
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return branch
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchType(v) {
|
|
||||||
setExtra1(undefined);
|
|
||||||
setGitType(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchExtra1(v) {
|
|
||||||
setExtra1(v)
|
|
||||||
if (git_type === 'branch') {
|
|
||||||
setExtra2(lds.get(versions.branches[v], '0.id'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (host_ids.length === 0) {
|
if (host_ids.length === 0) {
|
||||||
return message.error('请至少选择一个要发布的目标主机')
|
return message.error('请至少选择一个要发布的主机')
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const formData = form.getFieldsValue();
|
const formData = form.getFieldsValue();
|
||||||
formData['id'] = store.record.id;
|
formData['id'] = store.record.id;
|
||||||
formData['deploy_id'] = store.record.deploy_id;
|
|
||||||
formData['host_ids'] = host_ids;
|
formData['host_ids'] = host_ids;
|
||||||
formData['extra'] = [git_type, extra1, extra2];
|
formData['deploy_id'] = store.record.deploy_id;
|
||||||
http.post('/api/deploy/request/', formData)
|
http.post('/api/deploy/request/1/', formData)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
message.success('操作成功');
|
message.success('操作成功');
|
||||||
store.ext1Visible = false;
|
store.ext1Visible = false;
|
||||||
|
@ -111,11 +55,10 @@ export default observer(function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {branches, tags} = versions;
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
width={800}
|
width={600}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
title="新建发布申请"
|
title="新建发布申请"
|
||||||
onCancel={() => store.ext1Visible = false}
|
onCancel={() => store.ext1Visible = false}
|
||||||
|
@ -125,55 +68,22 @@ export default observer(function () {
|
||||||
<Form.Item required name="name" label="申请标题">
|
<Form.Item required name="name" label="申请标题">
|
||||||
<Input placeholder="请输入申请标题"/>
|
<Input placeholder="请输入申请标题"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item required label="选择分支/标签/版本" style={{marginBottom: 12}} extra={<span>
|
<Form.Item required name="repository_id" label="发布版本">
|
||||||
根据网络情况,首次刷新可能会很慢,请耐心等待。
|
<Select placeholder="请选择">
|
||||||
<a target="_blank" rel="noopener noreferrer"
|
{versions.map(item => (
|
||||||
href="https://spug.dev/docs/install-error/#%E6%96%B0%E5%BB%BA%E5%B8%B8%E8%A7%84%E5%8F%91%E5%B8%83%E7%94%B3%E8%AF%B7-git-clone-%E9%94%99%E8%AF%AF">clone 失败?</a>
|
|
||||||
</span>}>
|
|
||||||
<Form.Item style={{display: 'inline-block', marginBottom: 0, width: '450px'}}>
|
|
||||||
<Input.Group compact>
|
|
||||||
<Select value={git_type} onChange={switchType} style={{width: 100}}>
|
|
||||||
<Select.Option value="branch">Branch</Select.Option>
|
|
||||||
<Select.Option value="tag">Tag</Select.Option>
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
showSearch
|
|
||||||
style={{width: 350}}
|
|
||||||
value={extra1}
|
|
||||||
placeholder="请稍等"
|
|
||||||
onChange={switchExtra1}
|
|
||||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}>
|
|
||||||
{git_type === 'branch' ? (
|
|
||||||
Object.keys(branches || {}).map(b => <Select.Option key={b} value={b}>{b}</Select.Option>)
|
|
||||||
) : (
|
|
||||||
Object.entries(tags || {}).map(([tag, info]) => (
|
|
||||||
<Select.Option key={tag} value={tag}>{`${tag} ${info.author} ${info.message}`}</Select.Option>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</Input.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item style={{display: 'inline-block', width: 82, textAlign: 'center', marginBottom: 0}}>
|
|
||||||
{fetching ? <LoadingOutlined style={{fontSize: 18, color: '#1890ff'}}/> :
|
|
||||||
<Button type="link" icon={<SyncOutlined/>} disabled={fetching} onClick={fetchVersions}>刷新</Button>
|
|
||||||
}
|
|
||||||
</Form.Item>
|
|
||||||
</Form.Item>
|
|
||||||
{git_type === 'branch' && (
|
|
||||||
<Form.Item required label="选择Commit ID">
|
|
||||||
<Select value={extra2} placeholder="请选择" onChange={v => setExtra2(v)}>
|
|
||||||
{extra1 && branches ? branches[extra1].map(item => (
|
|
||||||
<Select.Option
|
<Select.Option
|
||||||
key={item.id}>{item.id.substr(0, 6)} {item['date']} {item['author']} {item['message']}</Select.Option>
|
key={item.id}
|
||||||
)) : null}
|
value={item.id}>
|
||||||
|
{item.remarks ? `${item.version} (${item.remarks})` : item.version}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
|
||||||
<Form.Item name="desc" label="备注信息">
|
<Form.Item name="desc" label="备注信息">
|
||||||
<Input placeholder="请输入备注信息"/>
|
<Input placeholder="请输入备注信息"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item required label="发布目标主机" help="通过点击主机名称自由选择本次发布的主机。">
|
<Form.Item required label="发布主机" help="通过点击主机名称自由选择本次发布的主机。">
|
||||||
{store.record['app_host_ids'].map(id => (
|
{store.record.app_host_ids.map(id => (
|
||||||
<Tag.CheckableTag key={id} checked={host_ids.includes(id)} onChange={() => handleChange(id)}>
|
<Tag.CheckableTag key={id} checked={host_ids.includes(id)} onChange={() => handleChange(id)}>
|
||||||
{lds.get(hostStore.idMap, `${id}.name`)}({lds.get(hostStore.idMap, `${id}.hostname`)}:{lds.get(hostStore.idMap, `${id}.port`)})
|
{lds.get(hostStore.idMap, `${id}.name`)}({lds.get(hostStore.idMap, `${id}.hostname`)}:{lds.get(hostStore.idMap, `${id}.port`)})
|
||||||
</Tag.CheckableTag>
|
</Tag.CheckableTag>
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
* Copyright (c) <spug.dev@gmail.com>
|
||||||
|
* Released under the AGPL-3.0 License.
|
||||||
|
*/
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import styles from './index.module.less';
|
||||||
|
|
||||||
|
function OutView(props) {
|
||||||
|
const el = useRef()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (el) el.current.scrollTop = el.current.scrollHeight
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre ref={el} className={styles.out}>{props.records}</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OutView
|
|
@ -6,7 +6,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { BranchesOutlined, BuildOutlined, TagOutlined, PlusOutlined } from '@ant-design/icons';
|
import { BranchesOutlined, BuildOutlined, TagOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { Radio, Modal, Popover, Tag, message } from 'antd';
|
import { Radio, Modal, Popover, Tag, Popconfirm, 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 store from './store';
|
import store from './store';
|
||||||
|
@ -42,20 +42,12 @@ class ComTable extends React.Component {
|
||||||
title: '版本',
|
title: '版本',
|
||||||
render: info => {
|
render: info => {
|
||||||
if (info['app_extend'] === '1') {
|
if (info['app_extend'] === '1') {
|
||||||
const [type, ext1, ext2] = info.extra;
|
const [ext1] = info.rep_extra;
|
||||||
if (type === 'branch') {
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<BranchesOutlined/> {ext1}#{ext2.substr(0, 6)}
|
{ext1 === 'branch' ? <BranchesOutlined/> : <TagOutlined/>} {info.version}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<TagOutlined/> {ext1}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -88,15 +80,15 @@ class ComTable extends React.Component {
|
||||||
}, {
|
}, {
|
||||||
title: '申请人',
|
title: '申请人',
|
||||||
dataIndex: 'created_by_user',
|
dataIndex: 'created_by_user',
|
||||||
|
hide: true
|
||||||
}, {
|
}, {
|
||||||
title: '申请时间',
|
title: '申请时间',
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
sorter: (a, b) => a['created_at'].localeCompare(b['created_at'])
|
sorter: (a, b) => a['created_at'].localeCompare(b['created_at']),
|
||||||
|
hide: true
|
||||||
}, {
|
}, {
|
||||||
title: '备注',
|
title: '备注',
|
||||||
dataIndex: 'desc',
|
dataIndex: 'desc',
|
||||||
ellipsis: true,
|
|
||||||
hide: true
|
|
||||||
}, {
|
}, {
|
||||||
title: '操作',
|
title: '操作',
|
||||||
className: hasPermission('deploy.request.do|deploy.request.edit|deploy.request.approve|deploy.request.del') ? null : 'none',
|
className: hasPermission('deploy.request.do|deploy.request.edit|deploy.request.approve|deploy.request.del') ? null : 'none',
|
||||||
|
@ -104,10 +96,10 @@ class ComTable extends React.Component {
|
||||||
switch (info.status) {
|
switch (info.status) {
|
||||||
case '-3':
|
case '-3':
|
||||||
return <Action>
|
return <Action>
|
||||||
<Action.Link
|
<Action.Button auth="deploy.request.do" onClick={() => store.readConsole(info)}>查看</Action.Button>
|
||||||
auth="deploy.request.do"
|
<Popconfirm title="确认要执行该发布申请?" onConfirm={() => store.showConsole(info)}>
|
||||||
to={`/deploy/do/ext${info['app_extend']}/${info.id}/1`}>查看</Action.Link>
|
<Action.Button auth="deploy.request.do">发布</Action.Button>
|
||||||
<Action.Link auth="deploy.request.do" to={`/deploy/do/ext${info['app_extend']}/${info.id}`}>发布</Action.Link>
|
</Popconfirm>
|
||||||
<Action.Button
|
<Action.Button
|
||||||
auth="deploy.request.do"
|
auth="deploy.request.do"
|
||||||
disabled={info.type === '2'}
|
disabled={info.type === '2'}
|
||||||
|
@ -138,7 +130,7 @@ class ComTable extends React.Component {
|
||||||
</Action>;
|
</Action>;
|
||||||
case '1':
|
case '1':
|
||||||
return <Action>
|
return <Action>
|
||||||
<Action.Link auth="deploy.request.do" to={`/deploy/do/ext${info['app_extend']}/${info.id}`}>发布</Action.Link>
|
<Action.Button auth="deploy.request.do" onClick={() => store.showConsole(info)}>发布</Action.Button>
|
||||||
<Action.Button auth="deploy.request.del" onClick={() => this.handleDelete(info)}>删除</Action.Button>
|
<Action.Button auth="deploy.request.del" onClick={() => this.handleDelete(info)}>删除</Action.Button>
|
||||||
</Action>;
|
</Action>;
|
||||||
case '2':
|
case '2':
|
||||||
|
|
|
@ -6,17 +6,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { ExclamationCircleOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import { Form, Select, DatePicker, Modal, Input, message } from 'antd';
|
import { Form, Select, DatePicker, Modal, Input, Row, Col, message } from 'antd';
|
||||||
import { SearchForm, AuthDiv, AuthButton, Breadcrumb, AppSelector } from 'components';
|
import { SearchForm, AuthDiv, AuthButton, Breadcrumb, AppSelector } from 'components';
|
||||||
import Ext1Form from './Ext1Form';
|
import Ext1Form from './Ext1Form';
|
||||||
import Ext2Form from './Ext2Form';
|
import Ext2Form from './Ext2Form';
|
||||||
import Approve from './Approve';
|
import Approve from './Approve';
|
||||||
import ComTable from './Table';
|
import ComTable from './Table';
|
||||||
|
import Ext1Console from './Ext1Console';
|
||||||
import { http, includes } from 'libs';
|
import { http, includes } from 'libs';
|
||||||
import envStore from 'pages/config/environment/store';
|
import envStore from 'pages/config/environment/store';
|
||||||
import appStore from 'pages/config/app/store';
|
import appStore from 'pages/config/app/store';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import styles from './index.module.less';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Index extends React.Component {
|
class Index extends React.Component {
|
||||||
|
@ -120,6 +122,15 @@ class Index extends React.Component {
|
||||||
{store.ext1Visible && <Ext1Form/>}
|
{store.ext1Visible && <Ext1Form/>}
|
||||||
{store.ext2Visible && <Ext2Form/>}
|
{store.ext2Visible && <Ext2Form/>}
|
||||||
{store.approveVisible && <Approve/>}
|
{store.approveVisible && <Approve/>}
|
||||||
|
{store.tabs.length > 0 && (
|
||||||
|
<Row gutter={12} className={styles.miniConsole}>
|
||||||
|
{store.tabs.map(item => (
|
||||||
|
<Col key={item.id}>
|
||||||
|
<Ext1Console request={item}/>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</AuthDiv>
|
</AuthDiv>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
.approve {
|
||||||
|
:global(.ant-switch) {
|
||||||
|
background: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-switch-checked) {
|
||||||
|
background: #389e0d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.miniConsole {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 12px;
|
||||||
|
right: 24px;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
width: 180px;
|
||||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, .3);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(0, 0, 0, .45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:hover {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.console {
|
||||||
|
.miniIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: block;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
line-height: 56px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(0, 0, 0, .45);
|
||||||
|
margin-right: 56px;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
width: 600px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.out {
|
||||||
|
min-height: 40px;
|
||||||
|
max-height: 400px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse :global(.ant-collapse-content-box) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
|
@ -5,11 +5,14 @@
|
||||||
*/
|
*/
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import http from 'libs/http';
|
import http from 'libs/http';
|
||||||
|
import lds from 'lodash';
|
||||||
|
|
||||||
class Store {
|
class Store {
|
||||||
@observable records = [];
|
@observable records = [];
|
||||||
@observable record = {};
|
@observable record = {};
|
||||||
@observable counter = {};
|
@observable counter = {};
|
||||||
|
@observable tabs = [];
|
||||||
|
@observable tabModes = {};
|
||||||
@observable isFetching = false;
|
@observable isFetching = false;
|
||||||
@observable addVisible = false;
|
@observable addVisible = false;
|
||||||
@observable ext1Visible = false;
|
@observable ext1Visible = false;
|
||||||
|
@ -82,6 +85,28 @@ class Store {
|
||||||
showApprove = (info) => {
|
showApprove = (info) => {
|
||||||
this.record = info;
|
this.record = info;
|
||||||
this.approveVisible = true;
|
this.approveVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
showConsole = (info, isClose) => {
|
||||||
|
const index = lds.findIndex(this.tabs, x => x.id === info.id);
|
||||||
|
if (isClose) {
|
||||||
|
if (index !== -1) {
|
||||||
|
this.tabs.splice(index, 1)
|
||||||
|
delete this.tabModes[info.id]
|
||||||
|
}
|
||||||
|
} else if (index === -1) {
|
||||||
|
this.tabModes[info.id] = true
|
||||||
|
this.tabs.push(info)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
readConsole = (info) => {
|
||||||
|
this.tabModes[info.id] = false
|
||||||
|
const index = lds.findIndex(this.tabs, x => x.id === info.id);
|
||||||
|
if (index === -1) {
|
||||||
|
info = Object.assign({}, info, {mode: 'read'})
|
||||||
|
this.tabs.push(info)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ export default function () {
|
||||||
<a title="文档" href="https://www.spug.dev/docs/about-spug/" target="_blank"
|
<a title="文档" href="https://www.spug.dev/docs/about-spug/" target="_blank"
|
||||||
rel="noopener noreferrer">文档</a>
|
rel="noopener noreferrer">文档</a>
|
||||||
</div>
|
</div>
|
||||||
<div style={{color: 'rgba(0, 0, 0, .45)'}}>Copyright <CopyrightOutlined/> 2020 By OpenSpug</div>
|
<div style={{color: 'rgba(0, 0, 0, .45)'}}>Copyright <CopyrightOutlined/> 2021 By OpenSpug</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue