2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 04:22:33 +08:00

feat: 支持修改Docker基本设置

This commit is contained in:
2026-01-16 03:12:46 +08:00
parent ff68df94c5
commit 1c40ad4d81
5 changed files with 705 additions and 3 deletions

View File

@@ -1,7 +1,10 @@
package docker
import (
"encoding/json"
"net/http"
"os"
"strings"
"github.com/go-chi/chi/v5"
@@ -19,6 +22,8 @@ func NewApp() *App {
func (s *App) Route(r chi.Router) {
r.Get("/config", s.GetConfig)
r.Post("/config", s.UpdateConfig)
r.Get("/settings", s.GetSettings)
r.Post("/settings", s.UpdateSettings)
}
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
@@ -50,3 +55,213 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
service.Success(w, nil)
}
// GetSettings 获取 Docker 设置
func (s *App) GetSettings(w http.ResponseWriter, r *http.Request) {
configPath := "/etc/docker/daemon.json"
// 读取配置文件
content, err := io.Read(configPath)
if err != nil {
// 如果文件不存在,返回默认设置
if os.IsNotExist(err) {
service.Success(w, Settings{})
return
}
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
// 解析 JSON
var daemonConfig DaemonConfig
if err = json.Unmarshal([]byte(content), &daemonConfig); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
// 转换为 Settings 结构
settings := Settings{
RegistryMirrors: daemonConfig.RegistryMirrors,
InsecureRegistries: daemonConfig.InsecureRegistries,
LiveRestore: daemonConfig.LiveRestore,
LogDriver: daemonConfig.LogDriver,
Hosts: daemonConfig.Hosts,
DataRoot: daemonConfig.DataRoot,
StorageDriver: daemonConfig.StorageDriver,
DNS: daemonConfig.DNS,
FirewallBackend: daemonConfig.FirewallBackend,
Iptables: daemonConfig.Iptables,
Ip6tables: daemonConfig.Ip6tables,
IpForward: daemonConfig.IpForward,
IPv6: daemonConfig.IPv6,
Bip: daemonConfig.Bip,
}
// 解析 log-opts
if daemonConfig.LogOpts != nil {
settings.LogOpts = LogOpts{
MaxSize: daemonConfig.LogOpts["max-size"],
MaxFile: daemonConfig.LogOpts["max-file"],
}
}
// 从 exec-opts 中提取 cgroup-driver
for _, opt := range daemonConfig.ExecOpts {
if strings.HasPrefix(opt, "native.cgroupdriver=") {
settings.CgroupDriver = strings.TrimPrefix(opt, "native.cgroupdriver=")
break
}
}
service.Success(w, settings)
}
// UpdateSettings 更新 Docker 设置
func (s *App) UpdateSettings(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[UpdateSettings](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
configPath := "/etc/docker/daemon.json"
settings := req.Settings
// 读取现有配置(保留其他字段)
var existingConfig map[string]any
content, err := io.Read(configPath)
if err == nil && content != "" {
if err = json.Unmarshal([]byte(content), &existingConfig); err != nil {
existingConfig = make(map[string]any)
}
} else {
existingConfig = make(map[string]any)
}
// 更新设置字段
if len(settings.RegistryMirrors) > 0 {
existingConfig["registry-mirrors"] = settings.RegistryMirrors
} else {
delete(existingConfig, "registry-mirrors")
}
if len(settings.InsecureRegistries) > 0 {
existingConfig["insecure-registries"] = settings.InsecureRegistries
} else {
delete(existingConfig, "insecure-registries")
}
if settings.LiveRestore {
existingConfig["live-restore"] = true
} else {
delete(existingConfig, "live-restore")
}
if settings.LogDriver != "" {
existingConfig["log-driver"] = settings.LogDriver
} else {
delete(existingConfig, "log-driver")
}
// 日志配置
if settings.LogOpts.MaxSize != "" || settings.LogOpts.MaxFile != "" {
logOpts := make(map[string]string)
if settings.LogOpts.MaxSize != "" {
logOpts["max-size"] = settings.LogOpts.MaxSize
}
if settings.LogOpts.MaxFile != "" {
logOpts["max-file"] = settings.LogOpts.MaxFile
}
existingConfig["log-opts"] = logOpts
} else {
delete(existingConfig, "log-opts")
}
// cgroup-driver
if settings.CgroupDriver != "" {
existingConfig["exec-opts"] = []string{"native.cgroupdriver=" + settings.CgroupDriver}
} else {
delete(existingConfig, "exec-opts")
}
if len(settings.Hosts) > 0 {
existingConfig["hosts"] = settings.Hosts
} else {
delete(existingConfig, "hosts")
}
if settings.DataRoot != "" {
existingConfig["data-root"] = settings.DataRoot
} else {
delete(existingConfig, "data-root")
}
if settings.StorageDriver != "" {
existingConfig["storage-driver"] = settings.StorageDriver
} else {
delete(existingConfig, "storage-driver")
}
if len(settings.DNS) > 0 {
existingConfig["dns"] = settings.DNS
} else {
delete(existingConfig, "dns")
}
// 防火墙后端
if settings.FirewallBackend != "" {
existingConfig["firewall-backend"] = settings.FirewallBackend
} else {
delete(existingConfig, "firewall-backend")
}
if settings.Iptables != nil {
existingConfig["iptables"] = *settings.Iptables
} else {
delete(existingConfig, "iptables")
}
if settings.Ip6tables != nil {
existingConfig["ip6tables"] = *settings.Ip6tables
} else {
delete(existingConfig, "ip6tables")
}
if settings.IpForward != nil {
existingConfig["ip-forward"] = *settings.IpForward
} else {
delete(existingConfig, "ip-forward")
}
if settings.IPv6 != nil {
existingConfig["ipv6"] = *settings.IPv6
} else {
delete(existingConfig, "ipv6")
}
if settings.Bip != "" {
existingConfig["bip"] = settings.Bip
} else {
delete(existingConfig, "bip")
}
// 序列化并写入文件
newContent, err := json.MarshalIndent(existingConfig, "", " ")
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = io.Write(configPath, string(newContent), 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
// 重启 Docker 服务
if err = systemctl.Restart("docker"); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}

View File

@@ -3,3 +3,34 @@ package docker
type UpdateConfig struct {
Config string `form:"config" json:"config" validate:"required"`
}
// LogOpts 日志配置选项
type LogOpts struct {
MaxSize string `json:"max-size,omitempty"` // 日志文件最大大小,如 "10m"
MaxFile string `json:"max-file,omitempty"` // 保存的日志文件份数,如 "3"
}
// Settings Docker daemon 设置
type Settings struct {
RegistryMirrors []string `json:"registry-mirrors,omitempty"` // 注册表镜像
InsecureRegistries []string `json:"insecure-registries,omitempty"` // 非安全镜像仓库
LiveRestore bool `json:"live-restore,omitempty"` // Live restore
LogDriver string `json:"log-driver,omitempty"` // 日志驱动
LogOpts LogOpts `json:"log-opts,omitempty"` // 日志配置选项
CgroupDriver string `json:"cgroup-driver,omitempty"` // cgroup 驱动(从 exec-opts 中提取)
Hosts []string `json:"hosts,omitempty"` // Socket 路径
DataRoot string `json:"data-root,omitempty"` // 数据目录
StorageDriver string `json:"storage-driver,omitempty"` // 存储驱动
DNS []string `json:"dns,omitempty"` // DNS 配置
FirewallBackend string `json:"firewall-backend,omitempty"` // 防火墙后端 (iptables/nftables)
Iptables *bool `json:"iptables,omitempty"` // iptables 规则
Ip6tables *bool `json:"ip6tables,omitempty"` // ip6tables 规则
IpForward *bool `json:"ip-forward,omitempty"` // IP 转发
IPv6 *bool `json:"ipv6,omitempty"` // IPv6 支持
Bip string `json:"bip,omitempty"` // 默认 bridge 网络 IP 段
}
// UpdateSettings 更新设置请求
type UpdateSettings struct {
Settings Settings `json:"settings" validate:"required"`
}

View File

@@ -0,0 +1,23 @@
package docker
// DaemonConfig Docker daemon.json 完整配置结构
type DaemonConfig struct {
RegistryMirrors []string `json:"registry-mirrors,omitempty"`
InsecureRegistries []string `json:"insecure-registries,omitempty"`
LiveRestore bool `json:"live-restore,omitempty"`
LogDriver string `json:"log-driver,omitempty"`
LogOpts map[string]string `json:"log-opts,omitempty"`
ExecOpts []string `json:"exec-opts,omitempty"`
Hosts []string `json:"hosts,omitempty"`
DataRoot string `json:"data-root,omitempty"`
StorageDriver string `json:"storage-driver,omitempty"`
DNS []string `json:"dns,omitempty"`
FirewallBackend string `json:"firewall-backend,omitempty"`
Iptables *bool `json:"iptables,omitempty"`
Ip6tables *bool `json:"ip6tables,omitempty"`
IpForward *bool `json:"ip-forward,omitempty"`
IPv6 *bool `json:"ipv6,omitempty"`
Bip string `json:"bip,omitempty"`
// 其他原有配置字段保留
Extra map[string]any `json:"-"`
}

View File

@@ -2,5 +2,8 @@ import { http } from '@/utils'
export default {
config: (): any => http.Get('/apps/docker/config'),
updateConfig: (config: string): any => http.Post('/apps/docker/config', { config })
updateConfig: (config: string): any => http.Post('/apps/docker/config', { config }),
settings: (): any => http.Get('/apps/docker/settings'),
updateSettings: (settings: any): any =>
http.Post('/apps/docker/settings', { settings })
}

View File

@@ -3,7 +3,6 @@ defineOptions({
name: 'apps-docker-index'
})
import { NButton } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import docker from '@/api/apps/docker'
@@ -18,11 +17,172 @@ const { data: config } = useRequest(docker.config, {
}
})
// 基本设置
const settingsLoading = ref(false)
const settings = ref<any>({
'registry-mirrors': [],
'insecure-registries': [],
'live-restore': false,
'log-driver': 'json-file',
'log-opts': {
'max-size': '',
'max-file': ''
},
'cgroup-driver': '',
hosts: [],
'data-root': '',
'storage-driver': '',
dns: [],
'firewall-backend': '',
'ip-forward': true,
ipv6: false,
bip: ''
})
// 镜像输入
const mirrorInput = ref('')
const insecureRegistryInput = ref('')
const dnsInput = ref('')
const hostInput = ref('')
// 日志驱动选项
const logDriverOptions = [
{ label: 'json-file', value: 'json-file' },
{ label: 'local', value: 'local' },
{ label: 'journald', value: 'journald' },
{ label: 'syslog', value: 'syslog' },
{ label: 'fluentd', value: 'fluentd' },
{ label: 'gelf', value: 'gelf' },
{ label: 'splunk', value: 'splunk' },
{ label: 'awslogs', value: 'awslogs' },
{ label: 'none', value: 'none' }
]
// cgroup 驱动选项
const cgroupDriverOptions = [
{ label: $gettext('Default'), value: '' },
{ label: 'systemd', value: 'systemd' },
{ label: 'cgroupfs', value: 'cgroupfs' }
]
// 存储驱动选项
const storageDriverOptions = [
{ label: $gettext('Default'), value: '' },
{ label: 'overlay2', value: 'overlay2' },
{ label: 'fuse-overlayfs', value: 'fuse-overlayfs' },
{ label: 'btrfs', value: 'btrfs' },
{ label: 'zfs', value: 'zfs' },
{ label: 'vfs', value: 'vfs' }
]
// 防火墙后端选项
const firewallBackendOptions = [
{ label: 'iptables (' + $gettext('Default') + ')', value: '' },
{ label: 'iptables', value: 'iptables' },
{ label: 'nftables (' + $gettext('Experimental') + ')', value: 'nftables' },
{ label: $gettext('None'), value: 'none' }
]
// 常用镜像源预设
const mirrorPresets = [
{ label: $gettext('China - Millisecond'), value: 'https://docker.1ms.run' },
{ label: $gettext('China - DaoCloud'), value: 'https://docker.m.daocloud.io' },
{
label: $gettext('China - Tencent (Internal only)'),
value: 'https://mirror.ccs.tencentyun.com'
}
]
// 获取设置
const fetchSettings = () => {
settingsLoading.value = true
useRequest(docker.settings())
.onSuccess((res) => {
settings.value = {
'registry-mirrors': res.data['registry-mirrors'] || [],
'insecure-registries': res.data['insecure-registries'] || [],
'live-restore': res.data['live-restore'] || false,
'log-driver': res.data['log-driver'] || 'json-file',
'log-opts': {
'max-size': res.data['log-opts']?.['max-size'] || '',
'max-file': res.data['log-opts']?.['max-file'] || ''
},
'cgroup-driver': res.data['cgroup-driver'] || '',
hosts: res.data.hosts || [],
'data-root': res.data['data-root'] || '',
'storage-driver': res.data['storage-driver'] || '',
dns: res.data.dns || [],
'firewall-backend': res.data['firewall-backend'] || '',
'ip-forward': res.data['ip-forward'] ?? true,
ipv6: res.data.ipv6 ?? false,
bip: res.data.bip || ''
}
})
.onComplete(() => {
settingsLoading.value = false
})
}
// 添加镜像
const addMirror = () => {
if (mirrorInput.value && !settings.value['registry-mirrors']?.includes(mirrorInput.value)) {
settings.value['registry-mirrors'] = [
...(settings.value['registry-mirrors'] || []),
mirrorInput.value
]
mirrorInput.value = ''
}
}
// 添加非安全镜像仓库
const addInsecureRegistry = () => {
if (
insecureRegistryInput.value &&
!settings.value['insecure-registries']?.includes(insecureRegistryInput.value)
) {
settings.value['insecure-registries'] = [
...(settings.value['insecure-registries'] || []),
insecureRegistryInput.value
]
insecureRegistryInput.value = ''
}
}
// 添加 DNS
const addDns = () => {
if (dnsInput.value && !settings.value.dns?.includes(dnsInput.value)) {
settings.value.dns = [...(settings.value.dns || []), dnsInput.value]
dnsInput.value = ''
}
}
// 添加 Host
const addHost = () => {
if (hostInput.value && !settings.value.hosts?.includes(hostInput.value)) {
settings.value.hosts = [...(settings.value.hosts || []), hostInput.value]
hostInput.value = ''
}
}
// 保存设置
const handleSaveSettings = () => {
useRequest(docker.updateSettings(settings.value)).onSuccess(() => {
window.$message.success($gettext('Saved successfully'))
})
}
const handleSaveConfig = () => {
useRequest(docker.updateConfig(config.value)).onSuccess(() => {
window.$message.success($gettext('Saved successfully'))
})
}
// 监听 tab 切换,加载设置
watch(currentTab, (tab) => {
if (tab === 'settings') {
fetchSettings()
}
})
</script>
<template>
@@ -31,7 +191,277 @@ const handleSaveConfig = () => {
<n-tab-pane name="status" :tab="$gettext('Running Status')">
<service-status service="docker" />
</n-tab-pane>
<n-tab-pane name="config" :tab="$gettext('Configuration')">
<n-tab-pane name="settings" :tab="$gettext('Basic Settings')">
<n-spin :show="settingsLoading">
<n-flex vertical :size="20">
<!-- 注册表镜像 -->
<n-card :title="$gettext('Registry Mirrors')">
<template #header-extra>
<n-popover trigger="click">
<template #trigger>
<n-button size="small" quaternary>
{{ $gettext('Presets') }}
</n-button>
</template>
<n-flex vertical>
<n-button
v-for="preset in mirrorPresets"
:key="preset.value"
size="small"
@click="
() => {
mirrorInput = preset.value
addMirror()
}
"
>
{{ preset.label }}
</n-button>
</n-flex>
</n-popover>
</template>
<n-flex vertical>
<n-alert type="info" :show-icon="false">
{{
$gettext(
'Configure registry mirrors to speed up image downloads. Domestic users can configure domestic mirrors.'
)
}}
</n-alert>
<n-input-group>
<n-input
v-model:value="mirrorInput"
:placeholder="
$gettext('Enter mirror address, e.g., https://registry.example.com')
"
@keydown.enter.prevent="addMirror"
/>
<n-button type="primary" @click="addMirror">{{ $gettext('Add') }}</n-button>
</n-input-group>
<n-dynamic-tags
:value="settings['registry-mirrors']"
@update:value="settings['registry-mirrors'] = $event"
/>
</n-flex>
</n-card>
<!-- 日志切割 -->
<n-card :title="$gettext('Log Configuration')">
<n-flex vertical>
<n-alert type="info" :show-icon="false">
{{
$gettext(
'Configure log driver and rotation settings. Setting max-size and max-file can prevent log files from growing indefinitely.'
)
}}
</n-alert>
<n-form label-placement="left" label-width="120">
<n-form-item :label="$gettext('Log Driver')">
<n-select
v-model:value="settings['log-driver']"
:options="logDriverOptions"
:placeholder="$gettext('Select log driver')"
style="width: 200px"
/>
</n-form-item>
<n-row :gutter="[24, 0]">
<n-col :span="12">
<n-form-item :label="$gettext('Max Size')">
<n-input
v-model:value="settings['log-opts']!['max-size']"
:placeholder="$gettext('e.g., 10m, 100m, 1g')"
/>
</n-form-item>
</n-col>
<n-col :span="12">
<n-form-item :label="$gettext('Max Files')">
<n-input
v-model:value="settings['log-opts']!['max-file']"
:placeholder="$gettext('e.g., 3, 5, 10')"
/>
</n-form-item>
</n-col>
</n-row>
</n-form>
</n-flex>
</n-card>
<!-- 运行时选项 -->
<n-card :title="$gettext('Runtime Options')">
<n-form label-placement="left" label-width="120">
<n-row :gutter="[24, 0]">
<n-col :span="12">
<n-form-item :label="$gettext('Live Restore')">
<n-switch v-model:value="settings['live-restore']" />
<span class="text-gray-400 ml-2">
{{ $gettext('Keep containers alive during daemon downtime') }}
</span>
</n-form-item>
</n-col>
<n-col :span="12">
<n-form-item :label="$gettext('Cgroup Driver')">
<n-select
v-model:value="settings['cgroup-driver']"
:options="cgroupDriverOptions"
:placeholder="$gettext('Select cgroup driver')"
style="width: 200px"
/>
</n-form-item>
</n-col>
</n-row>
<n-row :gutter="[24, 0]">
<n-col :span="12">
<n-form-item :label="$gettext('IPv6')">
<n-switch v-model:value="settings.ipv6" />
<span class="text-gray-400 ml-2">
{{ $gettext('Requires additional configuration.') }}
<n-button
text
tag="a"
href="https://docs.docker.com/engine/daemon/ipv6/"
target="_blank"
type="info"
>
{{ $gettext('Docs') }}
</n-button>
</span>
</n-form-item>
</n-col>
<n-col :span="12">
<n-form-item :label="$gettext('IP Forward')">
<n-switch v-model:value="settings['ip-forward']" />
<span class="text-gray-400 ml-2">
{{ $gettext('Enable IP forwarding') }}
</span>
</n-form-item>
</n-col>
</n-row>
</n-form>
</n-card>
<!-- 防火墙配置 -->
<n-card :title="$gettext('Firewall Configuration')">
<n-flex vertical>
<n-alert type="info" :show-icon="false">
{{
$gettext(
'Configure Docker firewall backend. nftables is experimental and does not support Swarm mode.'
)
}}
</n-alert>
<n-form label-placement="left" label-width="140">
<n-form-item :label="$gettext('Firewall Backend')">
<n-select
v-model:value="settings['firewall-backend']"
:options="firewallBackendOptions"
:placeholder="$gettext('Select firewall backend')"
style="width: 280px"
/>
</n-form-item>
</n-form>
</n-flex>
</n-card>
<!-- 存储与路径 -->
<n-card :title="$gettext('Storage & Paths')">
<n-form label-placement="left" label-width="120">
<n-form-item :label="$gettext('Storage Driver')">
<n-select
v-model:value="settings['storage-driver']"
:options="storageDriverOptions"
:placeholder="$gettext('Select storage driver')"
style="width: 200px"
/>
</n-form-item>
<n-form-item :label="$gettext('Data Root')">
<n-input
v-model:value="settings['data-root']"
:placeholder="$gettext('Docker data directory, default is /var/lib/docker')"
/>
</n-form-item>
<n-form-item :label="$gettext('Socket/Hosts')">
<n-flex vertical class="w-full">
<n-input-group>
<n-input
v-model:value="hostInput"
:placeholder="
$gettext('e.g., unix:///var/run/docker.sock, tcp://0.0.0.0:2375')
"
@keydown.enter.prevent="addHost"
/>
<n-button type="primary" @click="addHost">{{ $gettext('Add') }}</n-button>
</n-input-group>
<n-dynamic-tags
:value="settings.hosts"
@update:value="settings.hosts = $event"
/>
</n-flex>
</n-form-item>
</n-form>
</n-card>
<!-- 网络配置 -->
<n-card :title="$gettext('Network Configuration')">
<n-form label-placement="left" label-width="120">
<n-form-item :label="$gettext('Bridge IP')">
<n-input
v-model:value="settings.bip"
:placeholder="$gettext('Default bridge network IP range, e.g., 172.17.0.1/16')"
/>
</n-form-item>
<n-form-item :label="$gettext('DNS Servers')">
<n-flex vertical class="w-full">
<n-input-group>
<n-input
v-model:value="dnsInput"
:placeholder="$gettext('e.g., 8.8.8.8, 114.114.114.114')"
@keydown.enter.prevent="addDns"
/>
<n-button type="primary" @click="addDns">{{ $gettext('Add') }}</n-button>
</n-input-group>
<n-dynamic-tags :value="settings.dns" @update:value="settings.dns = $event" />
</n-flex>
</n-form-item>
</n-form>
</n-card>
<!-- 非安全镜像仓库 -->
<n-card :title="$gettext('Insecure Registries')">
<n-flex vertical>
<n-alert type="warning" :show-icon="false">
{{
$gettext(
'Insecure registries allow Docker to communicate with registries using HTTP or self-signed certificates. Use with caution.'
)
}}
</n-alert>
<n-input-group>
<n-input
v-model:value="insecureRegistryInput"
:placeholder="$gettext('e.g., 192.168.1.100:5000')"
@keydown.enter.prevent="addInsecureRegistry"
/>
<n-button type="primary" @click="addInsecureRegistry">
{{ $gettext('Add') }}
</n-button>
</n-input-group>
<n-dynamic-tags
:value="settings['insecure-registries']"
@update:value="settings['insecure-registries'] = $event"
/>
</n-flex>
</n-card>
<!-- 保存按钮 -->
<n-flex>
<n-button type="primary" @click="handleSaveSettings">
{{ $gettext('Save') }}
</n-button>
</n-flex>
</n-flex>
</n-spin>
</n-tab-pane>
<n-tab-pane name="config" :tab="$gettext('Configuration File')">
<n-flex vertical>
<n-alert type="warning">
{{ $gettext('This modifies the Docker configuration file (/etc/docker/daemon.json)') }}