Previously, we would always set the port mapping during a dockerfile build, making it difficult for users to override mappings. We also only _sometimes_ updated the detected port mapping, further confusing issues when users were migrating from Dockerfile to Buildpacks for builds. Now, we always detect the port mapping during the build process, and only use that detected port mapping if an override is not specified. This greatly simplifies the experience around port mapping, as now a user can create an app, set a port mapping, and that first deploy will respect the port mapping without an additional deploy. The builder always has the best context for what the app should be listening on, and thus we can always specify a "default" port mapping at this stage. Users can override this map as desired later. This change also results in the removal of a ton of internal code that is now centralized in the ports plugin. Closes #4067
415 lines
8.9 KiB
Go
415 lines
8.9 KiB
Go
package ports
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/dokku/dokku/plugins/common"
|
|
"github.com/dokku/dokku/plugins/config"
|
|
"github.com/ryanuber/columnize"
|
|
)
|
|
|
|
func addPortMaps(appName string, portMaps []PortMap) error {
|
|
allPortMaps := getPortMaps(appName)
|
|
allPortMaps = append(allPortMaps, portMaps...)
|
|
|
|
return setPortMaps(appName, allPortMaps)
|
|
}
|
|
|
|
func clearPorts(appName string) error {
|
|
if err := common.PropertyDelete("ports", appName, "map"); err != nil {
|
|
return err
|
|
}
|
|
|
|
return common.PropertyDelete("ports", appName, "map-detected")
|
|
}
|
|
|
|
func doesCertExist(appName string) bool {
|
|
b, _ := common.PlugnTriggerOutput("certs-exists", []string{appName}...)
|
|
certsExists := strings.TrimSpace(string(b[:]))
|
|
return certsExists == "true"
|
|
}
|
|
|
|
func filterAppPortMaps(appName string, scheme string, hostPort int) []PortMap {
|
|
var filteredPortMaps []PortMap
|
|
for _, portMap := range getPortMaps(appName) {
|
|
if portMap.Scheme == scheme && portMap.HostPort == hostPort {
|
|
filteredPortMaps = append(filteredPortMaps, portMap)
|
|
}
|
|
}
|
|
|
|
return filteredPortMaps
|
|
}
|
|
|
|
func getAvailablePort() int {
|
|
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
for {
|
|
l, err := net.ListenTCP("tcp", addr)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer l.Close()
|
|
|
|
port := l.Addr().(*net.TCPAddr).Port
|
|
if port >= 1025 && port <= 65535 {
|
|
return port
|
|
}
|
|
}
|
|
}
|
|
|
|
func getComputedProxyPort(appName string) int {
|
|
port := getProxyPort(appName)
|
|
if port == 0 {
|
|
port = getGlobalProxyPort()
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getComputedProxySSLPort(appName string) int {
|
|
port := getProxySSLPort(appName)
|
|
if port == 0 {
|
|
port = getGlobalProxySSLPort()
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getDetectedPortMaps(appName string) []PortMap {
|
|
basePort := getComputedProxyPort(appName)
|
|
if basePort == 0 {
|
|
basePort = 80
|
|
}
|
|
defaultMapping := []PortMap{
|
|
{
|
|
ContainerPort: 5000,
|
|
HostPort: basePort,
|
|
Scheme: "http",
|
|
},
|
|
}
|
|
|
|
portMaps := []PortMap{}
|
|
value, err := common.PropertyListGet("ports", appName, "map-detected")
|
|
if err == nil {
|
|
portMaps, _ = parsePortMapString(strings.Join(value, " "))
|
|
}
|
|
|
|
if len(portMaps) == 0 {
|
|
portMaps = defaultMapping
|
|
}
|
|
|
|
if doesCertExist(appName) {
|
|
setSSLPort := false
|
|
baseSSLPort := getComputedProxySSLPort(appName)
|
|
if baseSSLPort == 0 {
|
|
baseSSLPort = 443
|
|
}
|
|
|
|
for _, portMap := range portMaps {
|
|
if portMap.Scheme != "http" || portMap.HostPort != 80 {
|
|
continue
|
|
}
|
|
|
|
setSSLPort = true
|
|
portMaps = append(portMaps, PortMap{
|
|
ContainerPort: portMap.ContainerPort,
|
|
HostPort: baseSSLPort,
|
|
Scheme: "https",
|
|
})
|
|
}
|
|
|
|
if !setSSLPort {
|
|
for i, portMap := range portMaps {
|
|
if portMap.Scheme != "http" {
|
|
continue
|
|
}
|
|
|
|
portMaps[i].Scheme = "https"
|
|
}
|
|
}
|
|
}
|
|
|
|
return portMaps
|
|
}
|
|
|
|
func getGlobalProxyPort() int {
|
|
port := 0
|
|
b, _ := common.PlugnTriggerOutput("config-get-global", []string{"DOKKU_PROXY_PORT"}...)
|
|
if intVar, err := strconv.Atoi(strings.TrimSpace(string(b[:]))); err == nil {
|
|
port = intVar
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getGlobalProxySSLPort() int {
|
|
port := 0
|
|
b, _ := common.PlugnTriggerOutput("config-get-global", []string{"DOKKU_PROXY_SSL_PORT"}...)
|
|
if intVar, err := strconv.Atoi(strings.TrimSpace(string(b[:]))); err == nil {
|
|
port = intVar
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getPortMaps(appName string) []PortMap {
|
|
value, err := common.PropertyListGet("ports", appName, "map")
|
|
if err != nil {
|
|
return []PortMap{}
|
|
}
|
|
|
|
portMaps, _ := parsePortMapString(strings.Join(value, " "))
|
|
return portMaps
|
|
}
|
|
|
|
func getProxyPort(appName string) int {
|
|
port := 0
|
|
b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_PROXY_PORT"}...)
|
|
if intVar, err := strconv.Atoi(strings.TrimSpace(string(b[:]))); err == nil {
|
|
port = intVar
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getProxySSLPort(appName string) int {
|
|
port := 0
|
|
b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_PROXY_SSL_PORT"}...)
|
|
if intVar, err := strconv.Atoi(strings.TrimSpace(string(b[:]))); err == nil {
|
|
port = intVar
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func initializeProxyPort(appName string) error {
|
|
port := getProxyPort(appName)
|
|
if port != 0 {
|
|
return nil
|
|
}
|
|
|
|
if isAppVhostEnabled(appName) {
|
|
port = getGlobalProxyPort()
|
|
} else {
|
|
common.LogInfo1("No port set, setting to random open high port")
|
|
port = getAvailablePort()
|
|
common.LogInfo1(fmt.Sprintf("Random port %d", port))
|
|
}
|
|
|
|
if port == 0 {
|
|
port = 80
|
|
}
|
|
|
|
if err := setProxyPort(appName, port); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func initializeProxySSLPort(appName string) error {
|
|
port := getProxySSLPort(appName)
|
|
if port != 0 {
|
|
return nil
|
|
}
|
|
|
|
if !doesCertExist(appName) {
|
|
return nil
|
|
}
|
|
|
|
port = getGlobalProxySSLPort()
|
|
if port == 0 {
|
|
port = 443
|
|
}
|
|
|
|
if !isAppVhostEnabled(appName) {
|
|
common.LogInfo1("No ssl port set, setting to random open high port")
|
|
port = getAvailablePort()
|
|
}
|
|
|
|
if err := setProxySSLPort(appName, port); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func inRange(value int, min int, max int) bool {
|
|
return min < value && value < max
|
|
}
|
|
|
|
func isAppVhostEnabled(appName string) bool {
|
|
if err := common.PlugnTrigger("domains-vhost-enabled", []string{appName}...); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func listAppPortMaps(appName string) error {
|
|
portMaps := getPortMaps(appName)
|
|
|
|
if len(portMaps) == 0 {
|
|
return errors.New("No port mappings configured for app")
|
|
}
|
|
|
|
var lines []string
|
|
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
|
|
lines = append(lines, "-----> scheme:host port:container port")
|
|
}
|
|
|
|
for _, portMap := range portMaps {
|
|
lines = append(lines, portMap.String())
|
|
}
|
|
|
|
sort.Strings(lines)
|
|
common.LogInfo1Quiet(fmt.Sprintf("Port mappings for %s", appName))
|
|
config := columnize.DefaultConfig()
|
|
config.Delim = ":"
|
|
config.Prefix = " "
|
|
config.Empty = ""
|
|
fmt.Println(columnize.Format(lines, config))
|
|
return nil
|
|
}
|
|
|
|
func parsePortMapString(stringPortMap string) ([]PortMap, error) {
|
|
var portMaps []PortMap
|
|
|
|
for _, v := range strings.Split(strings.TrimSpace(stringPortMap), " ") {
|
|
parts := strings.SplitN(v, ":", 3)
|
|
if len(parts) == 1 {
|
|
hostPort, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [err=%s]", v, err.Error())
|
|
}
|
|
|
|
if !inRange(hostPort, 0, 65536) {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [hostPort=%d]", v, hostPort)
|
|
}
|
|
|
|
portMaps = append(portMaps, PortMap{
|
|
HostPort: hostPort,
|
|
Scheme: "__internal__",
|
|
})
|
|
continue
|
|
}
|
|
|
|
if len(parts) != 3 {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [len=%d]", v, len(parts))
|
|
}
|
|
|
|
hostPort, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [err=%s]", v, err.Error())
|
|
}
|
|
|
|
containerPort, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [err=%s]", v, err.Error())
|
|
}
|
|
|
|
if !inRange(hostPort, 0, 65536) {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [hostPort=%d]", v, hostPort)
|
|
}
|
|
|
|
if !inRange(containerPort, 0, 65536) {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [containerPort=%d]", v, containerPort)
|
|
}
|
|
|
|
portMaps = append(portMaps, PortMap{
|
|
ContainerPort: containerPort,
|
|
HostPort: hostPort,
|
|
Scheme: parts[0],
|
|
})
|
|
}
|
|
|
|
return uniquePortMaps(portMaps), nil
|
|
}
|
|
|
|
func removePortMaps(appName string, portMaps []PortMap) error {
|
|
toRemove := map[string]bool{}
|
|
toRemoveByPort := map[int]bool{}
|
|
|
|
for _, portMap := range portMaps {
|
|
if portMap.AllowsPersistence() {
|
|
toRemoveByPort[portMap.HostPort] = true
|
|
continue
|
|
}
|
|
toRemove[portMap.String()] = true
|
|
}
|
|
|
|
var toSet []PortMap
|
|
for _, portMap := range getPortMaps(appName) {
|
|
if toRemove[portMap.String()] {
|
|
continue
|
|
}
|
|
|
|
if toRemoveByPort[portMap.HostPort] {
|
|
continue
|
|
}
|
|
|
|
toSet = append(toSet, portMap)
|
|
}
|
|
|
|
if len(toSet) == 0 {
|
|
return common.PropertyDelete("ports", appName, "map")
|
|
}
|
|
|
|
return setPortMaps(appName, toSet)
|
|
}
|
|
|
|
func setPortMaps(appName string, portMaps []PortMap) error {
|
|
var value []string
|
|
for _, portMap := range uniquePortMaps(portMaps) {
|
|
if portMap.AllowsPersistence() {
|
|
continue
|
|
}
|
|
|
|
value = append(value, portMap.String())
|
|
}
|
|
|
|
sort.Strings(value)
|
|
return common.PropertyListWrite("ports", appName, "map", value)
|
|
}
|
|
|
|
func setProxyPort(appName string, port int) error {
|
|
return common.EnvWrap(func() error {
|
|
entries := map[string]string{
|
|
"DOKKU_PROXY_PORT": fmt.Sprint(port),
|
|
}
|
|
return config.SetMany(appName, entries, false)
|
|
}, map[string]string{"DOKKU_QUIET_OUTPUT": "1"})
|
|
}
|
|
|
|
func setProxySSLPort(appName string, port int) error {
|
|
return common.EnvWrap(func() error {
|
|
entries := map[string]string{
|
|
"DOKKU_PROXY_SSL_PORT": fmt.Sprint(port),
|
|
}
|
|
return config.SetMany(appName, entries, false)
|
|
}, map[string]string{"DOKKU_QUIET_OUTPUT": "1"})
|
|
}
|
|
|
|
func uniquePortMaps(portMaps []PortMap) []PortMap {
|
|
var unique []PortMap
|
|
existingPortMaps := map[string]bool{}
|
|
|
|
for _, portMap := range portMaps {
|
|
if existingPortMaps[portMap.String()] {
|
|
continue
|
|
}
|
|
|
|
existingPortMaps[portMap.String()] = true
|
|
unique = append(unique, portMap)
|
|
}
|
|
|
|
return unique
|
|
}
|