add webhook for auto deploy

pull/330/head
vapao 2021-04-27 23:30:41 +08:00
parent d428933b8f
commit 20f28fd64c
13 changed files with 241 additions and 15 deletions

View File

@ -0,0 +1,96 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.http.response import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
from apps.setting.utils import AppSetting
from apps.deploy.models import Deploy, DeployRequest
from apps.repository.models import Repository
from apps.repository.utils import dispatch as build_dispatch
from apps.deploy.utils import dispatch as deploy_dispatch
from threading import Thread
import json
def auto_deploy(request, deploy_id, kind):
token = request.headers.get('X-Gitlab-Token') or request.headers.get('X-Gitee-Token')
if token and _is_valid_token(token):
try:
body = json.loads(request.body)
commit_id = body['after']
_, _kind, ref = body['ref'].split('/', 2)
if commit_id != '0000000000000000000000000000000000000000':
if kind == 'branch':
if _kind == 'heads' and ref == request.GET.get('name'):
Thread(target=_dispatch, args=(deploy_id, ref, commit_id)).start()
return HttpResponse(status=202)
elif kind == 'tag':
if _kind == 'tags':
Thread(target=_dispatch, args=(deploy_id, ref)).start()
return HttpResponse(status=202)
return HttpResponse(status=204)
except Exception as e:
return HttpResponseBadRequest(e)
return HttpResponseForbidden()
def _is_valid_token(token):
api_key = AppSetting.get_default('api_key')
return api_key == token
def _dispatch(deploy_id, ref, commit_id=None):
deploy = Deploy.objects.filter(pk=deploy_id).first()
if not deploy:
raise Exception(f'no such deploy id for {deploy_id}')
if deploy.extend == '1':
_deploy_extend_1(deploy, ref, commit_id)
else:
_deploy_extend_2(deploy, ref, commit_id)
def _deploy_extend_1(deploy, ref, commit_id=None):
if commit_id:
extra = ['branch', ref, commit_id]
version = f'#_b_{commit_id[:6]}'
else:
extra = ['tag', ref, None]
version = f'#_t_{ref}'
rep = Repository.objects.create(
deploy=deploy,
app_id=deploy.app_id,
env_id=deploy.env_id,
version=version,
status='1',
extra=json.dumps(extra),
spug_version=Repository.make_spug_version(deploy.id),
created_by=deploy.created_by)
rep = build_dispatch(rep)
if rep.status == '5':
req = DeployRequest.objects.create(
type='3',
status='2',
deploy=deploy,
repository=rep,
name=rep.version,
version=rep.version,
spug_version=rep.spug_version,
host_ids=deploy.host_ids,
created_by=deploy.created_by
)
deploy_dispatch(req)
def _deploy_extend_2(deploy, ref, commit_id=None):
# 创建 环境变量 分支 commit-id tag
version = f'#_b_{commit_id[:6]}' if commit_id else f'#_t_{ref}'
req = DeployRequest.objects.create(
type='3',
status='2',
deploy=deploy,
name=version,
version=version,
spug_version=Repository.make_spug_version(deploy.id),
host_ids=deploy.host_ids,
created_by=deploy.created_by
)
deploy_dispatch(req)

View File

@ -4,7 +4,9 @@
from django.urls import path
from apps.apis import config
from apps.apis import deploy
urlpatterns = [
path('config/', config.get_configs),
path('deploy/<int:deploy_id>/<str:kind>/', deploy.auto_deploy)
]

View File

@ -23,10 +23,14 @@ def parse_envs(text):
def fetch_versions(deploy: Deploy):
git_repo = deploy.extend_obj.git_repo
repo_dir = os.path.join(settings.REPOS_DIR, str(deploy.id))
try:
pkey = AppSetting.get('private_key')
except KeyError:
pkey = None
pkey = AppSetting.get_default('private_key')
with Git(git_repo, repo_dir, pkey) as git:
return git.fetch_branches_tags()
def fetch_repo(deploy_id, git_repo):
repo_dir = os.path.join(settings.REPOS_DIR, str(deploy_id))
pkey = AppSetting.get_default('private_key')
with Git(git_repo, repo_dir, pkey) as git:
return git.fetch_branches_tags()

View File

@ -19,7 +19,8 @@ class DeployRequest(models.Model, ModelMixin):
)
TYPES = (
('1', '正常发布'),
('2', '回滚')
('2', '回滚'),
('3', '自动发布'),
)
deploy = models.ForeignKey(Deploy, on_delete=models.CASCADE)
repository = models.ForeignKey(Repository, null=True, on_delete=models.SET_NULL)

View File

@ -6,6 +6,7 @@ from django.conf import settings
from django.db import close_old_connections
from libs.utils import AttrDict, human_time
from apps.repository.models import Repository
from apps.app.utils import fetch_repo
import subprocess
import json
import uuid
@ -51,6 +52,7 @@ def dispatch(rep: Repository):
rds.expire(rds_key, 14 * 24 * 60 * 60)
rds.close()
rep.save()
return rep
def _build(rep: Repository, helper, env):
@ -66,6 +68,7 @@ def _build(rep: Repository, helper, env):
else:
tree_ish = extras[1]
env.update(SPUG_GIT_TAG=extras[1])
fetch_repo(rep.deploy_id, extend.git_repo)
helper.send_info('local', '完成\r\n')
if extend.hook_pre_server:
@ -115,7 +118,6 @@ class Helper:
return files
def _send(self, message):
print(message)
self.rds.rpush(self.key, json.dumps(message))
def send_info(self, key, message):

View File

@ -21,7 +21,7 @@ class Git:
self.repo.archive(f, commit)
def fetch_branches_tags(self):
self._fetch()
self.fetch()
branches, tags = {}, {}
for ref in self.repo.references:
if isinstance(ref, RemoteReference):
@ -42,7 +42,7 @@ class Git:
tags = sorted(tags.items(), key=lambda x: x[1]['date'], reverse=True)
return branches, dict(tags)
def _fetch(self):
def fetch(self):
try:
self.repo.remotes.origin.fetch(p=True, P=True)
except GitCommandError as e:

View File

@ -35,8 +35,7 @@ class AuthenticationMiddleware(MiddlewareMixin):
user = User.objects.filter(access_token=access_token).first()
if user and x_real_ip == user.last_ip and user.token_expired >= time.time() and user.is_active:
request.user = user
if request.path != '/notify/':
user.token_expired = time.time() + 8 * 60 * 60
user.token_expired = time.time() + 8 * 60 * 60
user.save()
return None
response = json_response(error="验证失败,请重新登录")

View File

@ -0,0 +1,105 @@
import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react';
import { Modal, Form, Input, Select, Radio, Button, message } from 'antd';
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
import { http } from 'libs';
import store from './store';
import styles from './index.module.css';
export default observer(function AutoDeploy() {
const [type, setType] = useState('branch');
const [fetching, setFetching] = useState(false);
const [branches, setBranches] = useState([]);
const [branch, setBranch] = useState();
const [url, setURL] = useState();
useEffect(() => {
if (store.deploy.extend === '1') {
fetchVersions()
}
}, [])
useEffect(() => {
const prefix = window.location.origin;
let tmp = `${prefix}/api/apis/deploy/${store.deploy.id}/${type}/`;
if (type === 'branch') {
tmp += `?name=${branch}`
}
setURL(tmp)
}, [type, branch])
function fetchVersions() {
setFetching(true);
http.get(`/api/app/deploy/${store.deploy.id}/versions/`)
.then(res => setBranches(Object.keys(res.branches)))
.finally(() => setFetching(false))
}
function copyToClipBoard() {
const t = document.createElement('input');
t.value = url;
document.body.appendChild(t);
t.select();
document.execCommand('copy');
t.remove();
message.success('已复制')
}
const tagMode = type === 'tag';
return (
<Modal
visible
width={540}
title="Webhook"
footer={null}
onCancel={() => store.autoVisible = false}>
<Form labelCol={{span: 6}} wrapperCol={{span: 16}}>
<Form.Item required label="触发方式">
<Radio.Group value={type} onChange={e => setType(e.target.value)}>
<Radio.Button value="branch">Branch</Radio.Button>
<Radio.Button value="tag">Tag</Radio.Button>
</Radio.Group>
</Form.Item>
{store.deploy.extend === '1' ? (
<Form.Item required={!tagMode} label="选择分支" extra={<span>
根据你的网络情况首次刷新可能会很慢请耐心等待
<a target="_blank" rel="noopener noreferrer"
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">刷新失败</a>
</span>}>
<Form.Item style={{display: 'inline-block', marginBottom: 0, width: '246px'}}>
<Select disabled={tagMode} placeholder="仅指定分支的事件触发自动发布" value={branch} onChange={setBranch}>
{branches.map(item => (
<Select.Option key={item} value={item}>{item}</Select.Option>
))}
</Select>
</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 || tagMode}
onClick={fetchVersions}>刷新</Button>
}
</Form.Item>
</Form.Item>
) : (
<Form.Item label="指定分支">
<Input
disabled={tagMode}
value={branch}
onChange={e => setBranch(e.target.value)}
placeholder="仅指定分支的事件触发自动发布"/>
</Form.Item>
)}
{type === 'branch' && !branch ? (
<Form.Item label="Webhook URL">
<div style={{color: '#ff4d4f'}}>请指定分支名称</div>
</Form.Item>
) : (
<Form.Item label="Webhook URL" extra="点击复制链接目前仅支持Gitee和Gitlab。">
<div className={styles.webhook} onClick={copyToClipBoard}>{url}</div>
</Form.Item>
)}
</Form>
</Modal>
)
})

View File

@ -102,10 +102,13 @@ function ComTable() {
<Action>
<Action.Button
auth="deploy.app.config"
onClick={e => store.showExtForm(e, record.id, info, false, true)}>查看</Action.Button>
<Action.Button auth="deploy.app.edit"
onClick={e => store.showExtForm(e, record.id, info)}>编辑</Action.Button>
<Action.Button auth="deploy.app.edit" onClick={() => handleDeployDelete(info)}>删除</Action.Button>
onClick={e => store.showAutoDeploy(info)}>Webhook</Action.Button>
{hasPermission('deploy.app.edit') ? (
<Action.Button onClick={e => store.showExtForm(e, record.id, info)}>编辑</Action.Button>
) : hasPermission('deploy.app.config') ? (
<Action.Button onClick={e => store.showExtForm(e, record.id, info, false, true)}>查看</Action.Button>
) : null}
<Action.Button danger auth="deploy.app.edit" onClick={() => handleDeployDelete(info)}>删除</Action.Button>
</Action>
)}/>
)}
@ -155,7 +158,7 @@ function ComTable() {
<Action.Button auth="deploy.app.edit" onClick={e => store.showExtForm(e, info.id)}>新建发布</Action.Button>
<Action.Button auth="deploy.app.edit" onClick={e => handleClone(e, info.id)}>克隆发布</Action.Button>
<Action.Button auth="deploy.app.edit" onClick={e => store.showForm(e, info)}>编辑</Action.Button>
<Action.Button auth="deploy.app.del" onClick={e => handleDelete(e, info)}>删除</Action.Button>
<Action.Button danger auth="deploy.app.del" onClick={e => handleDelete(e, info)}>删除</Action.Button>
</Action>
)}/>
)}

View File

@ -12,6 +12,7 @@ import ComForm from './Form';
import Ext1Form from './Ext1Form';
import Ext2Form from './Ext2Form';
import AddSelect from './AddSelect';
import AutoDeploy from './AutoDeploy';
import store from './store';
import envStore from 'pages/config/environment/store';
@ -42,6 +43,7 @@ export default observer(function () {
{store.addVisible && <AddSelect/>}
{store.ext1Visible && <Ext1Form/>}
{store.ext2Visible && <Ext2Form/>}
{store.autoVisible && <AutoDeploy/>}
</AuthDiv>
);
})

View File

@ -71,4 +71,9 @@
right: 0;
bottom: 0;
z-index: 999;
}
.webhook {
cursor: pointer;
color: #1890ff;
}

View File

@ -19,6 +19,7 @@ class Store {
@observable addVisible = false;
@observable ext1Visible = false;
@observable ext2Visible = false;
@observable autoVisible = false;
@observable selectorVisible = false;
@observable f_name;
@ -79,6 +80,11 @@ class Store {
}
};
showAutoDeploy = (deploy) => {
this.deploy = deploy;
this.autoVisible = true
}
addHost = () => {
this.deploy['host_ids'].push(undefined)
};

View File

@ -18,6 +18,7 @@ function ComTable() {
render: info => (
<div>
{info.type === '2' && <Tooltip title="回滚发布"><Tag color="#f50">R</Tag></Tooltip>}
{info.type === '3' && <Tooltip title="Webhook触发"><Tag color="#87d068">A</Tag></Tooltip>}
{info.plan && <Tooltip title={`定时发布(${info.plan}`}> <Tag color="#108ee9">P</Tag></Tooltip>}
{info.name}
</div>