feat: 容器适配 IPv6 (#3016)

Refs #2990 #2971
pull/3024/head
ssongliu 2023-11-21 22:14:07 +08:00 committed by GitHub
parent 055216604e
commit a5a707b923
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 938 additions and 61 deletions

View File

@ -83,7 +83,7 @@ func (b *BaseApi) UpdateDaemonJson(c *gin.Context) {
// @Param request body dto.LogOption true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /containers/daemonjson/update [post]
// @Router /containers/logoption/update [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 docker daemon.json 日志配置","formatEN":"Updated the docker daemon.json log option"}
func (b *BaseApi) UpdateLogOption(c *gin.Context) {
var req dto.LogOption
@ -99,6 +99,29 @@ func (b *BaseApi) UpdateLogOption(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags Container Docker
// @Summary Update docker daemon.json ipv6 option
// @Description 修改 docker ipv6 配置
// @Accept json
// @Param request body dto.LogOption true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /containers/ipv6option/update [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 docker daemon.json ipv6 配置","formatEN":"Updated the docker daemon.json ipv6 option"}
func (b *BaseApi) UpdateIpv6Option(c *gin.Context) {
var req dto.Ipv6Option
if err := helper.CheckBind(&req, c); err != nil {
return
}
if err := dockerService.UpdateIpv6Option(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Container Docker
// @Summary Update docker daemon.json by upload file
// @Description 上传替换 docker 配置文件

View File

@ -135,13 +135,21 @@ type Network struct {
Attachable bool `json:"attachable"`
}
type NetworkCreate struct {
Name string `json:"name" validate:"required"`
Driver string `json:"driver" validate:"required"`
Options []string `json:"options"`
Subnet string `json:"subnet"`
Gateway string `json:"gateway"`
IPRange string `json:"ipRange"`
Labels []string `json:"labels"`
Name string `json:"name" validate:"required"`
Driver string `json:"driver" validate:"required"`
Options []string `json:"options"`
Ipv4 bool `json:"ipv4"`
Subnet string `json:"subnet"`
Gateway string `json:"gateway"`
IPRange string `json:"ipRange"`
AuxAddress []SettingUpdate `json:"auxAddress"`
Ipv6 bool `json:"ipv6"`
SubnetV6 string `json:"subnetV6"`
GatewayV6 string `json:"gatewayV6"`
IPRangeV6 string `json:"ipRangeV6"`
AuxAddressV6 []SettingUpdate `json:"auxAddressV6"`
Labels []string `json:"labels"`
}
type Volume struct {

View File

@ -14,6 +14,11 @@ type DaemonJsonConf struct {
IPTables bool `json:"iptables"`
CgroupDriver string `json:"cgroupDriver"`
Ipv6 bool `json:"ipv6"`
FixedCidrV6 string `json:"fixedCidrV6"`
Ip6Tables bool `json:"ip6Tables"`
Experimental bool `json:"experimental"`
LogMaxSize string `json:"logMaxSize"`
LogMaxFile string `json:"logMaxFile"`
}
@ -23,6 +28,12 @@ type LogOption struct {
LogMaxFile string `json:"logMaxFile"`
}
type Ipv6Option struct {
FixedCidrV6 string `json:"fixedCidrV6"`
Ip6Tables bool `json:"ip6Tables" validate:"required"`
Experimental bool `json:"experimental"`
}
type DockerOperation struct {
Operation string `json:"operation" validate:"required,oneof=start restart stop"`
}

View File

@ -116,29 +116,57 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
return err
}
var (
ipam network.IPAMConfig
hasConf bool
ipams []network.IPAMConfig
enableV6 bool
)
if len(req.Subnet) != 0 {
ipam.Subnet = req.Subnet
hasConf = true
if req.Ipv4 {
var itemIpam network.IPAMConfig
if len(req.AuxAddress) != 0 {
itemIpam.AuxAddress = make(map[string]string)
}
if len(req.Subnet) != 0 {
itemIpam.Subnet = req.Subnet
}
if len(req.Gateway) != 0 {
itemIpam.Gateway = req.Gateway
}
if len(req.IPRange) != 0 {
itemIpam.IPRange = req.IPRange
}
for _, addr := range req.AuxAddress {
itemIpam.AuxAddress[addr.Key] = addr.Value
}
ipams = append(ipams, itemIpam)
}
if len(req.Gateway) != 0 {
ipam.Gateway = req.Gateway
hasConf = true
}
if len(req.IPRange) != 0 {
ipam.IPRange = req.IPRange
hasConf = true
if req.Ipv6 {
enableV6 = true
var itemIpam network.IPAMConfig
if len(req.AuxAddress) != 0 {
itemIpam.AuxAddress = make(map[string]string)
}
if len(req.SubnetV6) != 0 {
itemIpam.Subnet = req.SubnetV6
}
if len(req.GatewayV6) != 0 {
itemIpam.Gateway = req.GatewayV6
}
if len(req.IPRangeV6) != 0 {
itemIpam.IPRange = req.IPRangeV6
}
for _, addr := range req.AuxAddressV6 {
itemIpam.AuxAddress[addr.Key] = addr.Value
}
ipams = append(ipams, itemIpam)
}
options := types.NetworkCreate{
Driver: req.Driver,
Options: stringsToMap(req.Options),
Labels: stringsToMap(req.Labels),
EnableIPv6: enableV6,
Driver: req.Driver,
Options: stringsToMap(req.Options),
Labels: stringsToMap(req.Labels),
}
if hasConf {
options.IPAM = &network.IPAM{Config: []network.IPAMConfig{ipam}}
if len(ipams) != 0 {
options.IPAM = &network.IPAM{Config: ipams}
}
if _, err := client.NetworkCreate(context.TODO(), req.Name, options); err != nil {
return err

View File

@ -21,6 +21,7 @@ type DockerService struct{}
type IDockerService interface {
UpdateConf(req dto.SettingUpdate) error
UpdateLogOption(req dto.LogOption) error
UpdateIpv6Option(req dto.Ipv6Option) error
UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error
LoadDockerStatus() string
LoadDockerConf() *dto.DaemonJsonConf
@ -32,13 +33,17 @@ func NewIDockerService() IDockerService {
}
type daemonJsonItem struct {
Status string `json:"status"`
Mirrors []string `json:"registry-mirrors"`
Registries []string `json:"insecure-registries"`
LiveRestore bool `json:"live-restore"`
IPTables bool `json:"iptables"`
ExecOpts []string `json:"exec-opts"`
LogOption logOption `json:"log-opts"`
Status string `json:"status"`
Mirrors []string `json:"registry-mirrors"`
Registries []string `json:"insecure-registries"`
LiveRestore bool `json:"live-restore"`
Ipv6 bool `json:"ipv6"`
FixedCidrV6 string `json:"fixed-cidr-v6"`
Ip6Tables bool `json:"ip6tables"`
Experimental bool `json:"experimental"`
IPTables bool `json:"iptables"`
ExecOpts []string `json:"exec-opts"`
LogOption logOption `json:"log-opts"`
}
type logOption struct {
LogMaxSize string `json:"max-size"`
@ -110,6 +115,10 @@ func (u *DockerService) LoadDockerConf() *dto.DaemonJsonConf {
break
}
}
data.Ipv6 = conf.Ipv6
data.FixedCidrV6 = conf.FixedCidrV6
data.Ip6Tables = conf.Ip6Tables
data.Experimental = conf.Experimental
data.LogMaxSize = conf.LogOption.LogMaxSize
data.LogMaxFile = conf.LogOption.LogMaxFile
data.Mirrors = conf.Mirrors
@ -149,6 +158,13 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate) error {
} else {
daemonMap["registry-mirrors"] = strings.Split(req.Value, ",")
}
case "Ipv6":
if req.Value == "disable" {
delete(daemonMap, "ipv6")
delete(daemonMap, "fixed-cidr-v6")
delete(daemonMap, "ip6tables")
delete(daemonMap, "experimental")
}
case "LogOption":
if req.Value == "disable" {
delete(daemonMap, "log-opts")
@ -237,6 +253,48 @@ func (u *DockerService) UpdateLogOption(req dto.LogOption) error {
return nil
}
func (u *DockerService) UpdateIpv6Option(req dto.Ipv6Option) error {
if _, err := os.Stat(constant.DaemonJsonPath); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(path.Dir(constant.DaemonJsonPath), os.ModePerm); err != nil {
return err
}
_, _ = os.Create(constant.DaemonJsonPath)
}
file, err := os.ReadFile(constant.DaemonJsonPath)
if err != nil {
return err
}
daemonMap := make(map[string]interface{})
_ = json.Unmarshal(file, &daemonMap)
daemonMap["ipv6"] = true
daemonMap["fixed-cidr-v6"] = req.FixedCidrV6
if req.Ip6Tables {
daemonMap["ip6tables"] = req.Ip6Tables
}
if req.Experimental {
daemonMap["experimental"] = req.Experimental
}
if len(daemonMap) == 0 {
_ = os.Remove(constant.DaemonJsonPath)
return nil
}
newJson, err := json.MarshalIndent(daemonMap, "", "\t")
if err != nil {
return err
}
if err := os.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil {
return err
}
stdout, err := cmd.Exec("systemctl restart docker")
if err != nil {
return errors.New(string(stdout))
}
return nil
}
func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
if len(req.File) == 0 {
_ = os.Remove(constant.DaemonJsonPath)

View File

@ -79,6 +79,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("/docker/operate", baseApi.OperateDocker)
baRouter.POST("/daemonjson/update", baseApi.UpdateDaemonJson)
baRouter.POST("/logoption/update", baseApi.UpdateLogOption)
baRouter.POST("/ipv6option/update", baseApi.UpdateIpv6Option)
baRouter.POST("/daemonjson/update/byfile", baseApi.UpdateDaemonJsonByFile)
}
}

View File

@ -1414,14 +1414,14 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "修改 docker 日志配置",
"description": "修改 docker 配置信息",
"consumes": [
"application/json"
],
"tags": [
"Container Docker"
],
"summary": "Update docker daemon.json log option",
"summary": "Update docker daemon.json",
"parameters": [
{
"description": "request",
@ -1429,7 +1429,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.LogOption"
"$ref": "#/definitions/dto.SettingUpdate"
}
}
],
@ -1440,9 +1440,12 @@ const docTemplate = `{
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "Updated the docker daemon.json log option",
"formatZH": "更新 docker daemon.json 日志配置",
"bodyKeys": [
"key",
"value"
],
"formatEN": "Updated the docker daemon.json configuration [key]=\u003e[value]",
"formatZH": "更新 docker daemon.json 配置 [key]=\u003e[value]",
"paramKeys": []
}
}
@ -2029,6 +2032,46 @@ const docTemplate = `{
}
}
},
"/containers/ipv6option/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 docker ipv6 配置",
"consumes": [
"application/json"
],
"tags": [
"Container Docker"
],
"summary": "Update docker daemon.json ipv6 option",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.LogOption"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "Updated the docker daemon.json ipv6 option",
"formatZH": "更新 docker daemon.json ipv6 配置",
"paramKeys": []
}
}
},
"/containers/limit": {
"get": {
"security": [
@ -2128,6 +2171,46 @@ const docTemplate = `{
}
}
},
"/containers/logoption/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 docker 日志配置",
"consumes": [
"application/json"
],
"tags": [
"Container Docker"
],
"summary": "Update docker daemon.json log option",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.LogOption"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "Updated the docker daemon.json log option",
"formatZH": "更新 docker daemon.json 日志配置",
"paramKeys": []
}
}
},
"/containers/network": {
"get": {
"security": [
@ -14023,15 +14106,27 @@ const docTemplate = `{
"cgroupDriver": {
"type": "string"
},
"experimental": {
"type": "boolean"
},
"fixedCidrV6": {
"type": "string"
},
"insecureRegistries": {
"type": "array",
"items": {
"type": "string"
}
},
"ip6Tables": {
"type": "boolean"
},
"iptables": {
"type": "boolean"
},
"ipv6": {
"type": "boolean"
},
"isSwarm": {
"type": "boolean"
},
@ -15530,15 +15625,39 @@ const docTemplate = `{
"name"
],
"properties": {
"auxAddress": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.SettingUpdate"
}
},
"auxAddressV6": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.SettingUpdate"
}
},
"driver": {
"type": "string"
},
"gateway": {
"type": "string"
},
"gatewayV6": {
"type": "string"
},
"ipRange": {
"type": "string"
},
"ipRangeV6": {
"type": "string"
},
"ipv4": {
"type": "boolean"
},
"ipv6": {
"type": "boolean"
},
"labels": {
"type": "array",
"items": {
@ -15556,6 +15675,9 @@ const docTemplate = `{
},
"subnet": {
"type": "string"
},
"subnetV6": {
"type": "string"
}
}
},

View File

@ -1407,14 +1407,14 @@
"ApiKeyAuth": []
}
],
"description": "修改 docker 日志配置",
"description": "修改 docker 配置信息",
"consumes": [
"application/json"
],
"tags": [
"Container Docker"
],
"summary": "Update docker daemon.json log option",
"summary": "Update docker daemon.json",
"parameters": [
{
"description": "request",
@ -1422,7 +1422,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.LogOption"
"$ref": "#/definitions/dto.SettingUpdate"
}
}
],
@ -1433,9 +1433,12 @@
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "Updated the docker daemon.json log option",
"formatZH": "更新 docker daemon.json 日志配置",
"bodyKeys": [
"key",
"value"
],
"formatEN": "Updated the docker daemon.json configuration [key]=\u003e[value]",
"formatZH": "更新 docker daemon.json 配置 [key]=\u003e[value]",
"paramKeys": []
}
}
@ -2022,6 +2025,46 @@
}
}
},
"/containers/ipv6option/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 docker ipv6 配置",
"consumes": [
"application/json"
],
"tags": [
"Container Docker"
],
"summary": "Update docker daemon.json ipv6 option",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.LogOption"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "Updated the docker daemon.json ipv6 option",
"formatZH": "更新 docker daemon.json ipv6 配置",
"paramKeys": []
}
}
},
"/containers/limit": {
"get": {
"security": [
@ -2121,6 +2164,46 @@
}
}
},
"/containers/logoption/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 docker 日志配置",
"consumes": [
"application/json"
],
"tags": [
"Container Docker"
],
"summary": "Update docker daemon.json log option",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.LogOption"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "Updated the docker daemon.json log option",
"formatZH": "更新 docker daemon.json 日志配置",
"paramKeys": []
}
}
},
"/containers/network": {
"get": {
"security": [
@ -14016,15 +14099,27 @@
"cgroupDriver": {
"type": "string"
},
"experimental": {
"type": "boolean"
},
"fixedCidrV6": {
"type": "string"
},
"insecureRegistries": {
"type": "array",
"items": {
"type": "string"
}
},
"ip6Tables": {
"type": "boolean"
},
"iptables": {
"type": "boolean"
},
"ipv6": {
"type": "boolean"
},
"isSwarm": {
"type": "boolean"
},
@ -15523,15 +15618,39 @@
"name"
],
"properties": {
"auxAddress": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.SettingUpdate"
}
},
"auxAddressV6": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.SettingUpdate"
}
},
"driver": {
"type": "string"
},
"gateway": {
"type": "string"
},
"gatewayV6": {
"type": "string"
},
"ipRange": {
"type": "string"
},
"ipRangeV6": {
"type": "string"
},
"ipv4": {
"type": "boolean"
},
"ipv6": {
"type": "boolean"
},
"labels": {
"type": "array",
"items": {
@ -15549,6 +15668,9 @@
},
"subnet": {
"type": "string"
},
"subnetV6": {
"type": "string"
}
}
},

View File

@ -678,12 +678,20 @@ definitions:
properties:
cgroupDriver:
type: string
experimental:
type: boolean
fixedCidrV6:
type: string
insecureRegistries:
items:
type: string
type: array
ip6Tables:
type: boolean
iptables:
type: boolean
ipv6:
type: boolean
isSwarm:
type: boolean
liveRestore:
@ -1694,12 +1702,28 @@ definitions:
type: object
dto.NetworkCreate:
properties:
auxAddress:
items:
$ref: '#/definitions/dto.SettingUpdate'
type: array
auxAddressV6:
items:
$ref: '#/definitions/dto.SettingUpdate'
type: array
driver:
type: string
gateway:
type: string
gatewayV6:
type: string
ipRange:
type: string
ipRangeV6:
type: string
ipv4:
type: boolean
ipv6:
type: boolean
labels:
items:
type: string
@ -1712,6 +1736,8 @@ definitions:
type: array
subnet:
type: string
subnetV6:
type: string
required:
- driver
- name
@ -5426,27 +5452,29 @@ paths:
post:
consumes:
- application/json
description: 修改 docker 日志配置
description: 修改 docker 配置信息
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.LogOption'
$ref: '#/definitions/dto.SettingUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update docker daemon.json log option
summary: Update docker daemon.json
tags:
- Container Docker
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: Updated the docker daemon.json log option
formatZH: 更新 docker daemon.json 日志配置
bodyKeys:
- key
- value
formatEN: Updated the docker daemon.json configuration [key]=>[value]
formatZH: 更新 docker daemon.json 配置 [key]=>[value]
paramKeys: []
/containers/daemonjson/update/byfile:
post:
@ -5820,6 +5848,32 @@ paths:
summary: Container inspect
tags:
- Container
/containers/ipv6option/update:
post:
consumes:
- application/json
description: 修改 docker ipv6 配置
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.LogOption'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update docker daemon.json ipv6 option
tags:
- Container Docker
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: Updated the docker daemon.json ipv6 option
formatZH: 更新 docker daemon.json ipv6 配置
paramKeys: []
/containers/limit:
get:
description: 获取容器限制
@ -5879,6 +5933,32 @@ paths:
summary: Load container log
tags:
- Container
/containers/logoption/update:
post:
consumes:
- application/json
description: 修改 docker 日志配置
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.LogOption'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update docker daemon.json log option
tags:
- Container Docker
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: Updated the docker daemon.json log option
formatZH: 更新 docker daemon.json 日志配置
paramKeys: []
/containers/network:
get:
consumes:

View File

@ -297,6 +297,12 @@ export namespace Container {
liveRestore: boolean;
iptables: boolean;
cgroupDriver: string;
ipv6: boolean;
fixedCidrV6: string;
ip6Tables: boolean;
experimental: boolean;
logMaxSize: string;
logMaxFile: string;
}

View File

@ -179,6 +179,13 @@ export const updateDaemonJson = (key: string, value: string) => {
export const updateLogOption = (maxSize: string, maxFile: string) => {
return http.post(`/containers/logoption/update`, { logMaxSize: maxSize, logMaxFile: maxFile }, TimeoutEnum.T_60S);
};
export const updateIpv6Option = (fixedCidrV6: string, ip6Tables: boolean, experimental: boolean) => {
return http.post(
`/containers/ipv6option/update`,
{ fixedCidrV6: fixedCidrV6, ip6Tables: ip6Tables, experimental: experimental },
TimeoutEnum.T_60S,
);
};
export const updateDaemonJsonByfile = (params: Container.DaemonJsonUpdateByFile) => {
return http.post(`/containers/daemonjson/update/byfile`, params);
};

View File

@ -190,6 +190,7 @@ const message = {
leechExts: 'Only support letters, numbers and,',
paramSimple: 'Support lowercase letters and numbers, length 1-128',
filePermission: 'File Permission Error',
formatErr: 'Format error, please check and retry',
},
res: {
paramError: 'The request failed, please try again later!',
@ -658,6 +659,7 @@ const message = {
subnet: 'Subnet',
scope: 'IP Scope',
gateway: 'Gateway',
auxAddress: 'Exclude IP',
volume: 'Volume',
volumeDir: 'Volume dir',
@ -716,6 +718,12 @@ const message = {
'The acceleration URL is preferred to perform operations. If this parameter is set to empty, mirror acceleration is disabled.',
mirrorsHelper2: 'For details, see the official documents, ',
registries: 'Insecure registries',
ipv6Helper:
'When enabling IPv6, you need to add an IPv6 container network. Refer to the official documentation for specific configuration steps.',
ipv6CidrHelper: 'IPv6 address pool range for containers',
ipv6TablesHelper: 'Automatic configuration of Docker IPv6 for iptables rules',
experimentalHelper:
'Enabling ip6tables requires this configuration to be turned on; otherwise, ip6tables will be ignored',
cutLog: 'Log option',
cutLogHelper1: 'The current configuration will only affect newly created containers.',
cutLogHelper2: 'Existing containers need to be recreated for the configuration to take effect.',

View File

@ -189,6 +189,7 @@ const message = {
leechExts: ',',
paramSimple: ', 1-128',
filePermission: '',
formatErr: '',
},
res: {
paramError: ',!',
@ -642,6 +643,7 @@ const message = {
subnet: '',
scope: 'IP ',
gateway: '',
auxAddress: ' IP',
volume: '',
volumeDir: '',
@ -692,6 +694,10 @@ const message = {
mirrorsHelper: '使 URL ',
mirrorsHelper2: '',
registries: '',
ipv6Helper: ' IPv6 IPv6 ',
ipv6CidrHelper: ' IPv6 ',
ipv6TablesHelper: 'Docker IPv6 iptables ',
experimentalHelper: ' ip6tables ip6tables ',
cutLog: '',
cutLogHelper1: '',
cutLogHelper2: '使',

View File

@ -189,6 +189,7 @@ const message = {
leechExts: ',',
paramSimple: ',1-128',
filePermission: '',
formatErr: '',
},
res: {
paramError: ',!',
@ -643,6 +644,7 @@ const message = {
subnet: '',
scope: 'IP ',
gateway: '',
auxAddress: ' IP',
volume: '',
volumeDir: '',
@ -693,6 +695,10 @@ const message = {
mirrorsHelper: '使 URL ',
mirrorsHelper2: '',
registries: '',
ipv6Helper: ' IPv6 IPv6 ',
ipv6CidrHelper: ' IPv6 ',
ipv6TablesHelper: 'Docker IPv6 iptables ',
experimentalHelper: ' ip6tables ip6tables ',
cutLog: '',
cutLogHelper1: '',
cutLogHelper2: '使',

View File

@ -247,6 +247,33 @@ export function checkIpV4V6(value: string): boolean {
}
}
export function checkIpV6(value: string): boolean {
if (value === '' || typeof value === 'undefined' || value == null) {
return true;
} else {
const IPv4SegmentFormat = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])';
const IPv4AddressFormat = `(${IPv4SegmentFormat}[.]){3}${IPv4SegmentFormat}`;
const IPv6SegmentFormat = '(?:[0-9a-fA-F]{1,4})';
const IPv6AddressRegExp = new RegExp(
'^(' +
`(?:${IPv6SegmentFormat}:){7}(?:${IPv6SegmentFormat}|:)|` +
`(?:${IPv6SegmentFormat}:){6}(?:${IPv4AddressFormat}|:${IPv6SegmentFormat}|:)|` +
`(?:${IPv6SegmentFormat}:){5}(?::${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,2}|:)|` +
`(?:${IPv6SegmentFormat}:){4}(?:(:${IPv6SegmentFormat}){0,1}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,3}|:)|` +
`(?:${IPv6SegmentFormat}:){3}(?:(:${IPv6SegmentFormat}){0,2}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,4}|:)|` +
`(?:${IPv6SegmentFormat}:){2}(?:(:${IPv6SegmentFormat}){0,3}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,5}|:)|` +
`(?:${IPv6SegmentFormat}:){1}(?:(:${IPv6SegmentFormat}){0,4}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,6}|:)|` +
`(?::((?::${IPv6SegmentFormat}){0,5}:${IPv4AddressFormat}|(?::${IPv6SegmentFormat}){1,7}|:))` +
')(%[0-9a-zA-Z-.:]{1,})?$',
);
if (!IPv6AddressRegExp.test(value) && value !== '') {
return true;
} else {
return false;
}
}
}
export function checkCidr(value: string): boolean {
if (value === '') {
return true;

View File

@ -1,5 +1,5 @@
<template>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<el-drawer v-model="drawerVisible" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="$t('container.createNetwork')" :back="handleClose" />
</template>
@ -17,6 +17,99 @@
<el-option label="overlay" value="overlay" />
</el-select>
</el-form-item>
<el-checkbox v-model="form.ipv4">IPv4</el-checkbox>
<div v-if="form.ipv4">
<el-row type="flex" justify="center" :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('container.subnet')" prop="subnet">
<el-input placeholder="172.16.10.0/24" clearable v-model.trim="form.subnet" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('container.gateway')" prop="gateway">
<el-input placeholder="172.16.10.12" clearable v-model.trim="form.gateway" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('container.scope')" prop="scope">
<el-input placeholder="172.16.10.0/16" clearable v-model.trim="form.scope" />
</el-form-item>
</el-col>
<el-col :span="12"></el-col>
</el-row>
<el-form-item :label="$t('container.auxAddress')" prop="scopeV6">
<el-table :data="form.auxAddress" v-if="form.auxAddress.length !== 0">
<el-table-column :label="$t('container.label')" min-width="100">
<template #default="{ row }">
<el-input placeholder="my-router" v-model="row.key" />
</template>
</el-table-column>
<el-table-column label="IP" min-width="150">
<template #default="{ row }">
<el-input placeholder="172.16.10.13" v-model="row.value" />
</template>
</el-table-column>
<el-table-column min-width="40">
<template #default="scope">
<el-button link type="primary" @click="handleV4Delete(scope.$index)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-button class="mt-2" @click="handleV4Add()">
{{ $t('commons.button.add') }}
</el-button>
</el-form-item>
</div>
<el-checkbox class="mb-4" v-model="form.ipv6">IPv6</el-checkbox>
<div v-if="form.ipv6">
<el-row type="flex" justify="center" :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('container.subnet')" prop="subnetV6">
<el-input placeholder="2408:400e::/48" clearable v-model.trim="form.subnetV6" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('container.gateway')" prop="gatewayV6">
<el-input placeholder="2408:400e::1" clearable v-model.trim="form.gatewayV6" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('container.scope')" prop="scopeV6">
<el-input placeholder="2408:400e::/64" clearable v-model.trim="form.scopeV6" />
</el-form-item>
</el-col>
<el-col :span="12"></el-col>
</el-row>
<el-form-item :label="$t('container.auxAddress')" prop="scopeV6">
<el-table :data="form.auxAddressV6" v-if="form.auxAddressV6.length !== 0">
<el-table-column :label="$t('container.label')" min-width="100">
<template #default="{ row }">
<el-input placeholder="my-router" v-model="row.key" />
</template>
</el-table-column>
<el-table-column label="IP" min-width="150">
<template #default="{ row }">
<el-input placeholder="2408:400e::3" v-model="row.value" />
</template>
</el-table-column>
<el-table-column min-width="40">
<template #default="scope">
<el-button link type="primary" @click="handleV6Delete(scope.$index)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-button class="mt-2" @click="handleV6Add()">
{{ $t('commons.button.add') }}
</el-button>
</el-form-item>
</div>
<el-form-item :label="$t('container.option')" prop="optionStr">
<el-input
type="textarea"
@ -25,15 +118,6 @@
v-model="form.optionStr"
/>
</el-form-item>
<el-form-item :label="$t('container.subnet')" prop="subnet">
<el-input clearable v-model.trim="form.subnet" />
</el-form-item>
<el-form-item :label="$t('container.gateway')" prop="gateway">
<el-input clearable v-model.trim="form.gateway" />
</el-form-item>
<el-form-item :label="$t('container.scope')" prop="scope">
<el-input clearable v-model.trim="form.scope" />
</el-form-item>
<el-form-item :label="$t('container.tag')" prop="labelStr">
<el-input
type="textarea"
@ -66,6 +150,7 @@ import { ElForm } from 'element-plus';
import { createNetwork } from '@/api/modules/container';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { checkIpV6 } from '@/utils/util';
const loading = ref(false);
@ -77,9 +162,16 @@ const form = reactive({
optionStr: '',
options: [] as Array<string>,
driver: '',
ipv4: true,
subnet: '',
gateway: '',
scope: '',
auxAddress: [],
ipv6: false,
subnetV6: '',
gatewayV6: '',
scopeV6: '',
auxAddressV6: [],
});
const acceptParams = (): void => {
@ -88,10 +180,17 @@ const acceptParams = (): void => {
form.labels = [];
form.optionStr = '';
form.options = [];
form.driver = '';
form.driver = 'bridge';
form.ipv4 = true;
form.subnet = '';
form.gateway = '';
form.scope = '';
form.auxAddress = [];
form.ipv6 = false;
form.subnetV6 = '';
form.gatewayV6 = '';
form.scopeV6 = '';
form.auxAddressV6 = [];
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
@ -103,8 +202,65 @@ const handleClose = () => {
const rules = reactive({
name: [Rules.requiredInput],
driver: [Rules.requiredSelect],
subnet: [{ validator: checkCidr, trigger: 'blur' }],
gateway: [Rules.ip],
scope: [{ validator: checkCidr, trigger: 'blur' }],
subnetV6: [{ validator: checkFixedCidrV6, trigger: 'blur' }],
gatewayV6: [Rules.ipV6],
scopeV6: [{ validator: checkFixedCidrV6, trigger: 'blur' }],
});
function checkCidr(rule: any, value: any, callback: any) {
if (value === '') {
callback();
}
const reg =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
if (!reg.test(value)) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
callback();
}
function checkFixedCidrV6(rule: any, value: any, callback: any) {
if (value === '') {
callback();
}
if (!form.subnetV6 || form.subnetV6.indexOf('/') === -1) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
if (checkIpV6(form.subnetV6.split('/')[0])) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
const reg = /^(?:[1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/;
if (!reg.test(form.subnetV6.split('/')[1])) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
callback();
}
const handleV4Add = () => {
let item = {
key: '',
value: '',
};
form.auxAddress.push(item);
};
const handleV4Delete = (index: number) => {
form.auxAddress.splice(index, 1);
};
const handleV6Add = () => {
let item = {
key: '',
value: '',
};
form.auxAddressV6.push(item);
};
const handleV6Delete = (index: number) => {
form.auxAddressV6.splice(index, 1);
};
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const onSubmit = async (formEl: FormInstance | undefined) => {

View File

@ -100,6 +100,19 @@
</el-input>
</el-form-item>
<el-form-item label="ipv6" prop="ipv6">
<el-switch v-model="form.ipv6" @change="handleIPv6"></el-switch>
<span class="input-help"></span>
<div v-if="ipv6OptionShow">
<el-tag>{{ $t('container.subnet') }}: {{ form.fixedCidrV6 }}</el-tag>
<div>
<el-button @click="handleIPv6" type="primary" link>
{{ $t('commons.button.view') }}
</el-button>
</div>
</div>
</el-form-item>
<el-form-item :label="$t('container.cutLog')" prop="hasLogOption">
<el-switch v-model="form.logOptionShow" @change="handleLogOption"></el-switch>
<span class="input-help"></span>
@ -206,6 +219,8 @@
<Mirror ref="mirrorRef" @search="search" />
<Registry ref="registriesRef" @search="search" />
<LogOption ref="logOptionRef" @search="search" />
<Ipv6Option ref="ipv6OptionRef" @search="search" />
<ConfirmDialog ref="confirmDialogRefIpv6" @confirm="onSaveIPv6" @cancel="search" />
<ConfirmDialog ref="confirmDialogRefIptable" @confirm="onSubmitOpenIPtable" @cancel="search" />
<ConfirmDialog ref="confirmDialogRefLog" @confirm="onSubmitSaveLog" @cancel="search" />
<ConfirmDialog ref="confirmDialogRefLive" @confirm="onSubmitSaveLive" @cancel="search" />
@ -224,6 +239,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
import Mirror from '@/views/container/setting/mirror/index.vue';
import Registry from '@/views/container/setting/registry/index.vue';
import LogOption from '@/views/container/setting/log/index.vue';
import Ipv6Option from '@/views/container/setting/ipv6/index.vue';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import i18n from '@/lang';
import {
@ -245,13 +261,16 @@ const extensions = [javascript(), oneDark];
const confShowType = ref('base');
const logOptionRef = ref();
const ipv6OptionRef = ref();
const confirmDialogRefLog = ref();
const mirrorRef = ref();
const registriesRef = ref();
const confirmDialogRefLive = ref();
const confirmDialogRefCgroup = ref();
const confirmDialogRefIptable = ref();
const confirmDialogRefIpv6 = ref();
const logOptionShow = ref();
const ipv6OptionShow = ref();
const form = reactive({
isSwarm: false,
@ -262,6 +281,12 @@ const form = reactive({
liveRestore: false,
iptables: true,
cgroupDriver: '',
ipv6: false,
fixedCidrV6: '',
ip6Tables: false,
experimental: false,
logOptionShow: false,
logMaxSize: '',
logMaxFile: 3,
@ -292,6 +317,27 @@ const onChangeMirrors = () => {
const onChangeRegistries = () => {
registriesRef.value.acceptParams({ registries: form.registries });
};
const handleIPv6 = async () => {
if (form.ipv6) {
ipv6OptionRef.value.acceptParams({
fixedCidrV6: form.fixedCidrV6,
ip6Tables: form.ip6Tables,
experimental: form.experimental,
});
return;
}
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialogRefIpv6.value!.acceptParams(params);
};
const onSaveIPv6 = () => {
save('Ipv6', 'disable');
};
const handleLogOption = async () => {
if (form.logOptionShow) {
logOptionRef.value.acceptParams({ logMaxSize: form.logMaxSize, logMaxFile: form.logMaxFile });
@ -445,6 +491,11 @@ const search = async () => {
form.logOptionShow = false;
logOptionShow.value = false;
}
form.ipv6 = res.data.ipv6;
ipv6OptionShow.value = form.ipv6;
form.fixedCidrV6 = res.data.fixedCidrV6;
form.ip6Tables = res.data.ip6Tables;
form.experimental = res.data.experimental;
};
onMounted(() => {

View File

@ -0,0 +1,157 @@
<template>
<div>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
@close="handleClose"
size="30%"
>
<template #header>
<DrawerHeader header="IPv6" :back="handleClose" />
</template>
<el-alert class="common-prompt" :closable="false" type="warning">
<template #default>
<span class="input-help">
{{ $t('container.ipv6Helper') }}
<el-link
style="font-size: 12px; margin-left: 5px"
icon="Position"
@click="toDoc()"
type="primary"
>
{{ $t('firewall.quickJump') }}
</el-link>
</span>
</template>
</el-alert>
<el-form :model="form" ref="formRef" :rules="rules" v-loading="loading" label-position="top">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item prop="fixedCidrV6" :label="$t('container.subnet')">
<el-input v-model="form.fixedCidrV6" />
<span class="input-help">{{ $t('container.ipv6CidrHelper') }}</span>
</el-form-item>
<el-form-item>
<el-checkbox v-model="showMore" :label="$t('app.advanced')" />
</el-form-item>
<div v-if="showMore">
<el-form-item prop="ip6Tables" label="ip6tables">
<el-switch v-model="form.ip6Tables"></el-switch>
<span class="input-help">{{ $t('container.ipv6TablesHelper') }}</span>
</el-form-item>
<el-form-item prop="experimental" label="experimental">
<el-switch v-model="form.experimental"></el-switch>
<span class="input-help">{{ $t('container.experimentalHelper') }}</span>
</el-form-item>
</div>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
<ConfirmDialog ref="confirmDialogRef" @confirm="onSubmitSave"></ConfirmDialog>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { updateIpv6Option } from '@/api/modules/container';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { checkIpV6 } from '@/utils/util';
const loading = ref();
const drawerVisible = ref();
const confirmDialogRef = ref();
const formRef = ref();
const showMore = ref(false);
interface DialogProps {
fixedCidrV6: string;
ip6Tables: boolean;
experimental: boolean;
}
const form = reactive({
fixedCidrV6: '',
ip6Tables: false,
experimental: false,
});
const rules = reactive({
fixedCidrV6: [{ validator: checkFixedCidrV6, trigger: 'blur', required: true }],
});
function checkFixedCidrV6(rule: any, value: any, callback: any) {
if (!form.fixedCidrV6 || form.fixedCidrV6.indexOf('/') === -1) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
if (checkIpV6(form.fixedCidrV6.split('/')[0])) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
const reg = /^(?:[1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/;
if (!reg.test(form.fixedCidrV6.split('/')[1])) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
callback();
}
const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/containers/setting/', '_blank', 'noopener,noreferrer');
};
const emit = defineEmits<{ (e: 'search'): void }>();
const acceptParams = (params: DialogProps): void => {
form.fixedCidrV6 = params.fixedCidrV6;
form.ip6Tables = params.ip6Tables;
form.experimental = params.experimental;
drawerVisible.value = true;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialogRef.value!.acceptParams(params);
});
};
const onSubmitSave = async () => {
loading.value = true;
await updateIpv6Option(form.fixedCidrV6, form.ip6Tables, form.experimental)
.then(() => {
loading.value = false;
drawerVisible.value = false;
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
};
const handleClose = () => {
emit('search');
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>