diff --git a/app/application/http/controller/network.go b/app/application/http/controller/network.go index 0be9dfee..884d7e9b 100644 --- a/app/application/http/controller/network.go +++ b/app/application/http/controller/network.go @@ -123,6 +123,22 @@ func (self Network) Create(http *gin.Context) { return } + checkIpInSubnet := [][2]string{ + { + params.IpGateway, params.IpSubnet, + }, + { + params.IpV6Gateway, params.IpV6Subnet, + }, + } + for _, item := range checkIpInSubnet { + _, err := function.IpInSubnet(item[0], item[1]) + if err != nil { + self.JsonResponseWithError(http, err, 500) + return + } + } + ipAm := &network.IPAM{ Config: []network.IPAMConfig{}, Options: map[string]string{}, diff --git a/app/application/http/controller/site.go b/app/application/http/controller/site.go index e1271cb4..0ee809df 100644 --- a/app/application/http/controller/site.go +++ b/app/application/http/controller/site.go @@ -39,6 +39,31 @@ func (self Site) CreateByImage(http *gin.Context) { return } + checkIpInSubnet := [][2]string{ + { + buildParams.IpV4.Address, buildParams.IpV4.Subnet, + }, + { + buildParams.IpV4.Gateway, buildParams.IpV4.Subnet, + }, + { + buildParams.IpV6.Address, buildParams.IpV6.Subnet, + }, + { + buildParams.IpV6.Gateway, buildParams.IpV6.Subnet, + }, + } + for _, item := range checkIpInSubnet { + if item[0] == "" { + continue + } + _, err := function.IpInSubnet(item[0], item[1]) + if err != nil { + self.JsonResponseWithError(http, err, 500) + return + } + } + for _, itemDefault := range buildParams.VolumesDefault { for _, item := range buildParams.Volumes { if item.Dest == itemDefault.Dest { @@ -97,7 +122,7 @@ func (self Site) CreateByImage(http *gin.Context) { // 重新部署,先删掉之前的容器 if params.Id != 0 || params.ContainerId != "" { _ = notice.Message{}.Info("containerCreate", "正在停止旧容器") - if oldContainerInfo.ID != "" { + if oldContainerInfo.ContainerJSONBase != nil && oldContainerInfo.ID != "" { err := docker.Sdk.Client.ContainerStop(docker.Sdk.Ctx, params.SiteName, container.StopOptions{}) if err != nil { self.JsonResponseWithError(http, err, 500) diff --git a/app/application/logic/docker-container-task.go b/app/application/logic/docker-container-task.go index 162e8f35..b1df8f79 100644 --- a/app/application/logic/docker-container-task.go +++ b/app/application/logic/docker-container-task.go @@ -17,8 +17,38 @@ func (self DockerTask) ContainerCreate(task *CreateContainerOption) (string, err builder.WithContainerName(task.SiteName) // 如果绑定了ipv6 需要先创建一个ipv6的自身网络 - if task.BuildParams.BindIpV6 || !function.IsEmptyArray(task.BuildParams.Links) { - builder.CreateOwnerNetwork(task.BuildParams.BindIpV6) + // 如果容器配置了Ip,需要先创一个自身网络 + if task.BuildParams.BindIpV6 || + !function.IsEmptyArray(task.BuildParams.Links) || + task.BuildParams.IpV4.Address != "" || task.BuildParams.IpV6.Address != "" { + + option := network.CreateOptions{ + IPAM: &network.IPAM{ + Driver: "default", + Options: map[string]string{}, + Config: []network.IPAMConfig{}, + }, + } + if task.BuildParams.BindIpV6 { + option.EnableIPv6 = function.PtrBool(true) + } + if task.BuildParams.IpV4.Address != "" { + option.IPAM.Config = append(option.IPAM.Config, network.IPAMConfig{ + Subnet: task.BuildParams.IpV4.Subnet, + Gateway: task.BuildParams.IpV4.Gateway, + }) + } + if task.BuildParams.IpV6.Address != "" { + option.EnableIPv6 = function.PtrBool(true) + option.IPAM.Config = append(option.IPAM.Config, network.IPAMConfig{ + Subnet: task.BuildParams.IpV6.Subnet, + Gateway: task.BuildParams.IpV6.Gateway, + }) + } + err := builder.CreateOwnerNetwork(option) + if err != nil { + return "", err + } } // Environment @@ -31,13 +61,10 @@ func (self DockerTask) ContainerCreate(task *CreateContainerOption) (string, err } } - // Links + // Links Volume + // 避免其它容器先抢占了本身容器配置的ip,需要在容器都完成创建后,统一加入网络 if !function.IsEmptyArray(task.BuildParams.Links) { for _, value := range task.BuildParams.Links { - if value.Alise == "" { - value.Alise = value.Name - } - builder.WithLink(value.Name, value.Alise) if value.Volume { builder.WithContainerVolume(value.Name) } @@ -150,13 +177,36 @@ func (self DockerTask) ContainerCreate(task *CreateContainerOption) (string, err return "", err } + err = docker.Sdk.Client.ContainerStart(docker.Sdk.Ctx, response.ID, container.StartOptions{}) + if err != nil { + //notice.Message{}.Error("containerCreate", err.Error()) + return response.ID, err + } + // 仅当容器有关联时,才加新建自己的网络。对于ipv6支持,必须加入一个ipv6的网络 - if task.BuildParams.BindIpV6 || !function.IsEmptyArray(task.BuildParams.Links) { - err = docker.Sdk.Client.NetworkConnect(docker.Sdk.Ctx, task.SiteName, response.ID, &network.EndpointSettings{ + if task.BuildParams.BindIpV6 || !function.IsEmptyArray(task.BuildParams.Links) || task.BuildParams.IpV4.Address != "" || task.BuildParams.IpV6.Address != "" { + endpointSetting := &network.EndpointSettings{ Aliases: []string{ fmt.Sprintf("%s.pod.dpanel.local", task.SiteName), }, - }) + IPAMConfig: &network.EndpointIPAMConfig{}, + } + if task.BuildParams.IpV4.Address != "" { + endpointSetting.IPAMConfig.IPv4Address = task.BuildParams.IpV4.Address + } + if task.BuildParams.IpV6.Address != "" { + endpointSetting.IPAMConfig.IPv6Address = task.BuildParams.IpV6.Address + } + err = docker.Sdk.Client.NetworkConnect(docker.Sdk.Ctx, task.SiteName, response.ID, endpointSetting) + } + + if !function.IsEmptyArray(task.BuildParams.Links) { + for _, value := range task.BuildParams.Links { + if value.Alise == "" { + value.Alise = value.Name + } + builder.WithLink(value.Name, value.Alise) + } } // 网络需要在创建好容器后统一 connect 否则 bridge 网络会消失。当网络变更后了,可能绑定的端口无法使用。 @@ -184,11 +234,6 @@ func (self DockerTask) ContainerCreate(task *CreateContainerOption) (string, err return response.ID, err } - err = docker.Sdk.Client.ContainerStart(docker.Sdk.Ctx, response.ID, container.StartOptions{}) - if err != nil { - //notice.Message{}.Error("containerCreate", err.Error()) - return response.ID, err - } _ = notice.Message{}.Success("containerCreate", task.SiteName) return response.ID, err } diff --git a/common/accessor/site_env_option.go b/common/accessor/site_env_option.go index 300f0efc..ecf00bb4 100644 --- a/common/accessor/site_env_option.go +++ b/common/accessor/site_env_option.go @@ -36,30 +36,39 @@ type LogDriverItem struct { MaxFile string `json:"maxFile"` MaxSize string `json:"maxSize"` } + +type ContainerNetworkItem struct { + Address string `json:"address"` + Subnet string `json:"subnet"` + Gateway string `json:"gateway"` +} + type SiteEnvOption struct { - Environment []EnvItem `json:"environment"` - Links []LinkItem `json:"links"` - Ports []PortItem `json:"ports"` - Volumes []VolumeItem `json:"volumes"` - VolumesDefault []VolumeItem `json:"volumesDefault"` - Network []NetworkItem `json:"network"` - ImageName string `json:"imageName"` // 非表单提交 - ImageId string `json:"imageId"` // 非表单提交 - Privileged bool `json:"privileged"` - AutoRemove bool `json:"autoRemove"` - Restart string `json:"restart"` - Cpus float32 `json:"cpus"` - Memory int `json:"memory"` - ShmSize string `json:"shmsize,omitempty"` - WorkDir string `json:"workDir"` - User string `json:"user"` - Command string `json:"command"` - Entrypoint string `json:"entrypoint"` - UseHostNetwork bool `json:"useHostNetwork"` - BindIpV6 bool `json:"bindIpV6"` - Log LogDriverItem `json:"log"` - Dns []string `json:"dns"` - Label []EnvItem `json:"label"` - PublishAllPorts bool `json:"publishAllPorts"` - ExtraHosts []EnvItem `json:"extraHosts"` + Environment []EnvItem `json:"environment"` + Links []LinkItem `json:"links"` + Ports []PortItem `json:"ports"` + Volumes []VolumeItem `json:"volumes"` + VolumesDefault []VolumeItem `json:"volumesDefault"` + Network []NetworkItem `json:"network"` + ImageName string `json:"imageName"` // 非表单提交 + ImageId string `json:"imageId"` // 非表单提交 + Privileged bool `json:"privileged"` + AutoRemove bool `json:"autoRemove"` + Restart string `json:"restart"` + Cpus float32 `json:"cpus"` + Memory int `json:"memory"` + ShmSize string `json:"shmsize,omitempty"` + WorkDir string `json:"workDir"` + User string `json:"user"` + Command string `json:"command"` + Entrypoint string `json:"entrypoint"` + UseHostNetwork bool `json:"useHostNetwork"` + BindIpV6 bool `json:"bindIpV6"` + Log LogDriverItem `json:"log"` + Dns []string `json:"dns"` + Label []EnvItem `json:"label"` + PublishAllPorts bool `json:"publishAllPorts"` + ExtraHosts []EnvItem `json:"extraHosts"` + IpV4 ContainerNetworkItem `json:"ipV4"` + IpV6 ContainerNetworkItem `json:"ipV6"` } diff --git a/common/function/util.go b/common/function/util.go index 6de29137..76c79f7d 100644 --- a/common/function/util.go +++ b/common/function/util.go @@ -2,8 +2,10 @@ package function import ( "crypto/md5" + "errors" "fmt" "math/rand" + "net" "path/filepath" "strings" ) @@ -60,3 +62,22 @@ func GetRootPath() string { rootPath, _ := filepath.Abs("./") return rootPath } + +func IpInSubnet(ipAddress, subnetAddress string) (bool, error) { + ip := net.ParseIP(ipAddress) + if ip == nil { + return false, errors.New("错误的 ip 地址: " + ipAddress) + } + _, subnet, err := net.ParseCIDR(subnetAddress) + if err != nil { + return false, errors.New("错误的子网 CIDR 地址: " + subnetAddress) + } + + if subnetAddress != subnet.String() { + return false, errors.New("错误的子网 CIDR 地址, 应为: " + subnet.String()) + } + if !subnet.Contains(ip) { + return false, errors.New("ip 地址与子网地址不匹配") + } + return true, nil +} diff --git a/common/service/docker/container-create.go b/common/service/docker/container-create.go index 3dbb4615..cb05c3ff 100644 --- a/common/service/docker/container-create.go +++ b/common/service/docker/container-create.go @@ -234,18 +234,34 @@ func (self *ContainerCreateBuilder) WithExtraHosts(name, value string) { self.hostConfig.ExtraHosts = append(self.hostConfig.ExtraHosts, fmt.Sprintf("%s:%s", name, value)) } -func (self *ContainerCreateBuilder) CreateOwnerNetwork(enableIpV6 bool) { +func (self *ContainerCreateBuilder) CreateOwnerNetwork(option network.CreateOptions) error { // 利用Network关联容器 + // 每次创建自身网络时,先删除掉,最后再统一将关联和自身加入进来 + // 容器关联时必须采用 hostname 以保证容器可以访问 + selfNetwork, err := Sdk.Client.NetworkInspect(Sdk.Ctx, self.containerName, network.InspectOptions{}) + if err == nil { + for _, item := range selfNetwork.Containers { + err = Sdk.Client.NetworkDisconnect(Sdk.Ctx, self.containerName, item.Name, true) + } + if err != nil { + return err + } + _ = Sdk.Client.NetworkRemove(Sdk.Ctx, self.containerName) + } options := make(map[string]string) options["name"] = self.containerName - _, err := Sdk.Client.NetworkCreate(Sdk.Ctx, self.containerName, network.CreateOptions{ + + myOption := network.CreateOptions{ Driver: "bridge", Options: options, - EnableIPv6: &enableIpV6, - }) + EnableIPv6: option.EnableIPv6, + IPAM: option.IPAM, + } + _, err = Sdk.Client.NetworkCreate(Sdk.Ctx, self.containerName, myOption) if err != nil { slog.Debug("create network", "name", self.containerName, err) } + return err } func (self *ContainerCreateBuilder) Execute() (response container.CreateResponse, err error) { diff --git a/tests/docker_test.go b/tests/docker_test.go index 81a7fe8b..e4c19597 100644 --- a/tests/docker_test.go +++ b/tests/docker_test.go @@ -101,8 +101,8 @@ func TestCreateContainer(t *testing.T) { builder.WithImage("portainer/portainer-ce:latest", false) builder.WithContainerName("portainer") //builder.WithEnv("PMA_HOST", "localmysql") - builder.WithPort("8000", "8000") - builder.WithPort("9000", "9000") + builder.WithPort("", "8000", "8000") + builder.WithPort("", "9000", "9000") //builder.WithLink("localmysql", "localmysql") builder.WithRestart("always") builder.WithPrivileged() diff --git a/tests/func_test.go b/tests/func_test.go index 4aeed218..2997821c 100644 --- a/tests/func_test.go +++ b/tests/func_test.go @@ -113,3 +113,26 @@ func TestSplitCommand(t *testing.T) { cmdArr = function.CommandSplit(cmd) asserter.Equal(cmdArr[5], "config.yaml") } + +func TestIpInSubnet(t *testing.T) { + asserter := assert.New(t) + check, err := function.IpInSubnet("192.168.0.1", "192.168.0.0/24") + fmt.Printf("%v \n", err) + asserter.True(check) + + check, err = function.IpInSubnet("192.168.0.1", "192.168.1.0/24") + fmt.Printf("%v \n", err) + asserter.False(check) + + check, err = function.IpInSubnet("2001:db8::2", "2001:db8::/48") + fmt.Printf("%v \n", err) + asserter.True(check) + + check, err = function.IpInSubnet("", "") + fmt.Printf("%v \n", err) + asserter.True(check) + + check, err = function.IpInSubnet("192.168.0.1", "192.168.0.1/24") + fmt.Printf("%v \n", err) + asserter.True(check) +}