Refactor container port handling and improve user input parsing
- Use containerInfo.Config.ExposedPorts for preparing port mappings in ContainerManagerEditGET function. - Update port label and guidance text in createcontainer.gohtml template. - Add parsing logic for port mappings input in FromInputsToCmdParams function, replacing the basic string split method. - Introduce PortMapping struct and ParsePortsSpecs function in pkg/docker/containers.go for parsing port specifications. - Remove deprecated Ports field from ContainerCommandParams and replace it with PortMappings. - Update CreateContainer function to use the new PortMapping struct for port bindings.
- commit
- 362ea0889624545200456bbd455a9e57389c0d05
- author
- bnema · · 2023-10-09 10:54 UTC
- parent
- 608b713
- signature
- unsigned
4 files · +83 -16
M internal/httpserve/handler/editcontainer.go +6 -6
| @@ -51,14 +51,14 @@ func ContainerManagerEditGET(c echo.Context, a *app.App) error { | |||
| 51 | 51 | networkNames = append(networkNames, networkName) | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | - | // Prepare Ports | |
| 54 | + | // Prepare Ports while using the containerInfo.Config.ExposedPorts | |
| 55 | 55 | portMappings := make([]string, 0) | |
| 56 | - | for port, binding := range containerInfo.NetworkSettings.Ports { | |
| 57 | - | for _, b := range binding { | |
| 58 | - | portMappings = append(portMappings, fmt.Sprintf("%s:%s->%s", b.HostIP, b.HostPort, port)) | |
| 59 | - | } | |
| 56 | + | for exposedPort := range containerInfo.Config.ExposedPorts { | |
| 57 | + | // Here you might want to check your lastKnownPortMappings to find out what this exposedPort is mapped to on the host. | |
| 58 | + | hostPort := "" // Fetch this from lastKnownPortMappings or set a default | |
| 59 | + | portMapping := fmt.Sprintf("0.0.0.0:%s->%s", hostPort, exposedPort) | |
| 60 | + | portMappings = append(portMappings, portMapping) | |
| 60 | 61 | } | |
| 61 | - | ||
| 62 | 62 | // Prepare Volumes (Mounts) | |
| 63 | 63 | volumeMappings := make([]string, 0) | |
| 64 | 64 | for _, mount := range containerInfo.Mounts { | |
| @@ -51,14 +51,14 @@ func ContainerManagerEditGET(c echo.Context, a *app.App) error { | |||||
| 51 | networkNames = append(networkNames, networkName) | 51 | networkNames = append(networkNames, networkName) | ||
| 52 | } | 52 | } | ||
| 53 | 53 | ||||
| 54 | - | // Prepare Ports | 54 | + | // Prepare Ports while using the containerInfo.Config.ExposedPorts |
| 55 | portMappings := make([]string, 0) | 55 | portMappings := make([]string, 0) | ||
| 56 | - | for port, binding := range containerInfo.NetworkSettings.Ports { | 56 | + | for exposedPort := range containerInfo.Config.ExposedPorts { |
| 57 | - | for _, b := range binding { | 57 | + | // Here you might want to check your lastKnownPortMappings to find out what this exposedPort is mapped to on the host. |
| 58 | - | portMappings = append(portMappings, fmt.Sprintf("%s:%s->%s", b.HostIP, b.HostPort, port)) | 58 | + | hostPort := "" // Fetch this from lastKnownPortMappings or set a default |
| 59 | - | } | 59 | + | portMapping := fmt.Sprintf("0.0.0.0:%s->%s", hostPort, exposedPort) |
| 60 | + | portMappings = append(portMappings, portMapping) | |||
| 60 | } | 61 | } | ||
| 61 | - | ||||
| 62 | // Prepare Volumes (Mounts) | 62 | // Prepare Volumes (Mounts) | ||
| 63 | volumeMappings := make([]string, 0) | 63 | volumeMappings := make([]string, 0) | ||
| 64 | for _, mount := range containerInfo.Mounts { | 64 | for _, mount := range containerInfo.Mounts { | ||
M internal/templates/models/html/fragments/createcontainer.gohtml +2 -1
| @@ -90,7 +90,8 @@ | |||
| 90 | 90 | <div class="pb-4"> | |
| 91 | 91 | <label class="block">Ports</label> | |
| 92 | 92 | <span class="text-gray-500" | |
| 93 | - | >e.g. 80:80 (left is host, right is container)</span | |
| 93 | + | >eg: 80:80/tcp (You can specify multiple ports by separating them with | |
| 94 | + | a comma)</span | |
| 94 | 95 | > | |
| 95 | 96 | <input | |
| 96 | 97 | type="text" | |
| @@ -90,7 +90,8 @@ | |||||
| 90 | <div class="pb-4"> | 90 | <div class="pb-4"> | ||
| 91 | <label class="block">Ports</label> | 91 | <label class="block">Ports</label> | ||
| 92 | <span class="text-gray-500" | 92 | <span class="text-gray-500" | ||
| 93 | - | >e.g. 80:80 (left is host, right is container)</span | 93 | + | >eg: 80:80/tcp (You can specify multiple ports by separating them with |
| 94 | + | a comma)</span | |||
| 94 | > | 95 | > | ||
| 95 | <input | 96 | <input | ||
| 96 | type="text" | 97 | type="text" | ||
M internal/templates/render/cmdparams.go +17 -7
| @@ -17,9 +17,23 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||
| 17 | 17 | environmentStr := inputs["environment_variables"] | |
| 18 | 18 | environmentSlice := strings.Split(environmentStr, "\n") | |
| 19 | 19 | ||
| 20 | - | fmt.Println("environmentSlice", environmentSlice) | |
| 20 | + | // Ports input parsing | |
| 21 | + | portMappingsStr := inputs["ports"] | |
| 22 | + | portMappingsSliceRaw := strings.Split(portMappingsStr, ",") | |
| 23 | + | ||
| 24 | + | // Trim whitespace from each port mapping string | |
| 25 | + | portMappingsSlice := make([]string, len(portMappingsSliceRaw)) | |
| 26 | + | for i, spec := range portMappingsSliceRaw { | |
| 27 | + | portMappingsSlice[i] = strings.TrimSpace(spec) | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | portMappings, err := docker.ParsePortsSpecs(portMappingsSlice) | |
| 31 | + | if err != nil { | |
| 32 | + | return docker.ContainerCommandParams{}, err | |
| 33 | + | } | |
| 21 | 34 | ||
| 22 | - | portsStr := inputs["ports"] | |
| 35 | + | // Get the exposed port for traefik labels | |
| 36 | + | exposedPort := portMappings[0].ExposedPort | |
| 23 | 37 | ||
| 24 | 38 | params := docker.ContainerCommandParams{ | |
| 25 | 39 | IsSSL: inputs["container_protocol"] == "https", | |
| @@ -28,16 +42,13 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||
| 28 | 42 | Domain: inputs["container_domain"], | |
| 29 | 43 | ImageName: inputs["image_name"], | |
| 30 | 44 | ImageID: inputs["image_id"], | |
| 31 | - | Ports: strings.Split(portsStr, ","), | |
| 32 | 45 | Restart: inputs["restart"], | |
| 33 | 46 | Volumes: volumeSlice, | |
| 34 | 47 | Environment: environmentSlice, | |
| 35 | 48 | Network: ("traefik"), | |
| 49 | + | PortMappings: portMappings, | |
| 36 | 50 | } | |
| 37 | 51 | ||
| 38 | - | // Retain the port number for the container host for the load balancer | |
| 39 | - | exposedPort := strings.Split(params.Ports[0], ":")[0] | |
| 40 | - | ||
| 41 | 52 | entryPoint := "web" | |
| 42 | 53 | if params.IsSSL { | |
| 43 | 54 | entryPoint = "websecure" | |
| @@ -95,7 +106,6 @@ func FromYAMLStructToCmdParams(yamlParams YAMLContainerParams) (docker.Container | |||
| 95 | 106 | ContainerName: yamlParams.Name, | |
| 96 | 107 | ImageName: yamlParams.Image, | |
| 97 | 108 | ImageID: imageID, | |
| 98 | - | Ports: yamlParams.Ports, | |
| 99 | 109 | Volumes: yamlParams.Volumes, | |
| 100 | 110 | Environment: yamlParams.Environment, | |
| 101 | 111 | Labels: labels, | |
| @@ -17,9 +17,23 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||||
| 17 | environmentStr := inputs["environment_variables"] | 17 | environmentStr := inputs["environment_variables"] | ||
| 18 | environmentSlice := strings.Split(environmentStr, "\n") | 18 | environmentSlice := strings.Split(environmentStr, "\n") | ||
| 19 | 19 | ||||
| 20 | - | fmt.Println("environmentSlice", environmentSlice) | 20 | + | // Ports input parsing |
| 21 | + | portMappingsStr := inputs["ports"] | |||
| 22 | + | portMappingsSliceRaw := strings.Split(portMappingsStr, ",") | |||
| 23 | + | ||||
| 24 | + | // Trim whitespace from each port mapping string | |||
| 25 | + | portMappingsSlice := make([]string, len(portMappingsSliceRaw)) | |||
| 26 | + | for i, spec := range portMappingsSliceRaw { | |||
| 27 | + | portMappingsSlice[i] = strings.TrimSpace(spec) | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | portMappings, err := docker.ParsePortsSpecs(portMappingsSlice) | |||
| 31 | + | if err != nil { | |||
| 32 | + | return docker.ContainerCommandParams{}, err | |||
| 33 | + | } | |||
| 21 | 34 | ||||
| 22 | - | portsStr := inputs["ports"] | 35 | + | // Get the exposed port for traefik labels |
| 36 | + | exposedPort := portMappings[0].ExposedPort | |||
| 23 | 37 | ||||
| 24 | params := docker.ContainerCommandParams{ | 38 | params := docker.ContainerCommandParams{ | ||
| 25 | IsSSL: inputs["container_protocol"] == "https", | 39 | IsSSL: inputs["container_protocol"] == "https", | ||
| @@ -28,16 +42,13 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||||
| 28 | Domain: inputs["container_domain"], | 42 | Domain: inputs["container_domain"], | ||
| 29 | ImageName: inputs["image_name"], | 43 | ImageName: inputs["image_name"], | ||
| 30 | ImageID: inputs["image_id"], | 44 | ImageID: inputs["image_id"], | ||
| 31 | - | Ports: strings.Split(portsStr, ","), | |||
| 32 | Restart: inputs["restart"], | 45 | Restart: inputs["restart"], | ||
| 33 | Volumes: volumeSlice, | 46 | Volumes: volumeSlice, | ||
| 34 | Environment: environmentSlice, | 47 | Environment: environmentSlice, | ||
| 35 | Network: ("traefik"), | 48 | Network: ("traefik"), | ||
| 49 | + | PortMappings: portMappings, | |||
| 36 | } | 50 | } | ||
| 37 | 51 | ||||
| 38 | - | // Retain the port number for the container host for the load balancer | |||
| 39 | - | exposedPort := strings.Split(params.Ports[0], ":")[0] | |||
| 40 | - | ||||
| 41 | entryPoint := "web" | 52 | entryPoint := "web" | ||
| 42 | if params.IsSSL { | 53 | if params.IsSSL { | ||
| 43 | entryPoint = "websecure" | 54 | entryPoint = "websecure" | ||
| @@ -95,7 +106,6 @@ func FromYAMLStructToCmdParams(yamlParams YAMLContainerParams) (docker.Container | |||||
| 95 | ContainerName: yamlParams.Name, | 106 | ContainerName: yamlParams.Name, | ||
| 96 | ImageName: yamlParams.Image, | 107 | ImageName: yamlParams.Image, | ||
| 97 | ImageID: imageID, | 108 | ImageID: imageID, | ||
| 98 | - | Ports: yamlParams.Ports, | |||
| 99 | Volumes: yamlParams.Volumes, | 109 | Volumes: yamlParams.Volumes, | ||
| 100 | Environment: yamlParams.Environment, | 110 | Environment: yamlParams.Environment, | ||
| 101 | Labels: labels, | 111 | Labels: labels, | ||
M pkg/docker/containers.go +58 -2
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "bytes" | |
| 5 | 5 | "context" | |
| 6 | 6 | "fmt" | |
| 7 | + | "strconv" | |
| 7 | 8 | "strings" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| @@ -22,7 +23,7 @@ type ContainerCommandParams struct { | |||
| 22 | 23 | EnvVar string | |
| 23 | 24 | ImageName string | |
| 24 | 25 | ImageID string | |
| 25 | - | Ports []string | |
| 26 | + | PortMappings []PortMapping | |
| 26 | 27 | Volumes []string | |
| 27 | 28 | Labels []string | |
| 28 | 29 | Network string | |
| @@ -30,6 +31,50 @@ type ContainerCommandParams struct { | |||
| 30 | 31 | Environment []string | |
| 31 | 32 | } | |
| 32 | 33 | ||
| 34 | + | type PortMapping struct { | |
| 35 | + | HostPort string | |
| 36 | + | ExposedPort string | |
| 37 | + | Protocol string | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // ParsePortsSpecs receives a slice of strings in the format "hostPort:containerPort/Protocol" and returns a slice of PortMapping structs | |
| 41 | + | func ParsePortsSpecs(portsSpecs []string) ([]PortMapping, error) { | |
| 42 | + | portMappings := make([]PortMapping, 0, len(portsSpecs)) | |
| 43 | + | ||
| 44 | + | for _, portSpec := range portsSpecs { | |
| 45 | + | parts := strings.Split(portSpec, ":") | |
| 46 | + | if len(parts) != 2 { | |
| 47 | + | return nil, fmt.Errorf("invalid port specification: %s. Format should be hostPort:containerPort[/Protocol]", portSpec) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Validate hostPort and exposedPort are numbers | |
| 51 | + | hostPort := parts[0] | |
| 52 | + | if _, err := strconv.Atoi(hostPort); err != nil { | |
| 53 | + | return nil, fmt.Errorf("invalid host port: %s. Must be a number", hostPort) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | exposedParts := strings.Split(parts[1], "/") | |
| 57 | + | exposedPort := exposedParts[0] | |
| 58 | + | if _, err := strconv.Atoi(exposedPort); err != nil { | |
| 59 | + | return nil, fmt.Errorf("invalid exposed port: %s. Must be a number", exposedPort) | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Default protocol is tcp | |
| 63 | + | protocol := "tcp" | |
| 64 | + | if len(exposedParts) > 1 { | |
| 65 | + | protocol = exposedParts[1] | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | portMappings = append(portMappings, PortMapping{ | |
| 69 | + | HostPort: hostPort, | |
| 70 | + | ExposedPort: exposedPort, | |
| 71 | + | Protocol: protocol, | |
| 72 | + | }) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | return portMappings, nil | |
| 76 | + | } | |
| 77 | + | ||
| 33 | 78 | func ContainerCommandParamsToConfig(cmdParams ContainerCommandParams) (*container.Config, error) { | |
| 34 | 79 | return &container.Config{ | |
| 35 | 80 | Image: cmdParams.ImageName, | |
| @@ -152,7 +197,18 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |||
| 152 | 197 | // Check if the Docker client has been initialized | |
| 153 | 198 | CheckIfInitialized() | |
| 154 | 199 | ||
| 155 | - | portBindings := map[nat.Port][]nat.PortBinding{} | |
| 200 | + | // Prepare port bindings | |
| 201 | + | portBindings := nat.PortMap{} | |
| 202 | + | for _, portMapping := range cmdParams.PortMappings { | |
| 203 | + | key := fmt.Sprintf("%s/%s", portMapping.ExposedPort, portMapping.Protocol) | |
| 204 | + | portBindings[nat.Port(key)] = []nat.PortBinding{ | |
| 205 | + | { | |
| 206 | + | HostIP: "0.0.0.0", | |
| 207 | + | HostPort: portMapping.HostPort, | |
| 208 | + | }, | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | } | |
| 156 | 212 | ||
| 157 | 213 | // Prepare labels for Traefik | |
| 158 | 214 | labels := map[string]string{} | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "bytes" | 4 | "bytes" | ||
| 5 | "context" | 5 | "context" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | + | "strconv" | |||
| 7 | "strings" | 8 | "strings" | ||
| 8 | "time" | 9 | "time" | ||
| 9 | 10 | ||||
| @@ -22,7 +23,7 @@ type ContainerCommandParams struct { | |||||
| 22 | EnvVar string | 23 | EnvVar string | ||
| 23 | ImageName string | 24 | ImageName string | ||
| 24 | ImageID string | 25 | ImageID string | ||
| 25 | - | Ports []string | 26 | + | PortMappings []PortMapping |
| 26 | Volumes []string | 27 | Volumes []string | ||
| 27 | Labels []string | 28 | Labels []string | ||
| 28 | Network string | 29 | Network string | ||
| @@ -30,6 +31,50 @@ type ContainerCommandParams struct { | |||||
| 30 | Environment []string | 31 | Environment []string | ||
| 31 | } | 32 | } | ||
| 32 | 33 | ||||
| 34 | + | type PortMapping struct { | |||
| 35 | + | HostPort string | |||
| 36 | + | ExposedPort string | |||
| 37 | + | Protocol string | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // ParsePortsSpecs receives a slice of strings in the format "hostPort:containerPort/Protocol" and returns a slice of PortMapping structs | |||
| 41 | + | func ParsePortsSpecs(portsSpecs []string) ([]PortMapping, error) { | |||
| 42 | + | portMappings := make([]PortMapping, 0, len(portsSpecs)) | |||
| 43 | + | ||||
| 44 | + | for _, portSpec := range portsSpecs { | |||
| 45 | + | parts := strings.Split(portSpec, ":") | |||
| 46 | + | if len(parts) != 2 { | |||
| 47 | + | return nil, fmt.Errorf("invalid port specification: %s. Format should be hostPort:containerPort[/Protocol]", portSpec) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // Validate hostPort and exposedPort are numbers | |||
| 51 | + | hostPort := parts[0] | |||
| 52 | + | if _, err := strconv.Atoi(hostPort); err != nil { | |||
| 53 | + | return nil, fmt.Errorf("invalid host port: %s. Must be a number", hostPort) | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | exposedParts := strings.Split(parts[1], "/") | |||
| 57 | + | exposedPort := exposedParts[0] | |||
| 58 | + | if _, err := strconv.Atoi(exposedPort); err != nil { | |||
| 59 | + | return nil, fmt.Errorf("invalid exposed port: %s. Must be a number", exposedPort) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // Default protocol is tcp | |||
| 63 | + | protocol := "tcp" | |||
| 64 | + | if len(exposedParts) > 1 { | |||
| 65 | + | protocol = exposedParts[1] | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | portMappings = append(portMappings, PortMapping{ | |||
| 69 | + | HostPort: hostPort, | |||
| 70 | + | ExposedPort: exposedPort, | |||
| 71 | + | Protocol: protocol, | |||
| 72 | + | }) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | return portMappings, nil | |||
| 76 | + | } | |||
| 77 | + | ||||
| 33 | func ContainerCommandParamsToConfig(cmdParams ContainerCommandParams) (*container.Config, error) { | 78 | func ContainerCommandParamsToConfig(cmdParams ContainerCommandParams) (*container.Config, error) { | ||
| 34 | return &container.Config{ | 79 | return &container.Config{ | ||
| 35 | Image: cmdParams.ImageName, | 80 | Image: cmdParams.ImageName, | ||
| @@ -152,7 +197,18 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |||||
| 152 | // Check if the Docker client has been initialized | 197 | // Check if the Docker client has been initialized | ||
| 153 | CheckIfInitialized() | 198 | CheckIfInitialized() | ||
| 154 | 199 | ||||
| 155 | - | portBindings := map[nat.Port][]nat.PortBinding{} | 200 | + | // Prepare port bindings |
| 201 | + | portBindings := nat.PortMap{} | |||
| 202 | + | for _, portMapping := range cmdParams.PortMappings { | |||
| 203 | + | key := fmt.Sprintf("%s/%s", portMapping.ExposedPort, portMapping.Protocol) | |||
| 204 | + | portBindings[nat.Port(key)] = []nat.PortBinding{ | |||
| 205 | + | { | |||
| 206 | + | HostIP: "0.0.0.0", | |||
| 207 | + | HostPort: portMapping.HostPort, | |||
| 208 | + | }, | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | } | |||
| 156 | 212 | ||||
| 157 | // Prepare labels for Traefik | 213 | // Prepare labels for Traefik | ||
| 158 | labels := map[string]string{} | 214 | labels := map[string]string{} | ||