feat: write auto-detected port mappings during a deploy

During an app build, we now auto-detect ports based on the source code. This is usually http:80:5000, with Dockerfile-based deploys having their ports extracted from the docker image or Dockerfile. Additionally, we add an https:443 mapping for any detected http:80 mapping when there is an ssl certificate, and all http port mappings are transformed to https mappings for Dockerfile-based deploys.

While the ports aren't currently consumed, a future refactor will provide the ability to fallback to the new detected ports when there is no user-specified port mapping.
This commit is contained in:
Jose Diaz-Gonzalez
2023-07-16 03:35:40 -04:00
parent b44be8def9
commit 4bc3de5540
16 changed files with 246 additions and 10 deletions

View File

@@ -1292,7 +1292,7 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
- Description: Returns a list of port mappings, newline delimited - Description: Returns a list of port mappings, newline delimited
- Invoked by: Various networking plugins - Invoked by: Various networking plugins
- Arguments `$APP` - Arguments: `$APP`
- Example: - Example:
```shell ```shell
@@ -1307,7 +1307,21 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
- Description: Prints out an available port greater than 1024 - Description: Prints out an available port greater than 1024
- Invoked by: Various networking plugins - Invoked by: Various networking plugins
- Arguments `$APP` - Arguments: `$APP`
- Example:
```shell
#!/usr/bin/env bash
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
# TODO
```
### `ports-set-detected`
- Description: Allows builders to specify detected port mappings for a given app
- Invoked by: Builder plugins
- Arguments: `$APP [$PORT_MAPPING...]`
- Example: - Example:
```shell ```shell
@@ -1480,7 +1494,7 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
- Description: This trigger should be used to do stuff to containers after they are created but before they are started. They are explicitely for commands that may involve network traffic, and _not_ for commands that are self-contained, such as chown or tar. - Description: This trigger should be used to do stuff to containers after they are created but before they are started. They are explicitely for commands that may involve network traffic, and _not_ for commands that are self-contained, such as chown or tar.
- Invoked by: `dokku run`, `dokku ps:rebuild`, `dokku deploy` - Invoked by: `dokku run`, `dokku ps:rebuild`, `dokku deploy`
- Arguments "app|service" "$CONTAINER_ID" "$APP|$SERVICE" "$PHASE" - Arguments: "app|service" "$CONTAINER_ID" "$APP|$SERVICE" "$PHASE"
```shell ```shell
#!/usr/bin/env bash #!/usr/bin/env bash

View File

@@ -212,11 +212,14 @@ dokku ports:report
``` ```
=====> node-js-app ports information =====> node-js-app ports information
Port map: http:80:5000 https:443:5000 Port map detected: http:80:5000
Port map: http:80:5000 https:443:5000
=====> python-sample ports information =====> python-sample ports information
Port map: http:80:5000 Port map detected: http:80:5000
Port map: http:80:5000
=====> ruby-sample ports information =====> ruby-sample ports information
Port map: http:80:5000 Port map detected: http:80:5000
Port map: http:80:5000
``` ```
You can run the command for a specific app also. You can run the command for a specific app also.
@@ -227,7 +230,8 @@ dokku ports:report node-js-app
``` ```
=====> node-js-app ports information =====> node-js-app ports information
Port map: http:80:5000 https:443:5000 Port map detected: http:80:5000
Port map: http:80:5000 https:443:5000
``` ```
You can pass flags which will output only the value of the specific information you want. For example: You can pass flags which will output only the value of the specific information you want. For example:

View File

@@ -0,0 +1 @@
hook

View File

@@ -41,6 +41,10 @@ trigger-builder-dockerfile-builder-build() {
eval "$(config_export app "$APP")" eval "$(config_export app "$APP")"
"$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS "${ARG_ARRAY[@]}" ${DOKKU_DOCKER_BUILD_OPTS} -t $IMAGE . "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS "${ARG_ARRAY[@]}" ${DOKKU_DOCKER_BUILD_OPTS} -t $IMAGE .
set -x
fn-builder-dockerfile-get-detect-port-map "$APP" "$IMAGE" "$SOURCECODE_WORK_DIR/Dockerfile"
plugn trigger ports-set-detected "$APP" "$(fn-builder-dockerfile-get-detect-port-map "$APP" "$IMAGE" "$SOURCECODE_WORK_DIR/Dockerfile")"
set +x
plugn trigger post-build-dockerfile "$APP" plugn trigger post-build-dockerfile "$APP"
} }

View File

@@ -88,3 +88,47 @@ fn-builder-dockerfile-dockerfile-path() {
fn-plugin-property-get-default "builder-dockerfile" "$APP" "dockerfile-path" "" fn-plugin-property-get-default "builder-dockerfile" "$APP" "dockerfile-path" ""
} }
fn-builder-dockerfile-get-ports-from-dockerfile() {
declare desc="return all exposed ports from passed file path"
declare DOCKERFILE_PATH="$1"
suppress_output dos2unix "$DOCKERFILE_PATH"
local ports="$(grep -E "^EXPOSE " "$DOCKERFILE_PATH" | awk '{ print $2 }' | xargs)" || true
echo "$ports"
}
fn-builder-dockerfile-get-ports-from-image() {
declare desc="return all exposed ports from passed image name"
declare IMAGE="$1"
local ports="$("$DOCKER_BIN" image inspect --format "{{range $key, $value := .Config.ExposedPorts}}{{$key}} {{end}}" "$IMAGE" | xargs)" || true
echo "$ports"
}
fn-builder-dockerfile-get-detect-port-map() {
declare desc="extracts and echos a port mapping from the app"
declare APP="$1" IMAGE="$2" DOCKERFILE_PATH="$3"
local detected_ports=$(fn-builder-dockerfile-get-ports-from-dockerfile "$DOCKERFILE_PATH")
if [[ -z "$detected_ports" ]]; then
local detected_ports=$(fn-builder-dockerfile-get-ports-from-image "$IMAGE")
fi
if [[ -n "$detected_ports" ]]; then
local port_map=""
for p in $detected_ports; do
if [[ "$p" =~ .*udp.* ]]; then
p=${p//\/udp/}
port_map+="udp:$p:$p "
else
p=${p//\/tcp/}
port_map+="http:$p:$p "
fi
done
echo "$port_map" | xargs
else
echo "http:80:5000"
fi
}

View File

@@ -91,6 +91,7 @@ trigger-builder-herokuish-builder-build() {
"$DOCKER_BIN" container commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" "$CID" "$IMAGE" >/dev/null "$DOCKER_BIN" container commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" "$CID" "$IMAGE" >/dev/null
plugn trigger scheduler-register-retired "$APP" "$CID" plugn trigger scheduler-register-retired "$APP" "$CID"
plugn trigger ports-set-detected "$APP" "http:80:5000"
plugn trigger post-build-buildpack "$APP" "$SOURCECODE_WORK_DIR" plugn trigger post-build-buildpack "$APP" "$SOURCECODE_WORK_DIR"
} }

View File

@@ -36,6 +36,7 @@ trigger-builder-lambda-builder-build() {
cp "$SOURCECODE_WORK_DIR/Procfile" "${DOKKU_LIB_ROOT}/data/builder-lambda/$APP/$GIT_REV.Procfile" cp "$SOURCECODE_WORK_DIR/Procfile" "${DOKKU_LIB_ROOT}/data/builder-lambda/$APP/$GIT_REV.Procfile"
fi fi
plugn trigger ports-set-detected "$APP" "http:80:5000"
plugn trigger post-build-lambda "$APP" plugn trigger post-build-lambda "$APP"
} }

View File

@@ -35,6 +35,7 @@ trigger-builder-pack-builder-build() {
pack build "$IMAGE" --builder "$DOKKU_CNB_BUILDER" --path "$SOURCECODE_WORK_DIR" --default-process web "${ENV_ARGS[@]}" pack build "$IMAGE" --builder "$DOKKU_CNB_BUILDER" --path "$SOURCECODE_WORK_DIR" --default-process web "${ENV_ARGS[@]}"
docker-image-labeler --label=dokku --label=org.label-schema.schema-version=1.0 --label=org.label-schema.vendor=dokku --label=com.dokku.image-stage=build --label=com.dokku.builder-type=pack --label=com.dokku.app-name=$APP "$IMAGE" docker-image-labeler --label=dokku --label=org.label-schema.schema-version=1.0 --label=org.label-schema.vendor=dokku --label=com.dokku.image-stage=build --label=com.dokku.builder-type=pack --label=com.dokku.app-name=$APP "$IMAGE"
plugn trigger ports-set-detected "$APP" "http:80:5000"
plugn trigger post-build-pack "$APP" "$SOURCECODE_WORK_DIR" plugn trigger post-build-pack "$APP" "$SOURCECODE_WORK_DIR"
} }

View File

@@ -1,5 +1,5 @@
SUBCOMMANDS = subcommands/list subcommands/add subcommands/clear subcommands/remove subcommands/set subcommands/report SUBCOMMANDS = subcommands/list subcommands/add subcommands/clear subcommands/remove subcommands/set subcommands/report
TRIGGERS = triggers/install triggers/ports-clear triggers/ports-configure triggers/ports-dockerfile-raw-tcp-ports triggers/ports-get triggers/ports-get-available triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-certs-remove triggers/post-certs-update triggers/post-delete triggers/report TRIGGERS = triggers/install triggers/ports-clear triggers/ports-configure triggers/ports-dockerfile-raw-tcp-ports triggers/ports-get triggers/ports-get-available triggers/ports-set-detected triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-certs-remove triggers/post-certs-update triggers/post-delete triggers/report
BUILD = commands subcommands triggers BUILD = commands subcommands triggers
PLUGIN_NAME = ports PLUGIN_NAME = ports

View File

@@ -63,6 +63,54 @@ func getAvailablePort() int {
} }
} }
func getDetectedPortMaps(appName string) []PortMap {
defaultMapping := []PortMap{
{
ContainerPort: 5000,
HostPort: 80,
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
for _, portMap := range portMaps {
if portMap.Scheme != "http" || portMap.HostPort != 80 {
continue
}
setSSLPort = true
portMaps = append(portMaps, PortMap{
ContainerPort: portMap.ContainerPort,
HostPort: 443,
Scheme: "https",
})
}
if !setSSLPort {
for i, portMap := range portMaps {
if portMap.Scheme != "http" {
continue
}
portMaps[i].Scheme = "https"
}
}
}
return portMaps
}
func getDockerfileRawTCPPorts(appName string) []int { func getDockerfileRawTCPPorts(appName string) []int {
b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_DOCKERFILE_PORTS"}...) b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_DOCKERFILE_PORTS"}...)
dockerfilePorts := strings.TrimSpace(string(b[:])) dockerfilePorts := strings.TrimSpace(string(b[:]))

View File

@@ -13,7 +13,8 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
} }
flags := map[string]common.ReportFunc{ flags := map[string]common.ReportFunc{
"--ports-map": reportPortMap, "--ports-map": reportPortMap,
"--ports-map-detected": reportPortMapDetected,
} }
flagKeys := []string{} flagKeys := []string{}
@@ -35,3 +36,12 @@ func reportPortMap(appName string) string {
return strings.Join(portMaps, " ") return strings.Join(portMaps, " ")
} }
func reportPortMapDetected(appName string) string {
var portMaps []string
for _, portMap := range getDetectedPortMaps(appName) {
portMaps = append(portMaps, portMap.String())
}
return strings.Join(portMaps, " ")
}

View File

@@ -34,6 +34,10 @@ func main() {
err = ports.TriggerPortsGet(appName) err = ports.TriggerPortsGet(appName)
case "ports-get-available": case "ports-get-available":
err = ports.TriggerPortsGetAvailable() err = ports.TriggerPortsGetAvailable()
case "ports-set-detected":
appName := flag.Arg(0)
appName, portMapString := common.ShiftString(flag.Args())
err = ports.TriggerPortsSetDetected(appName, strings.Join(portMapString, " "))
case "post-app-clone-setup": case "post-app-clone-setup":
oldAppName := flag.Arg(0) oldAppName := flag.Arg(0)
newAppName := flag.Arg(1) newAppName := flag.Arg(1)

View File

@@ -2,6 +2,7 @@ package ports
import ( import (
"fmt" "fmt"
"sort"
"github.com/dokku/dokku/plugins/common" "github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config" "github.com/dokku/dokku/plugins/config"
@@ -101,6 +102,23 @@ func TriggerPortsGetAvailable() error {
return nil return nil
} }
// TriggerPortsGetAvailable prints out an available port greater than 1024
func TriggerPortsSetDetected(appName string, portMapString string) error {
portMaps, _ := parsePortMapString(portMapString)
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-detected", value)
}
// TriggerPostAppCloneSetup creates new ports files // TriggerPostAppCloneSetup creates new ports files
func TriggerPostAppCloneSetup(oldAppName string, newAppName string) error { func TriggerPostAppCloneSetup(oldAppName string, newAppName string) error {
err := common.PropertyClone("ports", oldAppName, newAppName) err := common.PropertyClone("ports", oldAppName, newAppName)

View File

@@ -0,0 +1,9 @@
FROM python:3.11.0-buster
EXPOSE 3001/udp
EXPOSE 3000/tcp
EXPOSE 3003
COPY . /app
WORKDIR /app

View File

@@ -90,7 +90,7 @@ teardown() {
assert_output "http 80 5000" assert_output "http 80 5000"
} }
@test "(ports) ports:add (post-deploy add)" { @test "(ports:add) post-deploy add" {
deploy_app deploy_app
run /bin/bash -c "dokku ports:add $TEST_APP http:8080:5000 http:8081:5000" run /bin/bash -c "dokku ports:add $TEST_APP http:8080:5000 http:8081:5000"
echo "output: $output" echo "output: $output"
@@ -104,3 +104,72 @@ teardown() {
assert_http_success "http://$TEST_APP.dokku.me:8080" assert_http_success "http://$TEST_APP.dokku.me:8080"
assert_http_success "http://$TEST_APP.dokku.me:8081" assert_http_success "http://$TEST_APP.dokku.me:8081"
} }
@test "(ports:report) herokuish tls" {
run /bin/bash -c "dokku builder-herokuish:set $TEST_APP allowed true"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ports:report $TEST_APP --ports-map"
echo "output: $output"
echo "status: $status"
assert_output ""
run /bin/bash -c "dokku ports:report $TEST_APP --ports-map-detected"
echo "output: $output"
echo "status: $status"
assert_output "http:80:5000"
run setup_test_tls
echo "output: $output"
echo "status: $status"
assert_success
run deploy_app
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ports:report $TEST_APP --ports-map"
echo "output: $output"
echo "status: $status"
assert_output "http:80:5000 https:443:5000"
run /bin/bash -c "dokku ports:report $TEST_APP --ports-map-detected"
echo "output: $output"
echo "status: $status"
assert_output "http:80:5000 https:443:5000"
}
@test "(ports:report) dockerfile tls" {
run deploy_app python dokku@dokku.me:$TEST_APP move_expose_dockerfile_into_place
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ports:report $TEST_APP --ports-map"
echo "output: $output"
echo "status: $status"
assert_output "http:3000:3000 http:3003:3003"
run /bin/bash -c "dokku ports:report $TEST_APP --ports-map-detected"
echo "output: $output"
echo "status: $status"
assert_output "http:3000:3000 http:3003:3003 udp:3001:3001"
run setup_test_tls
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku ports:report $TEST_APP --ports-map"
echo "output: $output"
echo "status: $status"
assert_output "http:3000:3000 http:3003:3003"
run /bin/bash -c "dokku ports:report $TEST_APP --ports-map-detected"
echo "output: $output"
echo "status: $status"
assert_output "https:3000:3000 https:3003:3003 udp:3001:3001"
}

View File

@@ -529,6 +529,14 @@ move_dockerfile_into_place() {
mv "$APP_REPO_DIR/alt.Dockerfile" "$APP_REPO_DIR/Dockerfile" mv "$APP_REPO_DIR/alt.Dockerfile" "$APP_REPO_DIR/Dockerfile"
} }
move_expose_dockerfile_into_place() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
cat "$APP_REPO_DIR/expose.Dockerfile"
mv "$APP_REPO_DIR/expose.Dockerfile" "$APP_REPO_DIR/Dockerfile"
}
add_requirements_txt() { add_requirements_txt() {
local APP="$1" local APP="$1"
local APP_REPO_DIR="$2" local APP_REPO_DIR="$2"