feat: OpenResty 支持新增扩展 (#6543)

This commit is contained in:
zhengkunwang 2024-09-20 17:24:57 +08:00 committed by GitHub
parent 6a4897b0aa
commit 3c0dca6992
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 531 additions and 13 deletions

View File

@ -116,3 +116,60 @@ func (b *BaseApi) ClearNginxProxyCache(c *gin.Context) {
}
helper.SuccessWithOutData(c)
}
// @Tags OpenResty
// @Summary Build OpenResty
// @Description 构建 OpenResty
// @Accept json
// @Param request body request.NginxBuildReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /openresty/build [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"构建 OpenResty","formatEN":"Build OpenResty"}
func (b *BaseApi) BuildNginx(c *gin.Context) {
var req request.NginxBuildReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := nginxService.Build(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags OpenResty
// @Summary Update OpenResty module
// @Description 更新 OpenResty 模块
// @Accept json
// @Param request body request.NginxModuleUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /openresty/module/update [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 OpenResty 模块","formatEN":"Update OpenResty module"}
func (b *BaseApi) UpdateNginxModule(c *gin.Context) {
var req request.NginxModuleUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := nginxService.UpdateModule(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags OpenResty
// @Summary Get OpenResty modules
// @Description 获取 OpenResty 模块
// @Success 200 {array} response.NginxModule
// @Security ApiKeyAuth
// @Router /openresty/modules [get]
func (b *BaseApi) GetNginxModules(c *gin.Context) {
modules, err := nginxService.GetModules()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, modules)
}

View File

@ -106,3 +106,16 @@ type NginxRedirectUpdate struct {
Content string `json:"content" validate:"required"`
Name string `json:"name" validate:"required"`
}
type NginxBuildReq struct {
TaskID string `json:"taskID" validate:"required"`
}
type NginxModuleUpdate struct {
Operate string `json:"operate" validate:"required,oneof=create delete update"`
Name string `json:"name" validate:"required"`
Script string `json:"script"`
Packages string `json:"packages"`
Enable bool `json:"enable"`
Params string `json:"params"`
}

View File

@ -67,3 +67,11 @@ type NginxProxyCache struct {
CacheExpire int `json:"cacheExpire" `
CacheExpireUnit string `json:"cacheExpireUnit" `
}
type NginxModule struct {
Name string `json:"name"`
Script string `json:"script"`
Packages []string `json:"packages"`
Params string `json:"params"`
Enable bool `json:"enable"`
}

View File

@ -1,7 +1,13 @@
package service
import (
"bufio"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
cmd2 "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/subosito/gotenv"
"io"
"net/http"
"os"
@ -29,6 +35,10 @@ type INginxService interface {
GetStatus() (response.NginxStatus, error)
UpdateConfigFile(req request.NginxConfigFileUpdate) error
ClearProxyCache() error
Build(req request.NginxBuildReq) error
GetModules() ([]response.NginxModule, error)
UpdateModule(req request.NginxModuleUpdate) error
}
func NewINginxService() INginxService {
@ -152,3 +162,157 @@ func (n NginxService) ClearProxyCache() error {
}
return nil
}
func (n NginxService) Build(req request.NginxBuildReq) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
fileOp := files.NewFileOp()
buildPath := path.Join(nginxInstall.GetPath(), "build")
if !fileOp.Stat(buildPath) {
return buserr.New("ErrBuildDirNotFound")
}
moduleConfigPath := path.Join(buildPath, "module.json")
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil {
return err
}
var (
modules []response.NginxModule
addModuleParams []string
addPackages []string
)
if len(moduleContent) > 0 {
_ = json.Unmarshal(moduleContent, &modules)
bashFile, err := os.OpenFile(path.Join(buildPath, "tmp", "pre.sh"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return err
}
defer bashFile.Close()
bashFileWriter := bufio.NewWriter(bashFile)
for _, module := range modules {
if !module.Enable {
continue
}
_, err = bashFileWriter.WriteString(module.Script + "\n")
if err != nil {
return err
}
addModuleParams = append(addModuleParams, module.Params)
addPackages = append(addPackages, module.Packages...)
}
err = bashFileWriter.Flush()
if err != nil {
return err
}
}
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
if err != nil {
return err
}
envs["RESTY_CONFIG_OPTIONS_MORE"] = ""
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = ""
if len(addModuleParams) > 0 {
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(addModuleParams, " ")
}
if len(addPackages) > 0 {
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(addPackages, " ")
}
_ = gotenv.Write(envs, nginxInstall.GetEnvPath())
buildTask, err := task.NewTaskWithOps(nginxInstall.Name, task.TaskBuild, task.TaskScopeApp, req.TaskID, nginxInstall.ID)
if err != nil {
return err
}
buildTask.AddSubTask("", func(t *task.Task) error {
if err = cmd2.ExecWithLogFile(fmt.Sprintf("docker compose -f %s build", nginxInstall.GetComposePath()), 15*time.Minute, t.Task.LogFile); err != nil {
return err
}
_, err = compose.DownAndUp(nginxInstall.GetComposePath())
return err
}, nil)
go func() {
_ = buildTask.Execute()
}()
return nil
}
func (n NginxService) GetModules() ([]response.NginxModule, error) {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return nil, err
}
fileOp := files.NewFileOp()
var modules []response.NginxModule
moduleConfigPath := path.Join(nginxInstall.GetPath(), "build", "module.json")
if !fileOp.Stat(moduleConfigPath) {
return modules, nil
}
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil {
return nil, err
}
if len(moduleContent) > 0 {
_ = json.Unmarshal(moduleContent, &modules)
}
return modules, nil
}
func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
fileOp := files.NewFileOp()
var modules []response.NginxModule
moduleConfigPath := path.Join(nginxInstall.GetPath(), "build", "module.json")
if !fileOp.Stat(moduleConfigPath) {
_ = fileOp.CreateFile(moduleConfigPath)
}
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil {
return err
}
if len(moduleContent) > 0 {
_ = json.Unmarshal(moduleContent, &modules)
}
switch req.Operate {
case "create":
for _, module := range modules {
if module.Name == req.Name {
return buserr.New("ErrNameIsExist")
}
}
modules = append(modules, response.NginxModule{
Name: req.Name,
Script: req.Script,
Packages: strings.Split(req.Packages, " "),
Params: req.Params,
Enable: true,
})
case "update":
for i, module := range modules {
if module.Name == req.Name {
modules[i].Script = req.Script
modules[i].Packages = strings.Split(req.Packages, " ")
modules[i].Params = req.Params
modules[i].Enable = req.Enable
break
}
}
case "delete":
for i, module := range modules {
if module.Name == req.Name {
modules = append(modules[:i], modules[i+1:]...)
break
}
}
}
moduleByte, err := json.Marshal(modules)
if err != nil {
return err
}
return fileOp.SaveFileWithByte(moduleConfigPath, moduleByte, 0644)
}

View File

@ -51,6 +51,7 @@ const (
TaskRestart = "TaskRestart"
TaskBackup = "TaskBackup"
TaskSync = "TaskSync"
TaskBuild = "TaskBuild"
)
const (

View File

@ -99,6 +99,7 @@ ErrDomainIsUsed: "Domain is already used by website {{ .name }}"
ErrDomainFormat: "{{ .name }} domain format error"
ErrDefaultAlias: "default is a reserved code name, please use another code name"
ErrParentWebsite: "You need to delete the subsite(s) {{ .name }} first"
ErrBuildDirNotFound: "Build directory does not exist"
#ssl
ErrSSLCannotDelete: "The certificate {{ .name }} is being used by the website and cannot be removed"
@ -244,4 +245,5 @@ TaskSync: "Sync"
LocalApp: "Local App"
SubTask: "Subtask"
RuntimeExtension: "Runtime Extension"
TaskBuild: "Build"

View File

@ -99,6 +99,7 @@ ErrDomainIsUsed: "域名已被網站【{{ .name }}】使用"
ErrDomainFormat: "{{ .name }} 域名格式不正確"
ErrDefaultAlias: "default 為保留代號,請使用其他代號"
ErrParentWebsite: "需要先刪除子網站 {{ .name }}"
ErrBuildDirNotFound: "編譯目錄不存在"
#ssl
ErrSSLCannotDelete: "{{ .name }} 證書正在被網站使用,無法刪除"
@ -246,4 +247,6 @@ TaskSync: "同步"
LocalApp: "本地應用"
SubTask: "子任務"
RuntimeExtension: "運行環境擴展"
TaskBuild: "構建"

View File

@ -99,6 +99,7 @@ ErrDomainIsUsed: "域名已被网站【{{ .name }}】使用"
ErrDomainFormat: "{{ .name }} 域名格式不正确"
ErrDefaultAlias: "default 为保留代号,请使用其他代号"
ErrParentWebsite: "需要先删除子网站 {{ .name }}"
ErrBuildDirNotFound: "构建目录不存在"
#ssl
ErrSSLCannotDelete: "{{ .name }} 证书正在被网站使用,无法删除"
@ -247,4 +248,5 @@ AppStore: "应用商店"
TaskSync: "同步"
LocalApp: "本地应用"
SubTask: "子任务"
RuntimeExtension: "运行环境扩展"
RuntimeExtension: "运行环境扩展"
TaskBuild: "构建"

View File

@ -19,5 +19,8 @@ func (a *NginxRouter) InitRouter(Router *gin.RouterGroup) {
groupRouter.GET("/status", baseApi.GetNginxStatus)
groupRouter.POST("/file", baseApi.UpdateNginxFile)
groupRouter.POST("/clear", baseApi.ClearNginxProxyCache)
groupRouter.POST("/build", baseApi.BuildNginx)
groupRouter.POST("/modules/update", baseApi.UpdateNginxModule)
groupRouter.GET("/modules", baseApi.GetNginxModules)
}
}

View File

@ -28,4 +28,25 @@ export namespace Nginx {
content: string;
backup: boolean;
}
export interface NginxBuildReq {
taskID: string;
}
export interface NginxModule {
name: string;
script?: string;
packages?: string[];
enable: boolean;
params: string;
}
export interface NginxModuleUpdate {
operate: string;
name: string;
script?: string;
packages?: string;
enable: boolean;
params: string;
}
}

View File

@ -11,7 +11,7 @@ export const GetNginxConfigByScope = (req: Nginx.NginxScopeReq) => {
};
export const UpdateNginxConfigByScope = (req: Nginx.NginxConfigReq) => {
return http.post<any>(`/openresty/update`, req);
return http.post(`/openresty/update`, req);
};
export const GetNginxStatus = () => {
@ -19,9 +19,21 @@ export const GetNginxStatus = () => {
};
export const UpdateNginxConfigFile = (req: Nginx.NginxFileUpdate) => {
return http.post<any>(`/openresty/file`, req);
return http.post(`/openresty/file`, req);
};
export const ClearNginxCache = () => {
return http.post<any>(`/openresty/clear`);
return http.post(`/openresty/clear`);
};
export const BuildNginx = (req: Nginx.NginxBuildReq) => {
return http.post(`/openresty/build`, req);
};
export const GetNginxModules = () => {
return http.get<Nginx.NginxModule[]>(`/openresty/modules`);
};
export const UpdateNginxModule = (req: Nginx.NginxModuleUpdate) => {
return http.post(`/openresty/modules/update`, req);
};

View File

@ -9,13 +9,7 @@
:width="width"
>
<div>
<highlightjs
class="editor-main"
ref="editorRef"
language="JavaScript"
:autodetect="false"
:code="content"
></highlightjs>
<highlightjs class="editor-main" ref="editorRef" :autodetect="false" :code="content"></highlightjs>
</div>
</el-dialog>
</template>
@ -98,7 +92,7 @@ const getContent = (pre: boolean) => {
});
data.value = res.data;
if (res.data.content != '') {
if (stopSignals.some((signal) => res.data.content.endsWith(signal))) {
if (stopSignals.some((signal) => res.data.content.includes(signal))) {
onCloseLog();
}
if (end.value) {

View File

@ -2251,6 +2251,15 @@ const message = {
clearProxyCache: 'Clear reverse proxy cache',
clearProxyCacheWarn:
'Clearing the reverse proxy cache will affect all websites configured with cache and requires restarting OpenResty. Do you want to continue? ',
create: 'Add a new module',
update: 'Edit a module',
params: 'Parameters',
packages: 'Packages',
script: 'Scripts',
module: 'Modules',
build: 'Build',
buildWarn:
'Building OpenResty requires reserving a certain amount of CPU and memory, which may take a long time, please be patient',
},
ssl: {
create: 'Apply',

View File

@ -2096,6 +2096,14 @@ const message = {
saveAndReload: '保存並重載',
clearProxyCache: '清除反代快取',
clearProxyCacheWarn: '清除反代快取會影響所有配置快取的網站並且需要重新啟動 OpenResty 是否繼續 ',
create: '新增模組',
update: '編輯模組',
params: '參數',
packages: '軟體包',
script: '腳本',
module: '模組',
build: '建構',
buildWarn: '建構 OpenResty 需要預留一定的 CPU 和內存時間較長請耐心等待',
},
ssl: {
create: '申請證書',

View File

@ -2097,6 +2097,14 @@ const message = {
saveAndReload: '保存并重载',
clearProxyCache: '清除反代缓存',
clearProxyCacheWarn: '清除反代缓存会影响所有配置缓存的网站并且需要重启 OpenResty 是否继续',
create: '新增模块',
update: '编辑模块',
params: '参数',
packages: '软件包',
script: '脚本',
module: '模块',
build: '构建',
buildWarn: '构建 OpenResty 需要预留一定的 CPU 和内存时间较长请耐心等待',
},
ssl: {
create: '申请证书',

View File

@ -23,6 +23,9 @@
<div v-if="row.status === 'Success'">
<el-tag type="success">{{ $t('commons.status.success') }}</el-tag>
</div>
<div v-else-if="row.status === 'Running'">
<el-tag type="primary">{{ $t('process.running') }}</el-tag>
</div>
<div v-else>
<el-tooltip
class="box-item"

View File

@ -83,7 +83,12 @@
<el-text type="primary" class="cursor-pointer" @click="openConfig(row.id)">
{{ row.primaryDomain }}
</el-text>
<el-popover placement="top-start" trigger="hover" @before-enter="searchDomains(row.id)">
<el-popover
placement="left"
trigger="hover"
:width="300"
@before-enter="searchDomains(row.id)"
>
<template #reference>
<el-button link icon="Promotion" class="ml-2.5"></el-button>
</template>
@ -95,6 +100,9 @@
{{ getUrl(domain, row) }}
</el-button>
</td>
<td>
<CopyButton :content="getUrl(domain, row)" type="icon" />
</td>
</tr>
</tbody>
</table>

View File

@ -23,12 +23,16 @@
>
{{ $t('website.log') }}
</el-button>
<el-button type="primary" :plain="activeName !== '5'" @click="changeTab('5')">
{{ $t('runtime.module') }}
</el-button>
</template>
<template #main>
<Status v-if="activeName === '1'" :status="status" />
<Source v-if="activeName === '2'" />
<NginxPer v-if="activeName === '3'" />
<ContainerLog v-if="activeName === '4'" ref="dialogContainerLogRef" />
<Module v-if="activeName === '5'" />
</template>
</LayoutContent>
</template>
@ -39,6 +43,7 @@ import { nextTick, ref } from 'vue';
import ContainerLog from '@/components/container-log/index.vue';
import NginxPer from './performance/index.vue';
import Status from './status/index.vue';
import Module from './module/index.vue';
const activeName = ref('1');
const dialogContainerLogRef = ref();

View File

@ -0,0 +1,105 @@
<template>
<div>
<ComplexTable :data="data" @search="search()" :heightDiff="350" v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="openOperate">{{ $t('commons.button.create') }}</el-button>
<el-button type="primary" plain @click="buildNginx">{{ $t('nginx.build') }}</el-button>
</template>
<el-table-column prop="name" :label="$t('commons.table.name')" />
<el-table-column prop="params" :label="$t('nginx.params')" />
<el-table-column :label="$t('commons.table.status')" fix>
<template #default="{ row }">
<el-switch v-model="row.enable" />
</template>
</el-table-column>
<fu-table-operations
:ellipsis="2"
width="100px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
<TaskLog ref="taskLogRef" />
<Operate ref="operateRef" @close="search" />
<OpDialog ref="deleteRef" @search="search" @cancel="search" />
</div>
</template>
<script lang="ts" setup>
import { BuildNginx, GetNginxModules, UpdateNginxModule } from '@/api/modules/nginx';
import { newUUID } from '@/utils/util';
import TaskLog from '@/components/task-log/index.vue';
import Operate from './operate/index.vue';
import i18n from '@/lang';
import { Nginx } from '@/api/interface/nginx';
const taskLogRef = ref();
const data = ref([]);
const loading = ref(false);
const buttons = [
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Nginx.NginxModule) {
deleteModule(row);
},
},
];
const operateRef = ref();
const deleteRef = ref();
const buildNginx = async () => {
ElMessageBox.confirm(i18n.global.t('nginx.buildWarn'), i18n.global.t('nginx.build'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
const taskID = newUUID();
try {
await BuildNginx({
taskID: taskID,
});
openTaskLog(taskID);
} catch (error) {}
});
};
const search = () => {
loading.value = true;
GetNginxModules()
.then((res) => {
data.value = res.data;
})
.finally(() => {
loading.value = false;
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const openOperate = () => {
operateRef.value.acceptParams();
};
const deleteModule = async (row: Nginx.NginxModule) => {
const data = {
name: row.name,
operate: 'delete',
};
deleteRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: [row.name],
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('nginx.module'),
i18n.global.t('commons.button.delete'),
]),
api: UpdateNginxModule,
params: data,
});
};
onMounted(() => {
search();
});
</script>

View File

@ -0,0 +1,92 @@
<template>
<DrawerPro
v-model="open"
:header="$t('nginx.' + mode)"
size="large"
:resource="mode === 'edit' ? module.name : ''"
:back="handleClose"
>
<el-form ref="moduleForm" label-position="top" :model="module" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="module.name" :disabled="mode === 'edit'"></el-input>
</el-form-item>
<el-form-item :label="$t('nginx.params')" prop="params">
<el-input v-model.trim="module.params"></el-input>
</el-form-item>
<el-form-item :label="$t('nginx.packages')" prop="packages">
<el-input v-model.trim="module.packages"></el-input>
</el-form-item>
<el-form-item :label="$t('nginx.script')" prop="script">
<el-input v-model="module.script" type="textarea" :rows="10"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(moduleForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</DrawerPro>
</template>
<script lang="ts" setup>
import { UpdateNginxModule } from '@/api/modules/nginx';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
const moduleForm = ref<FormInstance>();
const open = ref(false);
const em = defineEmits(['close']);
const mode = ref('create');
const loading = ref(false);
const module = ref({
name: '',
operate: 'create',
script: '',
enable: true,
params: '',
packages: '',
});
const rules = ref({
name: [Rules.requiredInput, Rules.simpleName],
params: [Rules.requiredInput],
});
const handleClose = () => {
open.value = false;
em('close', false);
};
const acceptParams = async () => {
open.value = true;
};
const submit = async (form: FormInstance) => {
await form.validate();
if (form.validate()) {
loading.value = true;
const data = {
...module.value,
operate: mode.value,
};
UpdateNginxModule(data)
.then(() => {
if (mode.value === 'edit') {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
} else if (mode.value === 'create') {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
}
handleClose();
})
.finally(() => {
loading.value = false;
});
}
};
defineExpose({
acceptParams,
});
</script>