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,6 +244,14 @@ 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': | ||||||
|  |             if action.get('src_mode') == '1': | ||||||
|  |                 try: | ||||||
|  |                     ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, env.SPUG_VERSION), action['dst']) | ||||||
|  |                 except Exception as 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']) |                 sp_dir, sd_dst = os.path.split(action['src']) | ||||||
|                 tar_gz_file = f'{env.SPUG_VERSION}.tar.gz' |                 tar_gz_file = f'{env.SPUG_VERSION}.tar.gz' | ||||||
|                 try: |                 try: | ||||||
|  |  | ||||||
|  | @ -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,7 +101,23 @@ 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 | ||||||
|  |                   spellCheck={false} | ||||||
|  |                   disabled={store.isReadOnly || item['src_mode'] === '1'} | ||||||
|  |                   placeholder="请输入本地(部署spug的容器或主机)路径" | ||||||
|  |                   value={item['src']} | ||||||
|  |                   onChange={e => item['src'] = e.target.value} | ||||||
|  |                   addonBefore={( | ||||||
|  |                     <Select disabled={store.isReadOnly} style={{width: 120}} value={item['src_mode'] || '0'} | ||||||
|  |                             onChange={v => item['src_mode'] = v}> | ||||||
|  |                       <Select.Option value="0">本地路径</Select.Option> | ||||||
|  |                       <Select.Option value="1">发布时上传</Select.Option> | ||||||
|  |                     </Select> | ||||||
|  |                   )}/> | ||||||
|  |               </Form.Item>, | ||||||
|  |               item['src_mode'] === '0' ? ( | ||||||
|  |                 <Form.Item key={1} label="过滤规则" help={this.helpMap[item['mode']]}> | ||||||
|                   <Input |                   <Input | ||||||
|                     spellCheck={false} |                     spellCheck={false} | ||||||
|                     placeholder="请输入逗号分割的过滤规则" |                     placeholder="请输入逗号分割的过滤规则" | ||||||
|  | @ -109,23 +125,18 @@ class Ext2Setup3 extends React.Component { | ||||||
|                     onChange={e => item['rule'] = e.target.value.replace(',', ',')} |                     onChange={e => item['rule'] = e.target.value.replace(',', ',')} | ||||||
|                     disabled={store.isReadOnly || item['mode'] === '0'} |                     disabled={store.isReadOnly || item['mode'] === '0'} | ||||||
|                     addonBefore={( |                     addonBefore={( | ||||||
|                     <Select disabled={store.isReadOnly} style={{width: 100}} value={item['mode']} |                       <Select disabled={store.isReadOnly} style={{width: 120}} value={item['mode']} | ||||||
|                               onChange={v => item['mode'] = v}> |                               onChange={v => item['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.Option value="2">排除</Select.Option> | ||||||
|                       </Select> |                       </Select> | ||||||
|                     )}/> |                     )}/> | ||||||
|               </Form.Item>, |                 </Form.Item> | ||||||
|               <Form.Item key={1} required label="传输路径" extra={<a |               ) : 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
	
	 vapao
						vapao