mirror of https://github.com/openspug/spug
A 自定义发布支持发布时上传数据 #156
parent
708ed4119c
commit
c82906a5df
|
@ -7,5 +7,6 @@ from .views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('request/', RequestView.as_view()),
|
path('request/', RequestView.as_view()),
|
||||||
|
path('request/upload/', do_upload),
|
||||||
path('request/<int:r_id>/', RequestDetailView.as_view()),
|
path('request/<int:r_id>/', RequestDetailView.as_view()),
|
||||||
]
|
]
|
||||||
|
|
|
@ -136,7 +136,9 @@ def _ext2_deploy(req, helper, env):
|
||||||
tmp_transfer_file = None
|
tmp_transfer_file = None
|
||||||
for action in host_actions:
|
for action in host_actions:
|
||||||
if action.get('type') == 'transfer':
|
if action.get('type') == 'transfer':
|
||||||
helper.send_info('local', f'{human_time()} 检测到数据传输动作,执行打包... ')
|
if action.get('src_mode') == '1':
|
||||||
|
break
|
||||||
|
helper.send_info('local', f'{human_time()} 检测到来源为本地路径的数据传输动作,执行打包... ')
|
||||||
action['src'] = action['src'].rstrip('/ ')
|
action['src'] = action['src'].rstrip('/ ')
|
||||||
action['dst'] = action['dst'].rstrip('/ ')
|
action['dst'] = action['dst'].rstrip('/ ')
|
||||||
if not action['src'] or not action['dst']:
|
if not action['src'] or not action['dst']:
|
||||||
|
@ -242,15 +244,23 @@ def _deploy_ext2_host(helper, h_id, actions, env):
|
||||||
for index, action in enumerate(actions):
|
for index, action in enumerate(actions):
|
||||||
helper.send_step(h_id, 2 + index, f'{human_time()} {action["title"]}...\r\n')
|
helper.send_step(h_id, 2 + index, f'{human_time()} {action["title"]}...\r\n')
|
||||||
if action.get('type') == 'transfer':
|
if action.get('type') == 'transfer':
|
||||||
sp_dir, sd_dst = os.path.split(action['src'])
|
if action.get('src_mode') == '1':
|
||||||
tar_gz_file = f'{env.SPUG_VERSION}.tar.gz'
|
try:
|
||||||
try:
|
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, env.SPUG_VERSION), action['dst'])
|
||||||
ssh.put_file(os.path.join(sp_dir, tar_gz_file), f'/tmp/{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}')
|
helper.send_info(host.id, 'transfer completed\r\n')
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
sp_dir, sd_dst = os.path.split(action['src'])
|
||||||
|
tar_gz_file = f'{env.SPUG_VERSION}.tar.gz'
|
||||||
|
try:
|
||||||
|
ssh.put_file(os.path.join(sp_dir, tar_gz_file), f'/tmp/{tar_gz_file}')
|
||||||
|
except Exception as e:
|
||||||
|
helper.send_error(host.id, f'exception: {e}')
|
||||||
|
|
||||||
command = f'cd /tmp && tar xf {tar_gz_file} && rm -f {tar_gz_file} '
|
command = f'cd /tmp && tar xf {tar_gz_file} && rm -f {tar_gz_file} '
|
||||||
command += f'&& rm -rf {action["dst"]} && mv /tmp/{sd_dst} {action["dst"]} && echo "transfer completed"'
|
command += f'&& rm -rf {action["dst"]} && mv /tmp/{sd_dst} {action["dst"]} && echo "transfer completed"'
|
||||||
else:
|
else:
|
||||||
command = f'cd /tmp && {action["data"]}'
|
command = f'cd /tmp && {action["data"]}'
|
||||||
helper.remote(host.id, ssh, command, env)
|
helper.remote(host.id, ssh, command, env)
|
||||||
|
|
|
@ -4,17 +4,20 @@
|
||||||
from django.views.generic import View
|
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_redis import get_redis_connection
|
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
|
from apps.app.models import Deploy, DeployExtend2
|
||||||
from apps.deploy.utils import deploy_dispatch, Helper
|
from apps.deploy.utils import deploy_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 json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class RequestView(View):
|
class RequestView(View):
|
||||||
|
@ -63,6 +66,11 @@ class RequestView(View):
|
||||||
return json_response(error='请选择要发布的Tag')
|
return json_response(error='请选择要发布的Tag')
|
||||||
if form.extra[0] == 'branch' and not form.extra[2]:
|
if form.extra[0] == 'branch' and not form.extra[2]:
|
||||||
return json_response(error='请选择要发布的分支及Commit ID')
|
return json_response(error='请选择要发布的分支及Commit ID')
|
||||||
|
if deploy.extend == '2':
|
||||||
|
if DeployExtend2.objects.filter(host_actions__contains='"src_mode": "1"').exists():
|
||||||
|
if len(form.extra) < 2:
|
||||||
|
return json_response(error='该应用的发布配置中使用了数据传输动作且设置为发布时上传,请上传要传输的数据')
|
||||||
|
form.version = form.extra[1].get('path')
|
||||||
form.status = '0' if deploy.is_audit else '1'
|
form.status = '0' if deploy.is_audit else '1'
|
||||||
form.extra = json.dumps(form.extra)
|
form.extra = json.dumps(form.extra)
|
||||||
form.host_ids = json.dumps(form.host_ids)
|
form.host_ids = json.dumps(form.host_ids)
|
||||||
|
@ -215,3 +223,22 @@ class RequestDetailView(View):
|
||||||
req.save()
|
req.save()
|
||||||
Thread(target=Helper.send_deploy_notify, args=(req, 'approve_rst')).start()
|
Thread(target=Helper.send_deploy_notify, args=(req, 'approve_rst')).start()
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
||||||
|
|
||||||
|
def do_upload(request):
|
||||||
|
repos_dir = settings.REPOS_DIR
|
||||||
|
file = request.FILES['file']
|
||||||
|
deploy_id = request.POST.get('deploy_id')
|
||||||
|
if file and deploy_id:
|
||||||
|
dir_name = os.path.join(repos_dir, deploy_id)
|
||||||
|
file_name = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
|
command = f'mkdir -p {dir_name} && cd {dir_name} && ls | sort -rn | tail -n +11 | xargs rm -rf'
|
||||||
|
code, outputs = subprocess.getstatusoutput(command)
|
||||||
|
if code != 0:
|
||||||
|
return json_response(error=outputs)
|
||||||
|
with open(os.path.join(dir_name, file_name), 'wb') as f:
|
||||||
|
for chunk in file.chunks():
|
||||||
|
f.write(chunk)
|
||||||
|
return json_response(file_name)
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Ext2Setup3 extends React.Component {
|
||||||
const info = store.deploy;
|
const info = store.deploy;
|
||||||
info['app_id'] = store.app_id;
|
info['app_id'] = store.app_id;
|
||||||
info['extend'] = '2';
|
info['extend'] = '2';
|
||||||
info['host_actions'] = info['host_actions'].filter(x => (x.title && x.data) || (x.title && x.src && x.dst));
|
info['host_actions'] = info['host_actions'].filter(x => (x.title && x.data) || (x.title && (x.src || x.src_mode === '1') && x.dst));
|
||||||
info['server_actions'] = info['server_actions'].filter(x => x.title && x.data);
|
info['server_actions'] = info['server_actions'].filter(x => x.title && x.data);
|
||||||
http.post('/api/app/deploy/', info)
|
http.post('/api/app/deploy/', info)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
@ -101,31 +101,42 @@ class Ext2Setup3 extends React.Component {
|
||||||
placeholder="请输入"/>
|
placeholder="请输入"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{item['type'] === 'transfer' ? ([
|
{item['type'] === 'transfer' ? ([
|
||||||
<Form.Item key={0} label="过滤规则" help={this.helpMap[item['mode']]}>
|
<Form.Item key={0} required label="数据来源">
|
||||||
<Input
|
<Input
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder="请输入逗号分割的过滤规则"
|
disabled={store.isReadOnly || item['src_mode'] === '1'}
|
||||||
value={item['rule']}
|
placeholder="请输入本地(部署spug的容器或主机)路径"
|
||||||
onChange={e => item['rule'] = e.target.value.replace(',', ',')}
|
value={item['src']}
|
||||||
disabled={store.isReadOnly || item['mode'] === '0'}
|
onChange={e => item['src'] = e.target.value}
|
||||||
addonBefore={(
|
addonBefore={(
|
||||||
<Select disabled={store.isReadOnly} style={{width: 100}} value={item['mode']}
|
<Select disabled={store.isReadOnly} style={{width: 120}} value={item['src_mode'] || '0'}
|
||||||
onChange={v => item['mode'] = v}>
|
onChange={v => item['src_mode'] = v}>
|
||||||
<Select.Option value="0">关闭</Select.Option>
|
<Select.Option value="0">本地路径</Select.Option>
|
||||||
<Select.Option value="1">包含</Select.Option>
|
<Select.Option value="1">发布时上传</Select.Option>
|
||||||
<Select.Option value="2">排除</Select.Option>
|
|
||||||
</Select>
|
</Select>
|
||||||
)}/>
|
)}/>
|
||||||
</Form.Item>,
|
</Form.Item>,
|
||||||
<Form.Item key={1} required label="传输路径" extra={<a
|
item['src_mode'] === '0' ? (
|
||||||
|
<Form.Item key={1} label="过滤规则" help={this.helpMap[item['mode']]}>
|
||||||
|
<Input
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="请输入逗号分割的过滤规则"
|
||||||
|
value={item['rule']}
|
||||||
|
onChange={e => item['rule'] = e.target.value.replace(',', ',')}
|
||||||
|
disabled={store.isReadOnly || item['mode'] === '0'}
|
||||||
|
addonBefore={(
|
||||||
|
<Select disabled={store.isReadOnly} style={{width: 120}} value={item['mode']}
|
||||||
|
onChange={v => item['mode'] = v}>
|
||||||
|
<Select.Option value="0">关闭</Select.Option>
|
||||||
|
<Select.Option value="1">包含</Select.Option>
|
||||||
|
<Select.Option value="2">排除</Select.Option>
|
||||||
|
</Select>
|
||||||
|
)}/>
|
||||||
|
</Form.Item>
|
||||||
|
) : null,
|
||||||
|
<Form.Item key={2} required label="目标路径" extra={<a
|
||||||
target="_blank" rel="noopener noreferrer"
|
target="_blank" rel="noopener noreferrer"
|
||||||
href="https://spug.dev/docs/deploy-config#%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93">使用前请务必阅读官方文档。</a>}>
|
href="https://spug.dev/docs/deploy-config#%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93">使用前请务必阅读官方文档。</a>}>
|
||||||
<Input
|
|
||||||
disabled={store.isReadOnly}
|
|
||||||
spellCheck={false}
|
|
||||||
value={item['src']}
|
|
||||||
placeholder="请输入本地路径(部署spug的容器或主机)"
|
|
||||||
onChange={e => item['src'] = e.target.value}/>
|
|
||||||
<Input
|
<Input
|
||||||
disabled={store.isReadOnly}
|
disabled={store.isReadOnly}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
@ -162,7 +173,7 @@ class Ext2Setup3 extends React.Component {
|
||||||
block
|
block
|
||||||
type="dashed"
|
type="dashed"
|
||||||
disabled={store.isReadOnly || lds.findIndex(host_actions, x => x.type === 'transfer') !== -1}
|
disabled={store.isReadOnly || lds.findIndex(host_actions, x => x.type === 'transfer') !== -1}
|
||||||
onClick={() => host_actions.push({type: 'transfer', title: '数据传输', mode: '0'})}>
|
onClick={() => host_actions.push({type: 'transfer', title: '数据传输', mode: '0', src_mode: '0'})}>
|
||||||
<Icon type="plus"/>添加数据传输动作(仅能添加一个)
|
<Icon type="plus"/>添加数据传输动作(仅能添加一个)
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Modal, Form, Input, Tag, message } from 'antd';
|
import { Modal, Form, Input, Tag, Upload, message, Button, Icon } 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,9 +15,11 @@ import lds from 'lodash';
|
||||||
class Ext2Form extends React.Component {
|
class Ext2Form extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.token = localStorage.getItem('token');
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: false,
|
loading: false,
|
||||||
type: null,
|
uploading: false,
|
||||||
|
fileList: [],
|
||||||
host_ids: store.record['app_host_ids'].concat()
|
host_ids: store.record['app_host_ids'].concat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +28,11 @@ class Ext2Form extends React.Component {
|
||||||
if (hostStore.records.length === 0) {
|
if (hostStore.records.length === 0) {
|
||||||
hostStore.fetchRecords()
|
hostStore.fetchRecords()
|
||||||
}
|
}
|
||||||
|
const file = lds.get(store, 'record.extra.1');
|
||||||
|
if (file) {
|
||||||
|
file.uid = '0';
|
||||||
|
this.setState({fileList: [file]})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
|
@ -37,6 +44,9 @@ class Ext2Form extends React.Component {
|
||||||
formData['id'] = store.record.id;
|
formData['id'] = store.record.id;
|
||||||
formData['deploy_id'] = store.record.deploy_id;
|
formData['deploy_id'] = store.record.deploy_id;
|
||||||
formData['extra'] = [formData['extra']];
|
formData['extra'] = [formData['extra']];
|
||||||
|
if (this.state.fileList.length > 0) {
|
||||||
|
formData['extra'].push(lds.pick(this.state.fileList[0], ['path', 'name']))
|
||||||
|
}
|
||||||
formData['host_ids'] = this.state.host_ids;
|
formData['host_ids'] = this.state.host_ids;
|
||||||
http.post('/api/deploy/request/', formData)
|
http.post('/api/deploy/request/', formData)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
@ -57,9 +67,28 @@ class Ext2Form extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleUploadChange = (v) => {
|
||||||
|
if (v.fileList.length === 0) {
|
||||||
|
this.setState({fileList: []})
|
||||||
|
} else {
|
||||||
|
this.setState({fileList: [v.file]})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleUpload = (file, fileList) => {
|
||||||
|
this.setState({uploading: true});
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('deploy_id', store.record.deploy_id);
|
||||||
|
http.post('/api/deploy/request/upload/', formData)
|
||||||
|
.then(res => file.path = res)
|
||||||
|
.finally(() => this.setState({uploading: false}))
|
||||||
|
return false
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const info = store.record;
|
const info = store.record;
|
||||||
const {host_ids} = this.state;
|
const {host_ids, fileList, uploading} = this.state;
|
||||||
const {getFieldDecorator} = this.props.form;
|
const {getFieldDecorator} = this.props.form;
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -81,10 +110,11 @@ class Ext2Form extends React.Component {
|
||||||
<Input placeholder="请输入环境变量 SPUG_RELEASE 的值"/>
|
<Input placeholder="请输入环境变量 SPUG_RELEASE 的值"/>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="备注信息">
|
<Form.Item label="上传数据" help="通过数据传输动作来使用上传的文件。">
|
||||||
{getFieldDecorator('desc', {initialValue: info['desc']})(
|
<Upload name="file" fileList={fileList} headers={{'X-Token': this.token}} beforeUpload={this.handleUpload}
|
||||||
<Input placeholder="请输入备注信息"/>
|
data={{deploy_id: info.deploy_id}} onChange={this.handleUploadChange}>
|
||||||
)}
|
{fileList.length === 0 ? <Button loading={uploading}><Icon type="upload"/> 点击上传</Button> : null}
|
||||||
|
</Upload>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item required label="发布目标主机" help="通过点击主机名称自由选择本次发布的主机。">
|
<Form.Item required label="发布目标主机" help="通过点击主机名称自由选择本次发布的主机。">
|
||||||
{info['app_host_ids'].map(id => (
|
{info['app_host_ids'].map(id => (
|
||||||
|
|
Loading…
Reference in New Issue