mirror of https://github.com/openspug/spug
fix issue
parent
ec90b820ba
commit
4e45fe9136
|
@ -2,10 +2,13 @@
|
||||||
# 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.
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
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.config.models import Environment
|
from apps.config.models import Environment
|
||||||
|
import subprocess
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class App(models.Model, ModelMixin):
|
class App(models.Model, ModelMixin):
|
||||||
|
@ -62,8 +65,15 @@ class Deploy(models.Model, ModelMixin):
|
||||||
deploy.update(self.extend_obj.to_dict())
|
deploy.update(self.extend_obj.to_dict())
|
||||||
return deploy
|
return deploy
|
||||||
|
|
||||||
|
def delete(self, using=None, keep_parents=False):
|
||||||
|
deploy_id = self.id
|
||||||
|
super().delete(using, keep_parents)
|
||||||
|
repo_dir = os.path.join(settings.REPOS_DIR, str(deploy_id))
|
||||||
|
build_dir = os.path.join(settings.BUILD_DIR, f'{deploy_id}_*')
|
||||||
|
subprocess.Popen(f'rm -rf {repo_dir} {repo_dir + "_*"} {build_dir}', shell=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Deploy app_id=%r>' % self.app_id
|
return '<Deploy app_id=%r env_id=%r>' % (self.app_id, self.env_id)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'deploys'
|
db_table = 'deploys'
|
||||||
|
|
|
@ -3,15 +3,12 @@
|
||||||
# Released under the AGPL-3.0 License.
|
# Released under the AGPL-3.0 License.
|
||||||
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 libs import JsonParser, Argument, json_response
|
from libs import JsonParser, Argument, json_response
|
||||||
from apps.app.models import App, Deploy, DeployExtend1, DeployExtend2
|
from apps.app.models import App, Deploy, DeployExtend1, DeployExtend2
|
||||||
from apps.config.models import Config
|
from apps.config.models import Config
|
||||||
from apps.app.utils import fetch_versions, remove_repo
|
from apps.app.utils import fetch_versions, remove_repo
|
||||||
from apps.setting.utils import AppSetting
|
from apps.setting.utils import AppSetting
|
||||||
import subprocess
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
@ -164,11 +161,11 @@ class DeployView(View):
|
||||||
).parse(request.GET)
|
).parse(request.GET)
|
||||||
if error is None:
|
if error is None:
|
||||||
deploy = Deploy.objects.get(pk=form.id)
|
deploy = Deploy.objects.get(pk=form.id)
|
||||||
if deploy.repository_set.exists():
|
if deploy.deployrequest_set.exists():
|
||||||
return json_response(error='已存在关联的构建版本,请删除关联的构建版本后再尝试删除发布配置')
|
return json_response(error='已存在关联的发布记录,请删除关联的发布记录后再尝试删除发布配置')
|
||||||
|
for item in deploy.repository_set.all():
|
||||||
|
item.delete()
|
||||||
deploy.delete()
|
deploy.delete()
|
||||||
repo_dir = os.path.join(settings.REPOS_DIR, str(form.id))
|
|
||||||
subprocess.Popen(f'rm -rf {repo_dir} {repo_dir + "_*"}', shell=True)
|
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
# 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.
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
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
|
from apps.repository.models import Repository
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class DeployRequest(models.Model, ModelMixin):
|
class DeployRequest(models.Model, ModelMixin):
|
||||||
|
@ -50,6 +52,17 @@ class DeployRequest(models.Model, ModelMixin):
|
||||||
return extra[0] in ('branch', 'tag')
|
return extra[0] in ('branch', 'tag')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def delete(self, using=None, keep_parents=False):
|
||||||
|
super().delete(using, keep_parents)
|
||||||
|
if self.repository_id:
|
||||||
|
if not DeployRequest.objects.filter(repository=self.repository).exists():
|
||||||
|
self.repository.delete()
|
||||||
|
if self.deploy.extend == '2':
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(settings.REPOS_DIR, str(self.deploy_id), self.spug_version))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<DeployRequest name={self.name}>'
|
return f'<DeployRequest name={self.name}>'
|
||||||
|
|
||||||
|
|
|
@ -85,36 +85,37 @@ class RequestView(View):
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
form, error = JsonParser(
|
form, error = JsonParser(
|
||||||
Argument('id', type=int, required=False),
|
Argument('id', type=int, required=False),
|
||||||
Argument('expire', required=False),
|
Argument('mode', filter=lambda x: x in ('count', 'expire', 'deploy'), required=False, help='参数错误'),
|
||||||
Argument('count', type=int, required=False, help='请输入数字')
|
Argument('value', required=False),
|
||||||
).parse(request.GET)
|
).parse(request.GET)
|
||||||
if error is None:
|
if error is None:
|
||||||
rds = get_redis_connection()
|
|
||||||
if form.id:
|
if form.id:
|
||||||
DeployRequest.objects.filter(pk=form.id, status__in=('0', '1', '-1')).delete()
|
deploy = DeployRequest.objects.filter(pk=form.id).first()
|
||||||
|
if not deploy or deploy.status not in ('0', '1', '-1'):
|
||||||
|
return json_response(error='未找到指定发布申请或当前状态不允许删除')
|
||||||
|
deploy.delete()
|
||||||
return json_response()
|
return json_response()
|
||||||
elif form.count:
|
|
||||||
if form.count < 1:
|
count = 0
|
||||||
|
if form.mode == 'count':
|
||||||
|
if not str(form.value).isdigit() or int(form.value) < 1:
|
||||||
return json_response(error='请输入正确的保留数量')
|
return json_response(error='请输入正确的保留数量')
|
||||||
counter, ids = defaultdict(int), []
|
counter, form.value = defaultdict(int), int(form.value)
|
||||||
for item in DeployRequest.objects.all():
|
for item in DeployRequest.objects.all():
|
||||||
if counter[item.deploy_id] == form.count:
|
counter[item.deploy_id] += 1
|
||||||
ids.append(item.id)
|
if counter[item.deploy_id] > form.value:
|
||||||
else:
|
count += 1
|
||||||
counter[item.deploy_id] += 1
|
item.delete()
|
||||||
count, _ = DeployRequest.objects.filter(id__in=ids).delete()
|
elif form.mode == 'expire':
|
||||||
if ids:
|
for item in DeployRequest.objects.filter(created_at__lt=form.value):
|
||||||
rds.delete(*(f'{settings.REQUEST_KEY}:{x}' for x in ids))
|
count += 1
|
||||||
return json_response(count)
|
item.delete()
|
||||||
elif form.expire:
|
elif form.mode == 'deploy':
|
||||||
requests = DeployRequest.objects.filter(created_at__lt=form.expire)
|
app_id, env_id = str(form.value).split(',')
|
||||||
ids = [x.id for x in requests]
|
for item in DeployRequest.objects.filter(deploy__app_id=app_id, deploy__env_id=env_id):
|
||||||
count, _ = requests.delete()
|
count += 1
|
||||||
if ids:
|
item.delete()
|
||||||
rds.delete(*(f'{settings.REQUEST_KEY}:{x}' for x in ids))
|
return json_response(count)
|
||||||
return json_response(count)
|
|
||||||
else:
|
|
||||||
return json_response(error='请至少使用一个删除条件')
|
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
# 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.
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
from libs.mixins import ModelMixin
|
from libs.mixins import ModelMixin
|
||||||
from apps.app.models import App, Environment, Deploy
|
from apps.app.models import App, Environment, Deploy
|
||||||
from apps.account.models import User
|
from apps.account.models import User
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Repository(models.Model, ModelMixin):
|
class Repository(models.Model, ModelMixin):
|
||||||
|
@ -43,7 +45,14 @@ class Repository(models.Model, ModelMixin):
|
||||||
tmp['created_by_user'] = self.created_by_user
|
tmp['created_by_user'] = self.created_by_user
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
|
def delete(self, using=None, keep_parents=False):
|
||||||
|
super().delete(using, keep_parents)
|
||||||
|
try:
|
||||||
|
build_file = f'{self.spug_version}.tar.gz'
|
||||||
|
os.remove(os.path.join(settings.BUILD_DIR, build_file))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'repositories'
|
db_table = 'repositories'
|
||||||
ordering = ('-id',)
|
ordering = ('-id',)
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ from apps.repository.utils import dispatch
|
||||||
from apps.app.models import Deploy
|
from apps.app.models import Deploy
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoryView(View):
|
class RepositoryView(View):
|
||||||
|
@ -83,11 +82,6 @@ class RepositoryView(View):
|
||||||
if repository.deployrequest_set.exists():
|
if repository.deployrequest_set.exists():
|
||||||
return json_response(error='已关联发布申请无法删除')
|
return json_response(error='已关联发布申请无法删除')
|
||||||
repository.delete()
|
repository.delete()
|
||||||
build_file = f'{repository.spug_version}.tar.gz'
|
|
||||||
try:
|
|
||||||
os.remove(os.path.join(settings.REPOS_DIR, 'build', build_file))
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,7 @@ EXEC_WORKER_KEY = 'spug:exec:worker'
|
||||||
REQUEST_KEY = 'spug:request'
|
REQUEST_KEY = 'spug:request'
|
||||||
BUILD_KEY = 'spug:build'
|
BUILD_KEY = 'spug:build'
|
||||||
REPOS_DIR = os.path.join(BASE_DIR, 'repos')
|
REPOS_DIR = os.path.join(BASE_DIR, 'repos')
|
||||||
|
BUILD_DIR = os.path.join(REPOS_DIR, 'build')
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
* Copyright (c) <spug.dev@gmail.com>
|
||||||
|
* Released under the AGPL-3.0 License.
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Modal, Form, Input, Select, Radio, DatePicker, Space, message } from 'antd';
|
||||||
|
import { http, includes } from 'libs';
|
||||||
|
import store from './store';
|
||||||
|
import appStore from '../app/store';
|
||||||
|
import envStore from 'pages/config/environment/store';
|
||||||
|
|
||||||
|
export default observer(function () {
|
||||||
|
const [mode, setMode] = useState('expire')
|
||||||
|
const [value, setValue] = useState()
|
||||||
|
const [appId, setAppId] = useState()
|
||||||
|
const [envId, setEnvId] = useState()
|
||||||
|
const [loading, setLoading] = useState()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(appStore.records).length === 0) appStore.fetchRecords()
|
||||||
|
if (envStore.records.length === 0) envStore.fetchRecords()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
const formData = {mode, value};
|
||||||
|
if (mode === 'deploy') {
|
||||||
|
if (!appId || !envId) return message.error('请选择要删除的应用和环境')
|
||||||
|
formData.value = `${appId},${envId}`
|
||||||
|
} else if (mode === 'expire') {
|
||||||
|
if (!value) return message.error('请选择截止日期')
|
||||||
|
formData.value = value.format('YYYY-MM-DD')
|
||||||
|
} else if (!value) {
|
||||||
|
return message.error('请输入保留个数')
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
http.delete('/api/deploy/request/', {params: formData})
|
||||||
|
.then(res => {
|
||||||
|
message.success(`删除 ${res} 条发布记录`);
|
||||||
|
store.batchVisible = false;
|
||||||
|
store.fetchRecords()
|
||||||
|
}, () => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(e) {
|
||||||
|
setMode(e.target.value)
|
||||||
|
setValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={400}
|
||||||
|
maskClosable={false}
|
||||||
|
title="批量删除发布申请"
|
||||||
|
onCancel={() => store.batchVisible = false}
|
||||||
|
confirmLoading={loading}
|
||||||
|
onOk={handleSubmit}>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item label="删除方式 :">
|
||||||
|
<Radio.Group value={mode} placeholder="请选择" style={{width: 280}} onChange={handleChange}>
|
||||||
|
<Radio.Button value="expire">截止时间</Radio.Button>
|
||||||
|
<Radio.Button value="count">保留记录</Radio.Button>
|
||||||
|
<Radio.Button value="deploy">发布配置</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
{mode === 'expire' && (
|
||||||
|
<Form.Item
|
||||||
|
label="截止日期 :"
|
||||||
|
help={<div>将删除截止日期<span style={{color: 'red'}}>之前</span>的所有发布申请记录。</div>}>
|
||||||
|
<DatePicker value={value} style={{width: 290}} onChange={setValue} placeholder="请选择截止日期"/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{mode === 'count' && (
|
||||||
|
<Form.Item label="保留记录 :" help="每个应用每个环境仅保留最新的N条发布申请。">
|
||||||
|
<Input value={value} style={{width: 290}} onChange={e => setValue(e.target.value)} placeholder="请输入保留个数"/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{mode === 'deploy' && (
|
||||||
|
<Form.Item label="发布配置 :" help="删除指定应用环境下的发布申请记录。">
|
||||||
|
<Space>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
style={{width: 160}}
|
||||||
|
value={appId}
|
||||||
|
onChange={setAppId}
|
||||||
|
filterOption={(i, o) => includes(o.children, i)}
|
||||||
|
placeholder="请选择应用">
|
||||||
|
{Object.values(appStore.records).map(item => (
|
||||||
|
<Select.Option key={item.id} value={item.id}>{item.name}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
style={{width: 122}}
|
||||||
|
value={envId}
|
||||||
|
onChange={setEnvId}
|
||||||
|
filterOption={(i, o) => includes(o.children, i)}
|
||||||
|
placeholder="请选择环境">
|
||||||
|
{envStore.records.map(item => (
|
||||||
|
<Select.Option key={item.id} value={item.id}>{item.name}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
})
|
|
@ -5,8 +5,8 @@
|
||||||
*/
|
*/
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { ExclamationCircleOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
import { Form, Select, DatePicker, Modal, Input, Space, message } from 'antd';
|
import { Select, DatePicker, Space } 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';
|
||||||
|
@ -14,7 +14,8 @@ import Approve from './Approve';
|
||||||
import ComTable from './Table';
|
import ComTable from './Table';
|
||||||
import Ext1Console from './Ext1Console';
|
import Ext1Console from './Ext1Console';
|
||||||
import Ext2Console from './Ext2Console';
|
import Ext2Console from './Ext2Console';
|
||||||
import { http, includes } from 'libs';
|
import BatchDelete from './BatchDelete';
|
||||||
|
import { 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';
|
||||||
|
@ -29,33 +30,6 @@ function Index() {
|
||||||
return () => store.leaveConsole()
|
return () => store.leaveConsole()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
function handleBatchDel() {
|
|
||||||
let [expire, count] = [];
|
|
||||||
Modal.confirm({
|
|
||||||
icon: <ExclamationCircleOutlined/>,
|
|
||||||
title: '批量删除发布申请',
|
|
||||||
content: (
|
|
||||||
<Form layout="vertical" style={{marginTop: 24}}>
|
|
||||||
<Form.Item label="截止日期 :" help={<div>将删除截止日期<span style={{color: 'red'}}>之前</span>的所有发布申请记录。</div>}>
|
|
||||||
<DatePicker style={{width: 200}} placeholder="请输入"
|
|
||||||
onChange={val => expire = val.format('YYYY-MM-DD')}/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="保留记录 :" help="每个应用每个环境仅保留最新的N条发布申请,优先级高于截止日期">
|
|
||||||
<Input allowClear style={{width: 200}} placeholder="请输入保留个数"
|
|
||||||
onChange={e => count = e.target.value}/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
),
|
|
||||||
onOk: () => {
|
|
||||||
return http.delete('/api/deploy/request/', {params: {expire, count}})
|
|
||||||
.then(res => {
|
|
||||||
message.success(`成功删除${res}条记录`);
|
|
||||||
store.fetchRecords()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthDiv auth="deploy.request.view">
|
<AuthDiv auth="deploy.request.view">
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
|
@ -100,7 +74,7 @@ function Index() {
|
||||||
auth="deploy.request.del"
|
auth="deploy.request.del"
|
||||||
type="danger"
|
type="danger"
|
||||||
icon={<DeleteOutlined/>}
|
icon={<DeleteOutlined/>}
|
||||||
onClick={handleBatchDel}>批量删除</AuthButton>
|
onClick={() => store.batchVisible = true}>批量删除</AuthButton>
|
||||||
</SearchForm.Item>
|
</SearchForm.Item>
|
||||||
</SearchForm>
|
</SearchForm>
|
||||||
<ComTable/>
|
<ComTable/>
|
||||||
|
@ -110,6 +84,7 @@ function Index() {
|
||||||
onSelect={store.confirmAdd}/>
|
onSelect={store.confirmAdd}/>
|
||||||
{store.ext1Visible && <Ext1Form/>}
|
{store.ext1Visible && <Ext1Form/>}
|
||||||
{store.ext2Visible && <Ext2Form/>}
|
{store.ext2Visible && <Ext2Form/>}
|
||||||
|
{store.batchVisible && <BatchDelete/>}
|
||||||
{store.approveVisible && <Approve/>}
|
{store.approveVisible && <Approve/>}
|
||||||
{store.tabs.length > 0 && (
|
{store.tabs.length > 0 && (
|
||||||
<Space className={styles.miniConsole}>
|
<Space className={styles.miniConsole}>
|
||||||
|
|
|
@ -19,6 +19,7 @@ class Store {
|
||||||
@observable addVisible = false;
|
@observable addVisible = false;
|
||||||
@observable ext1Visible = false;
|
@observable ext1Visible = false;
|
||||||
@observable ext2Visible = false;
|
@observable ext2Visible = false;
|
||||||
|
@observable batchVisible = false;
|
||||||
@observable approveVisible = false;
|
@observable approveVisible = false;
|
||||||
|
|
||||||
@observable f_status = 'all';
|
@observable f_status = 'all';
|
||||||
|
|
Loading…
Reference in New Issue