feat: implement container network attaching

Containers can be attached:

- after they are created, but before they are started
- after a successful deploy, but before the proxy reloads

This allows folks to have flexibility around when they would like a container to be made available to a network.
This commit is contained in:
Jose Diaz-Gonzalez
2020-02-03 04:37:31 -05:00
parent 8eb32e4371
commit 9818cc6461
13 changed files with 384 additions and 20 deletions

View File

@@ -821,6 +821,20 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
# TODO
```
### `post-container-create`
- 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`
- Arguments "app|service" "$CONTAINER_ID" "$APP|$SERVICE" "$PHASE"
```shell
#!/usr/bin/env bash
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
# TODO
```
### `post-create`
- Description: Can be used to run commands after an app is created.

View File

@@ -98,8 +98,13 @@ execute_script() {
DOKKU_APP_SHELL="$(config_get "$APP" DOKKU_APP_SHELL || echo "$DOKKU_APP_SHELL")"
[[ -z "$DOKKU_APP_SHELL" ]] && DOKKU_APP_SHELL="/bin/bash"
id=$("$DOCKER_BIN" container run "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS --env DOKKU_TRACE="$DOKKU_TRACE" --label=dokku_phase_script="${PHASE_SCRIPT_KEY}" -d -v "$CACHE_HOST_DIR:/cache" "${ARG_ARRAY[@]}" "$IMAGE" "$DOKKU_APP_SHELL" -c "$COMMAND")
if test "$("$DOCKER_BIN" container wait "$id")" -ne 0; then
local DOKKU_CONTAINER_EXIT_CODE=0
id=$("$DOCKER_BIN" container create "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS --env DOKKU_TRACE="$DOKKU_TRACE" --label=dokku_phase_script="${PHASE_SCRIPT_KEY}" -v "$CACHE_HOST_DIR:/cache" "${ARG_ARRAY[@]}" "$IMAGE" "$DOKKU_APP_SHELL" -c "$COMMAND")
plugn trigger post-container-create "app" "$id" "$APP" "$PHASE_SCRIPT_KEY"
"$DOCKER_BIN" container start "$id" >/dev/null || DOKKU_CONTAINER_EXIT_CODE=$?
DOKKU_CONTAINER_EXIT_CODE="$("$DOCKER_BIN" container wait "$id" 2>/dev/null || echo "$DOKKU_CONTAINER_EXIT_CODE")"
if [[ "$DOKKU_CONTAINER_EXIT_CODE" -ne 0 ]]; then
dokku_container_log_verbose_quiet "$id"
dokku_log_fail "execution of '$SCRIPT_CMD' failed!"
fi

View File

@@ -44,10 +44,15 @@ trigger-builder-build-builder-herokuish() {
declare -a ARG_ARRAY
eval "ARG_ARRAY=($DOCKER_ARGS)"
# shellcheck disable=SC2086
CID=$("$DOCKER_BIN" container run "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS -d -v $DOKKU_APP_HOST_CACHE_DIR:/cache -e CACHE_PATH=/cache "${ARG_ARRAY[@]}" $IMAGE /build)
local DOKKU_CONTAINER_EXIT_CODE=0
local CID=$("$DOCKER_BIN" container create "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS -v $DOKKU_APP_HOST_CACHE_DIR:/cache -e CACHE_PATH=/cache "${ARG_ARRAY[@]}" $IMAGE /build)
plugn trigger post-container-create "app" "$CID" "$APP" "build"
"$DOCKER_BIN" container start "$CID" >/dev/null || DOKKU_CONTAINER_EXIT_CODE=$?
"$DOCKER_BIN" container attach "$CID"
if test "$("$DOCKER_BIN" container wait "$CID")" -ne 0; then
DOKKU_CONTAINER_EXIT_CODE="$("$DOCKER_BIN" container wait "$CID" 2>/dev/null || echo "$DOKKU_CONTAINER_EXIT_CODE")"
if [[ "$DOKKU_CONTAINER_EXIT_CODE" -ne 0 ]]; then
dokku_log_warn "Failure during app build"
return 1
fi

View File

@@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"unicode"
@@ -115,6 +116,61 @@ func GetAppImageRepo(appName string) string {
return strings.Join([]string{"dokku", appName}, "/")
}
// GetAppContainerIDs returns a list of docker container ids for given app and optional container_type
func GetAppContainerIDs(appName string, containerType string) ([]string, error) {
var containerIDs []string
if err := VerifyAppName(appName); err != nil {
return containerIDs, err
}
dokkuRoot := MustGetEnv("DOKKU_ROOT")
appRoot := fmt.Sprintf("%v/%v", dokkuRoot, appName)
containerFilePath := fmt.Sprintf("%v/CONTAINER", appRoot)
_, err := os.Stat(containerFilePath)
if !os.IsNotExist(err) {
containerIDs = append(containerIDs, ReadFirstLine(containerFilePath))
}
containerPattern := fmt.Sprintf("%v/CONTAINER.*", appRoot)
if containerType != "" {
containerPattern = fmt.Sprintf("%v/CONTAINER.%v.*", appRoot, containerType)
if strings.Contains(".", containerType) {
containerPattern = fmt.Sprintf("%v/CONTAINER.%v", appRoot, containerType)
}
}
files, _ := filepath.Glob(containerPattern)
for _, containerFile := range files {
containerIDs = append(containerIDs, ReadFirstLine(containerFile))
}
return containerIDs, nil
}
// GetAppRunningContainerIDs return a list of running docker container ids for given app and optional container_type
func GetAppRunningContainerIDs(appName string, containerType string) ([]string, error) {
var runningContainerIDs []string
if err := VerifyAppName(appName); err != nil {
return runningContainerIDs, err
}
if !IsDeployed(appName) {
LogFail(fmt.Sprintf("App %v has not been deployed", appName))
}
containerIDs, err := GetAppContainerIDs(appName, containerType)
if err != nil {
return runningContainerIDs, nil
}
for _, containerID := range containerIDs {
if ContainerIsRunning(containerID) {
runningContainerIDs = append(runningContainerIDs, containerID)
}
}
return runningContainerIDs, nil
}
// ContainerIsRunning checks to see if a container is running
func ContainerIsRunning(containerID string) bool {
b, err := DockerInspect(containerID, "'{{.State.Running}}'")

View File

@@ -1,10 +1,9 @@
/commands
/core-post-deploy
/subcommands/*
/triggers/*
/triggers
/network-*
/install
/post-app-clone-setup
/post-create
/post-delete
/post-*
/report

View File

@@ -1,5 +1,5 @@
SUBCOMMANDS = subcommands/rebuild subcommands/rebuildall subcommands/report subcommands/set
TRIGGERS = triggers/install triggers/network-build-config triggers/network-compute-ports triggers/network-config-exists triggers/network-get-ipaddr triggers/network-get-listeners triggers/network-get-port triggers/network-get-property triggers/network-write-ipaddr triggers/network-write-port triggers/post-app-clone-setup triggers/post-create triggers/post-delete triggers/report
SUBCOMMANDS = subcommands/create subcommands/destroy subcommands/exists subcommands/info subcommands/list subcommands/rebuild subcommands/rebuildall subcommands/report subcommands/set
TRIGGERS = triggers/core-post-deploy triggers/install triggers/network-build-config triggers/network-compute-ports triggers/network-config-exists triggers/network-get-ipaddr triggers/network-get-listeners triggers/network-get-port triggers/network-get-property triggers/network-write-ipaddr triggers/network-write-port triggers/post-app-clone-setup triggers/post-container-create triggers/post-create triggers/post-delete triggers/report
BUILD = commands subcommands triggers
PLUGIN_NAME = network

View File

@@ -1,6 +1,8 @@
package network
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
@@ -19,9 +21,55 @@ var (
// DefaultProperties is a map of all valid network properties with corresponding default property values
DefaultProperties = map[string]string{
"bind-all-interfaces": "false",
"attach-post-create": "",
"attach-post-deploy": "",
"tld": "",
}
)
// AttachAppToNetwork attaches a container to a network
func AttachAppToNetwork(containerID string, networkName string, appName string, phase string, processType string) {
cmdParts := []string{
common.DockerBin(),
"network",
"connect",
}
if phase == "deploy" {
property := "tld"
defaultValue := GetDefaultValue(property)
tld := common.PropertyGetDefault("network", appName, property, defaultValue)
networkAlias := fmt.Sprintf("%v.%v", appName, processType)
if tld != "" {
networkAlias = fmt.Sprintf("%v.%v", networkAlias, tld)
}
cmdParts = append(cmdParts, "--alias")
cmdParts = append(cmdParts, networkAlias)
hostname, err := common.DockerInspect(containerID, "{{ .Config.Hostname }}")
if err != nil {
common.LogWarn(err.Error())
} else {
cmdParts = append(cmdParts, "--alias")
cmdParts = append(cmdParts, fmt.Sprintf("%v.%v", hostname, networkAlias))
}
}
cmdParts = append(cmdParts, networkName)
cmdParts = append(cmdParts, containerID)
attachCmd := common.NewShellCmd(strings.Join(cmdParts, " "))
var stderr bytes.Buffer
attachCmd.ShowOutput = false
attachCmd.Command.Stderr = &stderr
_, err := attachCmd.Output()
if err != nil {
err = errors.New(strings.TrimSpace(stderr.String()))
common.LogFail(fmt.Sprintf("Unable to attach container to network: %v", err.Error()))
}
}
// BuildConfig builds network config files
func BuildConfig(appName string) {
if err := common.VerifyAppName(appName); err != nil {
@@ -186,6 +234,21 @@ func HasNetworkConfig(appName string) bool {
return common.FileExists(ipfile) && common.FileExists(portfile)
}
// ListNetworks returns a list of docker networks
func ListNetworks() ([]string, error) {
b, err := sh.Command(common.DockerBin(), "network", "list", "--format", "{{ .Name }}").Output()
output := strings.TrimSpace(string(b[:]))
networks := []string{}
if err != nil {
common.LogVerboseQuiet(output)
return networks, err
}
networks = strings.Split(output, "\n")
return networks, nil
}
// PostAppCloneSetup removes old IP and PORT files for a newly cloned app
func PostAppCloneSetup(appName string) bool {
dokkuRoot := common.MustGetEnv("DOKKU_ROOT")
@@ -217,6 +280,8 @@ func ReportSingleApp(appName, infoFlag string) {
infoFlags := map[string]string{
"--network-bind-all-interfaces": common.PropertyGet("network", appName, "bind-all-interfaces"),
"--network-attach-post-create": common.PropertyGet("network", appName, "attach-post-create"),
"--network-attach-post-deploy": common.PropertyGet("network", appName, "attach-post-deploy"),
"--network-listeners": strings.Join(GetListeners(appName), " "),
}

View File

@@ -17,6 +17,38 @@ func main() {
flag.Parse()
switch subcommand {
case "create":
networkName := flag.Arg(1)
err := network.CommandCreate(networkName)
if err != nil {
common.LogFail(err.Error())
}
case "destroy":
networkName := flag.Arg(1)
forceDestroy := false
if flag.Arg(2) == "force" {
forceDestroy = true
}
err := network.CommandDestroy(networkName, forceDestroy)
if err != nil {
common.LogFail(err.Error())
}
case "exists":
networkName := flag.Arg(1)
err := network.CommandExists(networkName)
if err != nil {
common.LogFail(err.Error())
}
case "info":
err := network.CommandInfo()
if err != nil {
common.LogFail(err.Error())
}
case "list":
err := network.CommandList()
if err != nil {
common.LogFail(err.Error())
}
case "rebuild":
appName := flag.Arg(1)
network.BuildConfig(appName)

View File

@@ -63,12 +63,22 @@ func main() {
case "post-app-clone-setup":
appName := flag.Arg(1)
network.TriggerPostAppCloneSetup(appName)
case "post-container-create":
containerType := flag.Arg(0)
containerID := flag.Arg(1)
appName := flag.Arg(2)
phase := flag.Arg(3)
processType := flag.Arg(4)
network.TriggerPostContainerCreate(containerType, containerID, appName, phase, processType)
case "post-create":
appName := flag.Arg(0)
network.TriggerPostCreate(appName)
case "post-delete":
appName := flag.Arg(0)
network.TriggerPostDelete(appName)
case "core-post-deploy":
appName := flag.Arg(0)
network.TriggerCorePostDeploy(appName)
case "report":
appName := flag.Arg(0)
network.ReportSingleApp(appName, "")

View File

@@ -1,11 +1,132 @@
package network
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
)
// CommandCreate is an alias for "docker network create"
func CommandCreate(networkName string) error {
common.LogInfo1Quiet(fmt.Sprintf("Creating network %v", networkName))
createCmd := common.NewShellCmd(strings.Join([]string{
common.DockerBin(),
"network",
"create",
"--attachable",
"--label",
"com.dokku.network=true",
"--label",
fmt.Sprintf("com.dokku.network-name=%v", networkName),
networkName,
}, " "))
var stderr bytes.Buffer
createCmd.ShowOutput = false
createCmd.Command.Stderr = &stderr
_, err := createCmd.Output()
if err != nil {
err = errors.New(strings.TrimSpace(stderr.String()))
}
return err
}
// CommandDestroy is an alias for "docker network rm"
func CommandDestroy(networkName string, forceDestroy bool) error {
if os.Getenv("DOKKU_APPS_FORCE_DELETE") == "1" {
forceDestroy = true
}
if !forceDestroy {
common.LogWarn("WARNING: Potentially Destructive Action")
common.LogWarn(fmt.Sprintf("This command will destroy network %v.", networkName))
common.LogWarn(fmt.Sprintf("To proceed, type \"%v\"", networkName))
fmt.Print("> ")
var response string
_, err := fmt.Scanln(&response)
if err != nil {
return err
}
if response != networkName {
common.LogStderr("Confirmation did not match test. Aborted.")
os.Exit(1)
return nil
}
}
common.LogInfo1Quiet(fmt.Sprintf("Destroying network %v", networkName))
destroyCmd := common.NewShellCmd(strings.Join([]string{
common.DockerBin(),
"network",
"rm",
networkName,
}, " "))
var stderr bytes.Buffer
destroyCmd.ShowOutput = false
destroyCmd.Command.Stderr = &stderr
_, err := destroyCmd.Output()
if err != nil {
err = errors.New(strings.TrimSpace(stderr.String()))
}
return err
}
// CommandExists checks if a network exists
func CommandExists(networkName string) error {
exists := false
if networkName == "" {
return errors.New("No network name specified")
}
networks, err := ListNetworks()
if err != nil {
return err
}
for _, n := range networks {
if networkName == n {
exists = true
break
}
}
if exists {
common.LogInfo1Quiet("Network exists")
} else {
common.LogFail("Network does not exist")
}
return nil
}
// CommandInfo is an alias for "docker network inspect"
func CommandInfo() error {
return nil
}
// CommandList is an alias for "docker network ls"
func CommandList() error {
networks, err := ListNetworks()
if err != nil {
return err
}
common.LogInfo2Quiet("Networks")
for _, networkName := range networks {
fmt.Println(networkName)
}
return nil
}
// CommandRebuildall rebuilds network settings for all apps
func CommandRebuildall() {
apps, err := common.DokkuApps()
@@ -44,5 +165,18 @@ func CommandSet(appName string, property string, value string) {
value = "false"
}
attachProperites := map[string]bool{
"attach-post-create": true,
"attach-post-deploy": true,
}
invalidNetworks := map[string]bool{
"host": true,
"bridge": true,
}
if attachProperites[property] && invalidNetworks[value] {
common.LogFail("Invalid network name specified for attach")
}
common.CommandPropertySet("network", appName, property, value, DefaultProperties)
}

View File

@@ -161,6 +161,22 @@ func TriggerPostAppCloneSetup(appName string) {
}
}
// TriggerPostContainerCreate associates the container with a specified network
func TriggerPostContainerCreate(containerType string, containerID string, appName string, phase string, processType string) {
if containerType != "app" {
return
}
property := "attach-post-create"
defaultValue := GetDefaultValue(property)
networkName := common.PropertyGetDefault("network", appName, property, defaultValue)
if networkName == "" {
return
}
AttachAppToNetwork(containerID, networkName, appName, phase, processType)
}
// TriggerPostCreate sets bind-all-interfaces to false by default
func TriggerPostCreate(appName string) {
err := common.PropertyWrite("network", appName, "bind-all-interfaces", "false")
@@ -176,3 +192,27 @@ func TriggerPostDelete(appName string) {
common.LogFail(err.Error())
}
}
// TriggerCorePostDeploy associates the container with a specified network
func TriggerCorePostDeploy(appName string) {
property := "attach-post-deploy"
defaultValue := GetDefaultValue(property)
networkName := common.PropertyGetDefault("network", appName, property, defaultValue)
if networkName == "" {
return
}
common.LogInfo1Quiet(fmt.Sprintf("Associating app with network %s", networkName))
containerIDs, err := common.GetAppRunningContainerIDs(appName, "")
if err != nil {
common.LogFail(err.Error())
}
for _, containerID := range containerIDs {
processType, err := common.DockerInspect(containerID, "{{ index .Config.Labels \"com.dokku.process-type\"}}")
if err != nil {
common.LogFail(err.Error())
}
AttachAppToNetwork(containerID, networkName, appName, "deploy", processType)
}
}

View File

@@ -98,18 +98,21 @@ scheduler-docker-local-scheduler-deploy() {
START_CMD=$(fn-scheduler-docker-local-extract-start-cmd "$APP" "$PROC_TYPE" "$START_CMD" "$DOKKU_HEROKUISH" "$DOKKU_PORT")
if [[ "$DOKKU_NETWORK_BIND_ALL" == "false" ]]; then
# shellcheck disable=SC2086
cid=$("$DOCKER_BIN" container run --label=com.dokku.container-type=deploy --label=com.dokku.process-type=$PROC_TYPE "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS --init --detach --env PORT=$DOKKU_PORT "${ARG_ARRAY[@]}" $IMAGE $START_CMD)
cid=$("$DOCKER_BIN" container create --label=com.dokku.container-type=deploy --label=com.dokku.process-type=$PROC_TYPE "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS --init --env PORT=$DOKKU_PORT "${ARG_ARRAY[@]}" $IMAGE $START_CMD)
else
# shellcheck disable=SC2086
cid=$("$DOCKER_BIN" container run --label=com.dokku.container-type=deploy --label=com.dokku.process-type=$PROC_TYPE "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS --init --detach $DOKKU_DOCKER_PORT_ARGS --env PORT=$DOKKU_PORT "${ARG_ARRAY[@]}" $IMAGE $START_CMD)
cid=$("$DOCKER_BIN" container create --label=com.dokku.container-type=deploy --label=com.dokku.process-type=$PROC_TYPE "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS --init $DOKKU_DOCKER_PORT_ARGS --env PORT=$DOKKU_PORT "${ARG_ARRAY[@]}" $IMAGE $START_CMD)
fi
else
START_CMD=$(fn-scheduler-docker-local-extract-start-cmd "$APP" "$PROC_TYPE" "$START_CMD" "$DOKKU_HEROKUISH")
# shellcheck disable=SC2086
cid=$("$DOCKER_BIN" container run --label=com.dokku.container-type=deploy --label=com.dokku.process-type=$PROC_TYPE "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS --init --detach "${ARG_ARRAY[@]}" $IMAGE $START_CMD)
cid=$("$DOCKER_BIN" container create --label=com.dokku.container-type=deploy --label=com.dokku.process-type=$PROC_TYPE "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS --init "${ARG_ARRAY[@]}" $IMAGE $START_CMD)
fi
plugn trigger post-container-create "app" "$cid" "$APP" "deploy" "$PROC_TYPE"
"$DOCKER_BIN" container start "$cid" >/dev/null || true
ipaddr=$(plugn trigger network-get-ipaddr "$APP" "$PROC_TYPE" "$cid")
port=$(plugn trigger network-get-port "$APP" "$PROC_TYPE" "$DOKKU_HEROKUISH" "$cid")

View File

@@ -76,6 +76,7 @@ scheduler-docker-local-scheduler-run() {
# shellcheck disable=SC2086
CONTAINER_ID="$("$DOCKER_BIN" container create --label=com.dokku.container-type=run "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS "${DOCKER_ARGS_ARRAY[@]}" $IMAGE $EXEC_CMD "$@")"
plugn trigger post-container-create "app" "$CONTAINER_ID" "$APP" "run"
declare -a DOCKER_START_ARGS_ARRAY
if [[ "$DOKKU_DETACH_CONTAINER" != "1" ]]; then
@@ -84,17 +85,17 @@ scheduler-docker-local-scheduler-run() {
has_tty && DOCKER_START_ARGS_ARRAY+=" --interactive"
fi
local EARLY_EXIT_CODE=0 EXIT_CODE=0
local EXIT_CODE=0 DOKKU_CONTAINER_EXIT_CODE=0
if [[ "$DOKKU_DETACH_CONTAINER" == "1" ]]; then
"$DOCKER_BIN" container start ${DOCKER_START_ARGS_ARRAY[@]} "$CONTAINER_ID" >/dev/null || CONTAINER_EXIT_CODE=$?
"$DOCKER_BIN" container start ${DOCKER_START_ARGS_ARRAY[@]} "$CONTAINER_ID" >/dev/null || DOKKU_CONTAINER_EXIT_CODE=$?
echo "$CONTAINER_ID"
else
"$DOCKER_BIN" container start ${DOCKER_START_ARGS_ARRAY[@]} "$CONTAINER_ID" || CONTAINER_EXIT_CODE=$?
EXIT_CODE="$("$DOCKER_BIN" container wait "$CONTAINER_ID" 2>/dev/null || echo "$CONTAINER_EXIT_CODE")"
[[ -z "$EXIT_CODE" ]] && EXIT_CODE=0
"$DOCKER_BIN" container start ${DOCKER_START_ARGS_ARRAY[@]} "$CONTAINER_ID" || EXIT_CODE=$?
DOKKU_CONTAINER_EXIT_CODE="$("$DOCKER_BIN" container wait "$CONTAINER_ID" 2>/dev/null || echo "$EXIT_CODE")"
[[ -z "$DOKKU_CONTAINER_EXIT_CODE" ]] && DOKKU_CONTAINER_EXIT_CODE=0
fi
plugn trigger scheduler-post-run "$DOKKU_SCHEDULER" "$APP" "$CONTAINER_ID" "$EXIT_CODE"
return "$EXIT_CODE"
plugn trigger scheduler-post-run "$DOKKU_SCHEDULER" "$APP" "$CONTAINER_ID" "$DOKKU_CONTAINER_EXIT_CODE"
return "$DOKKU_CONTAINER_EXIT_CODE"
}
scheduler-docker-local-scheduler-run "$@"