upgrade deploy

pull/289/head
vapao 2021-02-07 23:52:46 +08:00
parent d076c51c3d
commit 4d86288ab5
9 changed files with 171 additions and 284 deletions

View File

@ -74,10 +74,7 @@ class DeployExtend1(models.Model, ModelMixin):
deploy = models.OneToOneField(Deploy, primary_key=True, on_delete=models.CASCADE)
git_repo = models.CharField(max_length=255)
dst_dir = models.CharField(max_length=255)
dst_repo = models.CharField(max_length=255)
versions = models.IntegerField()
filter_rule = models.TextField()
custom_envs = models.TextField()
hook_pre_server = models.TextField(null=True)
hook_post_server = models.TextField(null=True)
hook_pre_host = models.TextField(null=True)
@ -86,7 +83,6 @@ class DeployExtend1(models.Model, ModelMixin):
def to_dict(self, *args, **kwargs):
tmp = super().to_dict(*args, **kwargs)
tmp['filter_rule'] = json.loads(self.filter_rule)
tmp['custom_envs'] = '\n'.join(f'{k}={v}' for k, v in json.loads(self.custom_envs).items())
return tmp
def __repr__(self):

View File

@ -120,10 +120,7 @@ class DeployView(View):
extend_form, error = JsonParser(
Argument('git_repo', handler=str.strip, help='请输入git仓库地址'),
Argument('dst_dir', handler=str.strip, help='请输入发布目标路径'),
Argument('dst_repo', handler=str.strip, help='请输入目标仓库路径'),
Argument('versions', type=int, help='请输入保留历史版本数量'),
Argument('filter_rule', type=dict, help='参数错误'),
Argument('custom_envs', handler=str.strip, required=False),
Argument('hook_pre_server', handler=str.strip, default=''),
Argument('hook_post_server', handler=str.strip, default=''),
Argument('hook_pre_host', handler=str.strip, default=''),
@ -133,7 +130,6 @@ class DeployView(View):
return json_response(error=error)
extend_form.dst_dir = extend_form.dst_dir.rstrip('/')
extend_form.filter_rule = json.dumps(extend_form.filter_rule)
extend_form.custom_envs = json.dumps(parse_envs(extend_form.custom_envs))
if form.id:
extend = DeployExtend1.objects.filter(deploy_id=form.id).first()
if extend.git_repo != extend_form.git_repo:

View File

@ -19,8 +19,7 @@ class AddSelect extends React.Component {
git_type: 'branch',
is_audit: false,
rst_notify: {mode: '0'},
versions: 10,
host_ids: [undefined],
host_ids: [],
filter_rule: {type: 'contain', data: ''}
}
};

View File

@ -23,15 +23,15 @@ export default observer(function Ext1From() {
return (
<Modal
visible
width={900}
width={800}
maskClosable={false}
title={title}
onCancel={() => store.ext1Visible = false}
footer={null}>
<Steps current={store.page} className={styles.steps}>
<Steps.Step key={0} title="基本配置"/>
<Steps.Step key={1} title="发布主机"/>
<Steps.Step key={2} title="任务配置"/>
<Steps.Step key={1} title="构建配置"/>
<Steps.Step key={2} title="发布配置"/>
</Steps>
{store.page === 0 && <Setup1/>}
{store.page === 1 && <Setup2/>}

View File

@ -8,6 +8,7 @@ import { observer } from 'mobx-react';
import { Link } from 'react-router-dom';
import { Switch, Form, Input, Select, Button } from 'antd';
import envStore from 'pages/config/environment/store';
import Selector from 'pages/host/Selector';
import store from './store';
export default observer(function Ext1Setup1() {
@ -41,6 +42,10 @@ export default observer(function Ext1Setup1() {
<Link disabled={store.isReadOnly} to="/config/environment">新建环境</Link>
</Form.Item>
</Form.Item>
<Form.Item required label="目标主机">
{info.host_ids.length > 0 && `已选择 ${info.host_ids.length}`}
<Button type="link" onClick={() => store.selectorVisible = true}>选择主机</Button>
</Form.Item>
<Form.Item required label="Git仓库地址">
<Input disabled={store.isReadOnly} value={info['git_repo']} onChange={e => info['git_repo'] = e.target.value}
placeholder="请输入Git仓库地址"/>
@ -60,9 +65,10 @@ export default observer(function Ext1Setup1() {
</span>}>
<Input
addonBefore={(
<Select disabled={store.isReadOnly}
value={info['rst_notify']['mode']} style={{width: 100}}
onChange={v => info['rst_notify']['mode'] = v}>
<Select
disabled={store.isReadOnly}
value={info['rst_notify']['mode']} style={{width: 100}}
onChange={v => info['rst_notify']['mode'] = v}>
<Select.Option value="0">关闭</Select.Option>
<Select.Option value="1">钉钉</Select.Option>
<Select.Option value="3">企业微信</Select.Option>
@ -77,9 +83,14 @@ export default observer(function Ext1Setup1() {
<Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button
type="primary"
disabled={!(info.env_id && info['git_repo'])}
disabled={!(info.env_id && info.git_repo && info.host_ids.length)}
onClick={() => store.page += 1}>下一步</Button>
</Form.Item>
<Selector
visible={store.selectorVisible}
selectedRowKeys={[...info.host_ids]}
onCancel={() => store.selectorVisible = false}
onOk={(_, ids) => info.host_ids = ids}/>
</Form>
)
})

View File

@ -5,85 +5,99 @@
*/
import React from 'react';
import { observer } from 'mobx-react';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Form, Input, Select, Button, message } from "antd";
import { hasHostPermission } from 'libs';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { Form, Radio, Button, Tooltip } from "antd";
import { cleanCommand } from 'libs';
import Editor from 'react-ace';
import 'ace-builds/src-noconflict/mode-text';
import 'ace-builds/src-noconflict/mode-sh';
import 'ace-builds/src-noconflict/theme-tomorrow';
import store from './store';
import hostStore from 'pages/host/store';
import styles from './index.module.css';
@observer
class Ext1Setup2 extends React.Component {
componentDidMount() {
if (hostStore.records.length === 0) {
hostStore.fetchRecords()
}
export default observer(function () {
const Tips = (
<a
target="_blank"
rel="noopener noreferrer"
href="https://spug.dev/docs/deploy-config/#%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F">内置全局变量</a>
)
function handleNext() {
store.page += 1
}
checkStatus = () => {
const info = store.deploy;
return info['dst_dir'] && info['dst_repo'] && info['versions'] && info['host_ids'].filter(x => x).length > 0
};
const FilterHead = (
<div style={{width: 512, display: 'flex', justifyContent: 'space-between'}}>
<span>
文件过滤规则 &nbsp;
<Tooltip title="请输入相对于项目根目录的文件路径,根据包含或排除规则进行打包。">
<QuestionCircleOutlined style={{color: 'rgba(0, 0, 0, 0.45)'}}/>
</Tooltip>
</span>
<Radio.Group
size="small"
value={store.deploy.filter_rule.type}
onChange={e => store.deploy.filter_rule.type = e.target.value}>
<Radio.Button value="contain">
<Tooltip title="仅打包匹配到的文件或目录,如果内容为空则打包所有。">包含</Tooltip>
</Radio.Button>
<Radio.Button value="exclude">
<Tooltip title="打包时排除匹配到的文件或目录,如果内容为空则不排除任何文件。">排除</Tooltip>
</Radio.Button>
</Radio.Group>
</div>
)
handleNext = () => {
const {dst_dir, dst_repo} = store.deploy;
if (dst_repo.includes(dst_dir.replace(/\/*$/, '/'))) {
message.error('仓库目录不能位于发布部署目录内')
} else {
store.page += 1
}
};
render() {
const info = store.deploy;
return (
<Form labelCol={{span: 6}} wrapperCol={{span: 14}}>
<Form.Item required label="目标主机部署路径" help="目标主机的应用根目录,例如:/var/www/html">
<Input disabled={store.isReadOnly} value={info['dst_dir']} onChange={e => info['dst_dir'] = e.target.value}
placeholder="请输入目标主机部署路径"/>
</Form.Item>
<Form.Item required label="目标主机仓库路径" help="此目录用于存储应用的历史版本,例如:/data/spug/repos">
<Input disabled={store.isReadOnly} value={info['dst_repo']} onChange={e => info['dst_repo'] = e.target.value} placeholder="请输入目标主机仓库路径"/>
</Form.Item>
<Form.Item required label="保留历史版本数量" help="早于指定数量的历史版本会被删除,以释放空间">
<Input disabled={store.isReadOnly} value={info['versions']} onChange={e => info['versions'] = e.target.value} placeholder="请输入保留历史版本数量"/>
</Form.Item>
<Form.Item required label="发布目标主机">
{info['host_ids'].map((id, index) => (
<React.Fragment key={index}>
<Select
value={id}
showSearch
placeholder="请选择"
disabled={store.isReadOnly}
style={{width: '80%', marginRight: 10, marginBottom: 12}}
optionFilterProp="children"
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
onChange={v => store.editHost(index, v)}>
{hostStore.records.filter(x => hasHostPermission(x.id)).map(item => (
<Select.Option key={item.id} value={item.id} disabled={info['host_ids'].includes(item.id)}>
{`${item.name}(${item['hostname']}:${item['port']})`}
</Select.Option>
))}
</Select>
{!store.isReadOnly && info['host_ids'].length > 1 && (
<MinusCircleOutlined className={styles.delIcon} onClick={() => store.delHost(index)} />
)}
</React.Fragment>
))}
</Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button disabled={store.isReadOnly} type="dashed" style={{width: '80%'}} onClick={store.addHost}>
<PlusOutlined />添加目标主机
</Button>
</Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button disabled={!this.checkStatus()} type="primary" onClick={this.handleNext}>下一步</Button>
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
</Form.Item>
</Form>
)
}
}
export default Ext1Setup2
const info = store.deploy;
return (
<Form layout="vertical" style={{padding: '0 120px'}}>
<Form.Item label={FilterHead} tooltip="xxx">
<Editor
readOnly={store.isReadOnly}
mode="text"
theme="tomorrow"
width="100%"
height="80px"
placeholder="每行一条规则"
value={info['filter_rule']['data']}
onChange={v => info['filter_rule']['data'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</Form.Item>
<Form.Item
label="代码检出前执行"
tooltip="在运行 Spug 的服务器(或容器)上执行,当前目录为仓库源代码目录,可以执行任意自定义命令。"
help={<span>可使用 {Tips}请避免在此修改已跟踪的文件防止在检出代码时失败</span>}>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height="120px"
placeholder="输入要执行的命令"
value={info['hook_pre_server']}
onChange={v => info['hook_pre_server'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</Form.Item>
<Form.Item
label="代码检出后执行"
style={{marginTop: 12, marginBottom: 24}}
tooltip="在运行 Spug 的服务器(或容器)上执行,当前目录为检出后的源代码目录,可执行任意自定义命令。"
help={<span>可使用 {Tips}大多数情况下在此进行构建操作</span>}>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height="120px"
placeholder="输入要执行的命令"
value={info['hook_post_server']}
onChange={v => info['hook_post_server'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button type="primary" onClick={handleNext}>下一步</Button>
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
</Form.Item>
</Form>
)
})

View File

@ -3,210 +3,79 @@
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React from 'react';
import React, {useState} from 'react';
import { observer } from 'mobx-react';
import { GitlabOutlined, InfoCircleOutlined, SettingOutlined, SwapOutlined } from '@ant-design/icons';
import { Form, Row, Col, Button, Radio, Tooltip, message } from 'antd';
import { LinkButton } from 'components';
import { Form, Button, Input, message } from 'antd';
import Editor from 'react-ace';
import 'ace-builds/src-noconflict/mode-text';
import 'ace-builds/src-noconflict/mode-sh';
import 'ace-builds/src-noconflict/theme-tomorrow';
import { http, cleanCommand } from 'libs';
import store from './store';
import http from 'libs/http';
import styles from './index.module.css';
import { cleanCommand } from "../../../libs";
@observer
class Ext1Setup3 extends React.Component {
constructor(props) {
super(props);
this.helpMap = {
'2': <span>
Spug 内置了一些全局变量这些变量可以直接使用请参考官方文档
<a target="_blank" rel="noopener noreferrer"
href="https://spug.dev/docs/deploy-config/#%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F">全局变量</a>
</span>,
'3': '在部署 Spug 的服务器上运行,可以执行任意自定义命令。',
'4': '在部署 Spug 的服务器上运行,当前目录为检出后待发布的源代码目录,可执行任意自定义命令。',
'5': '在发布的目标主机上运行,当前目录为目标主机上待发布的源代码目录,可执行任意自定义命令。',
'6': '在发布的目标主机上运行,当前目录为已发布的应用目录,可执行任意自定义命令。'
};
this.state = {
loading: false,
full: ''
}
}
export default observer(function () {
const [loading, setLoading] = useState(false);
const Tips = (
<a
target="_blank"
rel="noopener noreferrer"
href="https://spug.dev/docs/deploy-config/#%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F">内置全局变量</a>
)
handleSubmit = () => {
this.setState({loading: true});
function handleSubmit() {
setLoading(true);
const info = store.deploy;
info['app_id'] = store.app_id;
info['extend'] = '1';
info['host_ids'] = info['host_ids'].filter(x => x);
http.post('/api/app/deploy/', info)
.then(() => {
message.success('保存成功');
store.loadDeploys(store.app_id);
store.ext1Visible = false
}, () => this.setState({loading: false}))
};
handleFullscreen = (id) => {
if (this.state.full) {
this.setState({full: ''})
} else {
this.setState({full: id})
}
}, () => setLoading(false))
}
FilterLabel = (props) => (
<div style={{display: 'flex', alignItems: 'center', height: 40}}>
<div>文件过滤 :</div>
<Radio.Group
disabled={store.isReadOnly}
style={{marginLeft: 20, float: 'left'}}
value={props.type}
onChange={e => store.deploy['filter_rule']['type'] = e.target.value}>
<Radio value="contain">包含
<Tooltip title="请输入相对于项目根目录的文件路径,仅将匹配到文件传输至要发布的目标主机。">
<InfoCircleOutlined style={{color: '#515151', marginLeft: 8}}/>
</Tooltip>
</Radio>
<Radio value="exclude">排除
<Tooltip title="支持模糊匹配,如果路径以 / 开头则基于项目根目录匹配,匹配到文件将不会被传输。">
<InfoCircleOutlined style={{color: '#515151', marginLeft: 8}}/>
</Tooltip>
</Radio>
</Radio.Group>
<div style={{flex: 1, textAlign: 'right'}}>
<LinkButton onClick={() => this.handleFullscreen('1')}>{this.state.full ? '退出全屏' : '全屏'}</LinkButton>
</div>
</div>
);
NormalLabel = (props) => (
<div style={{display: 'flex', alignItems: 'center', height: 40}}>
<div style={{marginRight: 8}}>{props.title} :</div>
<Tooltip title={this.helpMap[props.id]}>
<InfoCircleOutlined style={{color: '#515151'}}/>
</Tooltip>
<div style={{flex: 1, textAlign: 'right'}}>
<LinkButton onClick={() => this.handleFullscreen(props.id)}>{this.state.full ? '退出全屏' : '全屏'}</LinkButton>
</div>
</div>
);
render() {
const info = store.deploy;
const {full} = this.state;
return (
<React.Fragment>
<Row>
<Col span={11}>
<div className={full === '1' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.FilterLabel type={info['filter_rule']['type']}/>
<Editor
readOnly={store.isReadOnly}
mode="text"
theme="tomorrow"
width="100%"
height={full === '1' ? '100vh' : '100px'}
placeholder="每行一条规则"
value={info['filter_rule']['data']}
onChange={v => info['filter_rule']['data'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
<div className={full === '3' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="代码检出前执行" id="3"/>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height={full === '3' ? '100vh' : '100px'}
placeholder="输入要执行的命令"
value={info['hook_pre_server']}
onChange={v => info['hook_pre_server'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
<div className={full === '5' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="应用发布前执行" id="5"/>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height={full === '5' ? '100vh' : '100px'}
placeholder="输入要执行的命令"
value={info['hook_pre_host']}
onChange={v => info['hook_pre_host'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
</Col>
<Col span={2}>
<div className={styles.deployBlock} style={{marginTop: 39}}>
<SettingOutlined style={{fontSize: 32}}/>
<span style={{fontSize: 12, marginTop: 5}}>基础设置</span>
</div>
<div className={styles.deployBlock}>
<GitlabOutlined style={{fontSize: 32}}/>
<span style={{fontSize: 12, marginTop: 5}}>检出代码</span>
</div>
<div className={styles.deployBlock}>
<SwapOutlined style={{fontSize: 32}}/>
<span style={{fontSize: 12, marginTop: 5}}>版本切换</span>
</div>
</Col>
<Col span={11}>
<div className={full === '2' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="自定义全局变量" id="2"/>
<Editor
readOnly={store.isReadOnly}
mode="text"
theme="tomorrow"
width="100%"
height={full === '2' ? '100vh' : '100px'}
placeholder="每行一个例如HOME=/data/spug"
value={info['custom_envs']}
onChange={v => info['custom_envs'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
<div className={full === '4' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="代码检出后执行" id="4"/>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height={full === '4' ? '100vh' : '100px'}
placeholder="输入要执行的命令"
value={info['hook_post_server']}
onChange={v => info['hook_post_server'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
<div className={full === '6' ? styles.fullScreen : null} style={{marginBottom: 24}}>
<this.NormalLabel title="应用发布后执行" id="6"/>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height={full === '6' ? '100vh' : '100px'}
placeholder="输入要执行的命令"
value={info['hook_post_host']}
onChange={v => info['hook_post_host'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</div>
</Col>
</Row>
<Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button disabled={store.isReadOnly} type="primary" onClick={this.handleSubmit}>提交</Button>
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
</Form.Item>
</React.Fragment>
)
}
}
export default Ext1Setup3
const info = store.deploy;
return (
<Form layout="vertical" style={{padding: '0 120px'}}>
<Form.Item required label="部署目标路径" tooltip="应用最终在主机上部署路径,构建的结果将会放置于该路径下。">
<Input value={info['dst_dir']} onChange={e => info['dst_dir'] = e.target.value} placeholder="请输入部署目标路径" />
</Form.Item>
<Form.Item
label="应用发布前执行"
tooltip="在发布的目标主机上运行,当前目录为目标主机上待发布的源代码目录,可执行任意自定义命令。"
help={<span>可使用 {Tips}此时还未进行文件变更可进行一些发布前置操作</span>}>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height="150px"
placeholder="输入要执行的命令"
value={info['hook_pre_host']}
onChange={v => info['hook_pre_host'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</Form.Item>
<Form.Item
label="应用发布后执行"
style={{marginTop: 12, marginBottom: 24}}
tooltip="在发布的目标主机上运行,当前目录为已发布的应用目录,可执行任意自定义命令。"
help={<span>可使用 {Tips}可以在发布后进行重启服务等操作</span>}>
<Editor
readOnly={store.isReadOnly}
mode="sh"
theme="tomorrow"
width="100%"
height="150px"
placeholder="输入要执行的命令"
value={info['hook_post_host']}
onChange={v => info['hook_post_host'] = cleanCommand(v)}
style={{border: '1px solid #e8e8e8'}}/>
</Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button disabled={store.isReadOnly} loading={loading} type="primary" onClick={handleSubmit}>提交</Button>
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
</Form.Item>
</Form>
)
})

View File

@ -91,7 +91,7 @@ class ComTable extends React.Component {
};
expandedRowRender = (record) => {
if (record['deploys'] === undefined) {
if (!record.isLoaded) {
store.loadDeploys(record.id)
}

View File

@ -18,6 +18,7 @@ class Store {
@observable addVisible = false;
@observable ext1Visible = false;
@observable ext2Visible = false;
@observable selectorVisible = false;
@observable f_name;
@observable f_desc;
@ -47,6 +48,7 @@ class Store {
};
loadDeploys = (app_id) => {
this.records[`a${app_id}`].isLoaded = true;
return http.get('/api/app/deploy/', {params: {app_id}})
.then(res => this.records[`a${app_id}`]['deploys'] = res)
};