Files
dokku/plugins/ports/functions.go
Jose Diaz-Gonzalez 625ea14c8f feat: add openresty proxy implementation
This plugin is mostly compatible with the nginx plugin, but runs the proxy within a docker container. Users do not have direct access to add custom openresty configuration at this time, but instead receive the ability to setup automatic ssl on first request via letsencrypt integration.
2023-08-05 20:59:01 -04:00

419 lines
9.0 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 {
certsExists, _ := common.PlugnTriggerOutputAsString("certs-exists", []string{appName}...)
if certsExists == "true" {
return true
}
certsForce, _ := common.PlugnTriggerOutputAsString("certs-force", []string{appName}...)
return certsForce == "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
}