Merge pull request #4117 from dokku/go-app-json

This commit is contained in:
Jose Diaz-Gonzalez
2020-08-31 21:22:21 -04:00
committed by GitHub
54 changed files with 1306 additions and 198 deletions

View File

@@ -0,0 +1 @@
hook

View File

@@ -0,0 +1 @@
hook

View File

@@ -0,0 +1 @@
hook

View File

@@ -0,0 +1 @@
hook

4
plugins/app-json/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/triggers/*
/triggers
/pre-*
/post-*

View File

@@ -0,0 +1,5 @@
TRIGGERS = triggers/post-deploy triggers/pre-deploy
BUILD = triggers
PLUGIN_NAME = app-json
include ../../common.mk

380
plugins/app-json/appjson.go Normal file
View File

@@ -0,0 +1,380 @@
package appjson
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
shellquote "github.com/kballard/go-shellquote"
)
// AppJSON is a struct that represents an app.json file as understood by Dokku
type AppJSON struct {
Scripts struct {
Dokku struct {
Predeploy string `json:"predeploy"`
Postdeploy string `json:"postdeploy"`
} `json:"dokku"`
} `json:"scripts"`
}
func constructScript(command string, shell string, isHerokuishImage bool, hasEntrypoint bool) []string {
if hasEntrypoint {
return []string{command}
}
script := []string{"set -eo pipefail;"}
if os.Getenv("DOKKU_TRACE") == "1" {
script = append(script, "set -x;")
}
if isHerokuishImage {
script = append(script, []string{
"if [[ -d '/app' ]]; then",
" export HOME=/app;",
" cd $HOME;",
"fi;",
"if [[ -d '/app/.profile.d' ]]; then",
" for file in /app/.profile.d/*; do source $file; done;",
"fi;",
"if [[ -d '/cache' ]]; then",
" rm -rf /tmp/cache ;",
" ln -sf /cache /tmp/cache;",
"fi;",
}...)
}
if strings.HasPrefix(command, "/") {
commandBin := strings.Split(command, " ")[0]
script = append(script, []string{
fmt.Sprintf("if [[ ! -x \"%s\" ]]; then", commandBin),
" echo specified binary is not executable;",
" exit 1;",
"fi;",
}...)
}
script = append(script, fmt.Sprintf("%s || exit 1;", command))
if isHerokuishImage {
script = append(script, []string{
"if [[ -d '/cache' ]]; then",
" rm -f /tmp/cache;",
"fi;",
}...)
}
return []string{shell, "-c", strings.Join(script, " ")}
}
// getPhaseScript extracts app.json from app image and returns the appropriate json key/value
func getPhaseScript(appName string, image string, phase string) (string, error) {
appJSONFile, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("dokku-%s-%s", common.MustGetEnv("DOKKU_PID"), "getPhaseScript"))
if err != nil {
return "", fmt.Errorf("Cannot create temporary file: %v", err)
}
defer os.Remove(appJSONFile.Name())
common.CopyFromImage(appName, image, "app.json", appJSONFile.Name())
if !common.FileExists(appJSONFile.Name()) {
return "", nil
}
b, err := ioutil.ReadFile(appJSONFile.Name())
if err != nil {
return "", fmt.Errorf("Cannot read app.json file: %v", err)
}
if strings.TrimSpace(string(b)) == "" {
return "", nil
}
var appJSON AppJSON
if err = json.Unmarshal(b, &appJSON); err != nil {
return "", fmt.Errorf("Cannot parse app.json: %v", err)
}
if phase == "predeploy" {
return appJSON.Scripts.Dokku.Predeploy, nil
}
return appJSON.Scripts.Dokku.Postdeploy, nil
}
// getReleaseCommand extracts the release command from a given app's procfile
func getReleaseCommand(appName string, image string) string {
forceExtract := "true"
err := common.SuppressOutput(func() error {
return common.PlugnTrigger("procfile-extract", []string{appName, image, forceExtract}...)
})
if err != nil {
return ""
}
processType := "release"
port := "5000"
b, _ := common.PlugnTriggerOutput("procfile-get-command", []string{appName, processType, port}...)
return strings.TrimSpace(string(b[:]))
}
func getDokkuAppShell(appName string) string {
dokkuAppShell := "/bin/bash"
if b, _ := common.PlugnTriggerOutput("config-get-global", []string{"DOKKU_APP_SHELL"}...); strings.TrimSpace(string(b[:])) != "" {
dokkuAppShell = strings.TrimSpace(string(b[:]))
}
if b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_APP_SHELL"}...); strings.TrimSpace(string(b[:])) != "" {
dokkuAppShell = strings.TrimSpace(string(b[:]))
}
return dokkuAppShell
}
func executeScript(appName string, imageTag string, phase string) error {
image := common.GetDeployingAppImageName(appName, imageTag, "")
command := ""
phaseSource := ""
if phase == "release" {
command = getReleaseCommand(appName, image)
phaseSource = "Procfile"
} else {
var err error
phaseSource = "app.json"
if command, err = getPhaseScript(appName, image, phase); err != nil {
common.LogExclaim(err.Error())
}
}
if command == "" {
return nil
}
common.LogInfo1(fmt.Sprintf("Executing %s task from %s: %s", phase, phaseSource, command))
isHerokuishImage := common.IsImageHerokuishBased(image, appName)
dockerfileEntrypoint := ""
dockerfileCommand := ""
if !isHerokuishImage {
dockerfileEntrypoint, _ = getEntrypointFromImage(image)
dockerfileCommand, _ = getCommandFromImage(image)
}
hasEntrypoint := dockerfileEntrypoint != ""
dokkuAppShell := getDokkuAppShell(appName)
script := constructScript(command, dokkuAppShell, isHerokuishImage, hasEntrypoint)
imageSourceType := "dockerfile"
if isHerokuishImage {
imageSourceType = "herokuish"
}
cacheDir := fmt.Sprintf("%s/cache", common.AppRoot(appName))
cacheHostDir := fmt.Sprintf("%s/cache", common.AppHostRoot(appName))
if !common.DirectoryExists(cacheDir) {
os.MkdirAll(cacheDir, 0755)
}
var dockerArgs []string
if b, err := common.PlugnTriggerSetup("docker-args-deploy", []string{appName, imageTag}...).SetInput("").Output(); err == nil {
words, err := shellquote.Split(strings.TrimSpace(string(b[:])))
if err != nil {
return err
}
dockerArgs = append(dockerArgs, words...)
}
if b, err := common.PlugnTriggerSetup("docker-args-process-deploy", []string{appName, imageSourceType, imageTag}...).SetInput("").Output(); err == nil {
words, err := shellquote.Split(strings.TrimSpace(string(b[:])))
if err != nil {
return err
}
dockerArgs = append(dockerArgs, words...)
}
filteredArgs := []string{"restart", "cpus", "memory", "memory-swap", "memory-reservation", "gpus"}
for _, filteredArg := range filteredArgs {
// re := regexp.MustCompile("--" + filteredArg + "=[0-9A-Za-z!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~]+ ")
var filteredDockerArgs []string
for _, dockerArg := range dockerArgs {
if !strings.HasPrefix(dockerArg, "--"+filteredArg+"=") {
filteredDockerArgs = append(filteredDockerArgs, dockerArg)
}
}
dockerArgs = filteredDockerArgs
}
dockerArgs = append(dockerArgs, "--label=dokku_phase_script="+phase)
dockerArgs = append(dockerArgs, "-v", cacheHostDir+":/cache")
if os.Getenv("DOKKU_TRACE") != "" {
dockerArgs = append(dockerArgs, "--env", "DOKKU_TRACE="+os.Getenv("DOKKU_TRACE"))
}
containerID, err := createdContainerID(appName, dockerArgs, image, script, phase)
if err != nil {
common.LogFail(fmt.Sprintf("Failed to create %s execution container: %s", phase, err.Error()))
}
if !waitForExecution(containerID) {
common.LogInfo2Quiet(fmt.Sprintf("Start of %s %s task (%s) output", appName, phase, containerID[0:9]))
common.LogVerboseQuietContainerLogs(containerID)
common.LogInfo2Quiet(fmt.Sprintf("End of %s %s task (%s) output", appName, phase, containerID[0:9]))
common.LogFail(fmt.Sprintf("Execution of %s task failed: %s", phase, command))
}
common.LogInfo2Quiet(fmt.Sprintf("Start of %s %s task (%s) output", appName, phase, containerID[0:9]))
common.LogVerboseQuietContainerLogs(containerID)
common.LogInfo2Quiet(fmt.Sprintf("End of %s %s task (%s) output", appName, phase, containerID[0:9]))
if phase != "predeploy" {
return nil
}
commitArgs := []string{"container", "commit"}
if !isHerokuishImage {
if dockerfileEntrypoint != "" {
commitArgs = append(commitArgs, "--change", dockerfileEntrypoint)
}
if dockerfileCommand != "" {
commitArgs = append(commitArgs, "--change", dockerfileCommand)
}
}
commitArgs = append(commitArgs, []string{
"--change",
"LABEL org.label-schema.schema-version=1.0",
"--change",
"LABEL org.label-schema.vendor=dokku",
"--change",
fmt.Sprintf("LABEL com.dokku.app-name=%s", appName),
"--change",
fmt.Sprintf("LABEL com.dokku.%s-phase=true", phase),
}...)
commitArgs = append(commitArgs, containerID, image)
containerCommitCmd := common.NewShellCmdWithArgs(
common.DockerBin(),
commitArgs...,
)
containerCommitCmd.ShowOutput = false
containerCommitCmd.Command.Stderr = os.Stderr
if !containerCommitCmd.Execute() {
common.LogFail(fmt.Sprintf("Commiting of '%s' to image failed: %s", phase, command))
}
return nil
}
func getEntrypointFromImage(image string) (string, error) {
output, err := common.DockerInspect(image, "{{json .Config.Entrypoint}}")
if err != nil {
return "", err
}
if output == "null" {
return "", err
}
var entrypoint []string
if err = json.Unmarshal([]byte(output), &entrypoint); err != nil {
return "", err
}
if len(entrypoint) == 3 && entrypoint[0] == "/bin/sh" && entrypoint[1] == "-c" {
return fmt.Sprintf("ENTRYPOINT %s", entrypoint[2]), nil
}
serializedEntrypoint, err := json.Marshal(entrypoint)
return fmt.Sprintf("ENTRYPOINT %s", string(serializedEntrypoint)), err
}
func getCommandFromImage(image string) (string, error) {
output, err := common.DockerInspect(image, "{{json .Config.Cmd}}")
if err != nil {
return "", err
}
if output == "null" {
return "", err
}
var command []string
if err = json.Unmarshal([]byte(output), &command); err != nil {
return "", err
}
if len(command) == 3 && command[0] == "/bin/sh" && command[1] == "-c" {
return fmt.Sprintf("CMD %s", command[2]), nil
}
serializedEntrypoint, err := json.Marshal(command)
return fmt.Sprintf("CMD %s", string(serializedEntrypoint)), err
}
func waitForExecution(containerID string) bool {
containerStartCmd := common.NewShellCmdWithArgs(
common.DockerBin(),
"container",
"start",
containerID,
)
containerStartCmd.ShowOutput = false
containerStartCmd.Command.Stderr = os.Stderr
if !containerStartCmd.Execute() {
return false
}
containerWaitCmd := common.NewShellCmdWithArgs(
common.DockerBin(),
"container",
"wait",
containerID,
)
containerWaitCmd.ShowOutput = false
containerWaitCmd.Command.Stderr = os.Stderr
b, err := containerWaitCmd.Output()
if err != nil {
return false
}
containerExitCode := strings.TrimSpace(string(b[:]))
return containerExitCode == "0"
}
func createdContainerID(appName string, dockerArgs []string, image string, command []string, phase string) (string, error) {
runLabelArgs := fmt.Sprintf("--label=com.dokku.app-name=%s", appName)
arguments := strings.Split(common.MustGetEnv("DOKKU_GLOBAL_RUN_ARGS"), " ")
arguments = append(arguments, runLabelArgs)
arguments = append(arguments, dockerArgs...)
arguments = append([]string{"container", "create"}, arguments...)
arguments = append(arguments, image)
arguments = append(arguments, command...)
containerCreateCmd := common.NewShellCmdWithArgs(
common.DockerBin(),
arguments...,
)
var stderr bytes.Buffer
containerCreateCmd.ShowOutput = false
containerCreateCmd.Command.Stderr = &stderr
b, err := containerCreateCmd.Output()
if err != nil {
return "", errors.New(stderr.String())
}
containerID := strings.TrimSpace(string(b))
err = common.PlugnTrigger("post-container-create", []string{"app", appName, containerID, phase}...)
return containerID, err
}

12
plugins/app-json/go.mod Normal file
View File

@@ -0,0 +1,12 @@
module github.com/dokku/dokku/plugins/app-json
go 1.14
require (
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 // indirect
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 // indirect
)
replace github.com/dokku/dokku/plugins/common => ../common

8
plugins/app-json/go.sum Normal file
View File

@@ -0,0 +1,8 @@
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 h1:HHUr4P/aKh4quafGxDT9LDasjGdlGkzLbfmmrlng3kA=
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 h1:utdYOikI1XjNtTFGCwSM6OmFJblU4ld4gACoJsbadJg=
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=

View File

@@ -1,133 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
get_phase_script() {
declare desc="extracts app.json from app image and returns the appropriate json key/value"
declare IMAGE_TAG="$1" PHASE_SCRIPT_KEY="$2"
local APP_JSON_FILE=$(mktemp "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX")
trap "rm -rf '$APP_JSON_FILE' >/dev/null" RETURN INT TERM
copy_from_image "$IMAGE" "app.json" "$APP_JSON_FILE" 2>/dev/null || true
if [[ ! -f "$APP_JSON_FILE" ]]; then
return 0
fi
get_json_value ".scripts.dokku.${PHASE_SCRIPT_KEY}" <"$APP_JSON_FILE"
}
get_release_cmd() {
declare desc="extracts the release command from a given app's procfile"
declare APP="$1" IMAGE_TAG="$2"
extract_procfile "$APP" "$IMAGE_TAG" >/dev/null
trap "remove_procfile $APP" RETURN INT TERM EXIT
get_cmd_from_procfile "$APP" "release" "5000"
}
execute_script() {
declare desc="executes appropriate phase script key from app.json"
declare APP="$1" IMAGE_TAG="$2" PHASE_SCRIPT_KEY="$3"
local IMAGE id SCRIPT_CMD
local DOCKER_COMMIT_LABEL_ARGS=("--change" "\"LABEL org.label-schema.schema-version=1.0\"" "--change" "\"LABEL org.label-schema.vendor=dokku\"" "--change" "\"LABEL com.dokku.app-name=$APP\"")
local DOCKER_RUN_LABEL_ARGS="--label=com.dokku.app-name=$APP"
IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG")
if [[ "$PHASE_SCRIPT_KEY" == "release" ]]; then
SCRIPT_CMD=$(get_release_cmd "$APP" "$IMAGE_TAG" 2>/dev/null || true)
else
SCRIPT_CMD=$(get_phase_script "$IMAGE" "$PHASE_SCRIPT_KEY" 2>/dev/null || true)
fi
if [[ -z "$SCRIPT_CMD" ]]; then
return
fi
dokku_log_exclaim "${PHASE_SCRIPT_KEY^} command declared: '$SCRIPT_CMD'"
local COMMAND
COMMAND="set -eo pipefail; [[ \$DOKKU_TRACE ]] && set -x ; "
COMMAND+=" if [[ -d '/app' ]]; then "
COMMAND+=" export HOME=/app ; "
COMMAND+=" cd \$HOME ; "
COMMAND+=" fi ; "
COMMAND+=" if [[ -d '/app/.profile.d' ]]; then "
COMMAND+=" for file in /app/.profile.d/*; do source \$file; done ; "
COMMAND+=" fi ; "
COMMAND+=" if [[ -d '/cache' ]]; then "
COMMAND+=" rm -rf /tmp/cache ; "
COMMAND+=" ln -sf /cache /tmp/cache ; "
COMMAND+=" fi ; "
if [[ "$SCRIPT_CMD" == /* ]]; then
local SCRIPT_BIN="$(echo "$SCRIPT_CMD" | cut -d' ' -f1)"
COMMAND+=" if [[ ! -x \"$SCRIPT_BIN\" ]]; then "
COMMAND+=" echo specified binary is not executable ; "
COMMAND+=" exit 1 ; "
COMMAND+=" fi ; "
fi
COMMAND+=" $SCRIPT_CMD || exit 1;"
COMMAND+=" if [[ -d '/cache' ]]; then "
COMMAND+=" rm -f /tmp/cache ; "
COMMAND+=" fi ; "
local CACHE_DIR="$DOKKU_ROOT/$APP/cache"
local CACHE_HOST_DIR="$DOKKU_HOST_ROOT/$APP/cache"
[[ -d $CACHE_DIR ]] || mkdir -p "$CACHE_DIR"
local DOCKER_ARGS=$(: | plugn trigger docker-args-deploy "$APP" "$IMAGE_TAG")
local IMAGE_SOURCE_TYPE="dockerfile"
is_image_herokuish_based "$IMAGE" "$APP" && IMAGE_SOURCE_TYPE="herokuish"
DOCKER_ARGS+=$(: | plugn trigger docker-args-process-deploy "$APP" "$IMAGE_SOURCE_TYPE" "$IMAGE_TAG")
# strip certain args from DOCKER_ARGS
local filtered_args=("restart" "cpus" "memory" "memory-swap" "memory-reservation" "gpus")
for filtered_arg in "${filtered_args[@]}"; do
DOCKER_ARGS=$(sed -e "s/--${filtered_arg}=[[:graph:]]\+[[:blank:]]\?//g" <<<"$DOCKER_ARGS")
done
# eval args as array to respect escapes
declare -a ARG_ARRAY
eval "ARG_ARRAY=($DOCKER_ARGS)"
local DOKKU_APP_SHELL="/bin/bash"
DOKKU_APP_SHELL="$(config_get --global DOKKU_APP_SHELL || echo "$DOKKU_APP_SHELL")"
DOKKU_APP_SHELL="$(config_get "$APP" DOKKU_APP_SHELL || echo "$DOKKU_APP_SHELL")"
[[ -z "$DOKKU_APP_SHELL" ]] && DOKKU_APP_SHELL="/bin/bash"
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
dokku_container_log_verbose_quiet "$id"
if [[ "$PHASE_SCRIPT_KEY" != "predeploy" ]]; then
return
fi
if ! is_image_herokuish_based "$IMAGE" "$APP"; then
local DOKKU_DOCKERFILE_ENTRYPOINT=$(config_get "$APP" DOKKU_DOCKERFILE_ENTRYPOINT)
local DOKKU_DOCKERFILE_CMD=$(config_get "$APP" DOKKU_DOCKERFILE_CMD)
[[ -z "$DOKKU_DOCKERFILE_ENTRYPOINT" ]] && DOKKU_DOCKERFILE_ENTRYPOINT="$(get_entrypoint_from_image "$IMAGE")"
[[ -z "$DOKKU_DOCKERFILE_CMD" ]] && DOKKU_DOCKERFILE_CMD="$(get_cmd_from_image "$IMAGE")"
[[ -n "$DOKKU_DOCKERFILE_ENTRYPOINT" ]] && local DOCKER_COMMIT_ENTRYPOINT_CHANGE_ARG="--change='$DOKKU_DOCKERFILE_ENTRYPOINT'"
[[ -n "$DOKKU_DOCKERFILE_CMD" ]] && local DOCKER_COMMIT_CMD_CHANGE_ARG="--change='$DOKKU_DOCKERFILE_CMD'"
local DOCKER_COMMIT_ARGS="$DOCKER_COMMIT_ENTRYPOINT_CHANGE_ARG $DOCKER_COMMIT_CMD_CHANGE_ARG"
fi
eval "$DOCKER_BIN" container commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" "${DOCKER_COMMIT_ARGS[@]}" "$id" "$IMAGE" >/dev/null
}

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_CORE_AVAILABLE_PATH/app-json/internal-functions"
trigger-app-json-post-deploy() {
declare desc="app-json scripts execution"
declare trigger="post-deploy"
declare APP="$1" IMAGE_TAG="$4"
local PHASE_SCRIPT_KEY="postdeploy"
execute_script "$APP" "$IMAGE_TAG" "$PHASE_SCRIPT_KEY"
}
trigger-app-json-post-deploy "$@"

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_CORE_AVAILABLE_PATH/app-json/internal-functions"
trigger-app-json-pre-deploy() {
declare desc="app-json scripts execution"
declare trigger="pre-deploy"
declare APP="$1" IMAGE_TAG="$2"
local PHASE_SCRIPT_KEY="predeploy"
execute_script "$APP" "$IMAGE_TAG" "$PHASE_SCRIPT_KEY"
execute_script "$APP" "$IMAGE_TAG" "release"
}
trigger-app-json-pre-deploy "$@"

View File

@@ -0,0 +1,36 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/app-json"
"github.com/dokku/dokku/plugins/common"
)
// main entrypoint to all triggers
func main() {
parts := strings.Split(os.Args[0], "/")
trigger := parts[len(parts)-1]
flag.Parse()
var err error
switch trigger {
case "post-deploy":
appName := flag.Arg(0)
imageTag := flag.Arg(3)
err = appjson.TriggerPostDeploy(appName, imageTag)
case "pre-deploy":
appName := flag.Arg(0)
imageTag := flag.Arg(1)
err = appjson.TriggerPreDeploy(appName, imageTag)
default:
common.LogFail(fmt.Sprintf("Invalid plugin trigger call: %s", trigger))
}
if err != nil {
common.LogFail(err.Error())
}
}

View File

@@ -0,0 +1,21 @@
package appjson
// TriggerPostDeploy is a trigger to execute the postdeploy deployment task
func TriggerPostDeploy(appName string, imageTag string) error {
if err := executeScript(appName, imageTag, "postdeploy"); err != nil {
return err
}
return nil
}
// TriggerPreDeploy is a trigger to execute predeploy and release deployment tasks
func TriggerPreDeploy(appName string, imageTag string) error {
if err := executeScript(appName, imageTag, "predeploy"); err != nil {
return err
}
if err := executeScript(appName, imageTag, "release"); err != nil {
return err
}
return nil
}

View File

@@ -20,12 +20,6 @@ trigger-builder-dockerfile-builder-build() {
# extract first port from Dockerfile
local DOCKERFILE_PORTS=$(get_dockerfile_exposed_ports Dockerfile)
[[ -n "$DOCKERFILE_PORTS" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_PORTS="$DOCKERFILE_PORTS"
# extract ENTRYPOINT/CMD from Dockerfile
local DOCKERFILE_ENTRYPOINT=$(extract_directive_from_dockerfile Dockerfile ENTRYPOINT)
[[ -n "$DOCKERFILE_ENTRYPOINT" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_ENTRYPOINT="$DOCKERFILE_ENTRYPOINT"
local DOCKERFILE_CMD=$(extract_directive_from_dockerfile Dockerfile CMD)
[[ -n "$DOCKERFILE_CMD" ]] && config_set --no-restart "$APP" DOKKU_DOCKERFILE_CMD="$DOCKERFILE_CMD"
plugn trigger pre-build-dockerfile "$APP"
[[ "$DOKKU_DOCKERFILE_CACHE_BUILD" == "false" ]] && DOKKU_DOCKER_BUILD_OPTS="$DOKKU_DOCKER_BUILD_OPTS --no-cache"

View File

@@ -35,16 +35,22 @@ func NewShellCmd(command string) *ShellCmd {
items := strings.Split(command, " ")
cmd := items[0]
args := items[1:]
return NewShellCmdWithArgs(cmd, args...)
}
// NewShellCmdWithArgs returns a new ShellCmd struct
func NewShellCmdWithArgs(cmd string, args ...string) *ShellCmd {
commandString := strings.Join(append([]string{cmd}, args...), " ")
return &ShellCmd{
Command: exec.Command(cmd, args...),
CommandString: command,
CommandString: commandString,
Args: args,
ShowOutput: true,
}
}
// Execute is a lightweight wrapper around exec.Command
func (sc *ShellCmd) Execute() bool {
func (sc *ShellCmd) setup() {
env := os.Environ()
for k, v := range sc.Env {
env = append(env, fmt.Sprintf("%s=%s", k, v))
@@ -54,6 +60,12 @@ func (sc *ShellCmd) Execute() bool {
sc.Command.Stdout = os.Stdout
sc.Command.Stderr = os.Stderr
}
}
// Execute is a lightweight wrapper around exec.Command
func (sc *ShellCmd) Execute() bool {
sc.setup()
if err := sc.Command.Run(); err != nil {
return false
}
@@ -62,24 +74,28 @@ func (sc *ShellCmd) Execute() bool {
// Output is a lightweight wrapper around exec.Command.Output()
func (sc *ShellCmd) Output() ([]byte, error) {
env := os.Environ()
for k, v := range sc.Env {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
sc.Command.Env = env
if sc.ShowOutput {
sc.Command.Stdout = os.Stdout
sc.Command.Stderr = os.Stderr
}
sc.setup()
return sc.Command.Output()
}
// CombinedOutput is a lightweight wrapper around exec.Command.CombinedOutput()
func (sc *ShellCmd) CombinedOutput() ([]byte, error) {
sc.setup()
return sc.Command.CombinedOutput()
}
// AppRoot returns the app root path
func AppRoot(appName string) string {
dokkuRoot := MustGetEnv("DOKKU_ROOT")
return fmt.Sprintf("%v/%v", dokkuRoot, appName)
}
// AppHostRoot returns the app root path
func AppHostRoot(appName string) string {
dokkuHostRoot := MustGetEnv("DOKKU_HOST_ROOT")
return fmt.Sprintf("%v/%v", dokkuHostRoot, appName)
}
// AskForDestructiveConfirmation checks for confirmation on destructive actions
func AskForDestructiveConfirmation(name string, objectType string) error {
LogWarn("WARNING: Potentially Destructive Action")
@@ -112,6 +128,107 @@ func CommandUsage(helpHeader string, helpContent string) {
fmt.Println(columnize.Format(content, config))
}
// CopyFromImage copies a file from named image to destination
func CopyFromImage(appName string, image string, source string, destination string) error {
if err := VerifyAppName(appName); err != nil {
return err
}
if !VerifyImage(image) {
return fmt.Errorf("Invalid docker image for copying content")
}
workDir := ""
if !IsAbsPath(source) {
if IsImageHerokuishBased(image, appName) {
workDir = "/app"
} else {
workDir, _ = DockerInspect(image, "{{.Config.WorkingDir}}")
}
if workDir != "" {
source = fmt.Sprintf("%s/%s", workDir, source)
}
}
tmpFile, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("dokku-%s-%s", MustGetEnv("DOKKU_PID"), "CopyFromImage"))
if err != nil {
return fmt.Errorf("Cannot create temporary file: %v", err)
}
defer tmpFile.Close()
defer os.Remove(tmpFile.Name())
globalRunArgs := MustGetEnv("DOKKU_GLOBAL_RUN_ARGS")
createLabelArgs := []string{"--label", fmt.Sprintf("com.dokku.app-name=%s", appName), globalRunArgs}
containerID, err := DockerContainerCreate(image, createLabelArgs)
if err != nil {
return fmt.Errorf("Unable to create temporary container: %v", err)
}
// docker cp exits with status 1 when run as non-root user when it tries to chown the file
// after successfully copying the file. Thus, we suppress stderr.
// ref: https://github.com/dotcloud/docker/issues/3986
containerCopyCmd := NewShellCmd(strings.Join([]string{
DockerBin(),
"container",
"cp",
fmt.Sprintf("%s:%s", containerID, source),
tmpFile.Name(),
}, " "))
containerCopyCmd.ShowOutput = false
fileCopied := containerCopyCmd.Execute()
containerRemoveCmd := NewShellCmd(strings.Join([]string{
DockerBin(),
"container",
"rm",
"--force",
containerID,
}, " "))
containerRemoveCmd.ShowOutput = false
containerRemoveCmd.Execute()
if !fileCopied {
return fmt.Errorf("Unable to copy file %s from image", source)
}
fi, err := os.Stat(tmpFile.Name())
if err != nil {
return err
}
if fi.Size() == 0 {
return fmt.Errorf("Unable to copy file %s from image", source)
}
// workaround for CHECKS file when owner is root. seems to only happen when running inside docker
dos2unixCmd := NewShellCmd(strings.Join([]string{
"dos2unix",
"-l",
"-n",
tmpFile.Name(),
destination,
}, " "))
dos2unixCmd.ShowOutput = false
dos2unixCmd.Execute()
// add trailing newline for certain places where file parsing depends on it
b, err := sh.Command("tail", "-c1", destination).Output()
if string(b) != "" {
f, err := os.OpenFile(destination, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString("\n"); err != nil {
return fmt.Errorf("Unable to append trailing newline to copied file: %v", err)
}
}
return nil
}
// DockerCleanup cleans up all exited/dead containers and removes all dangling images
func DockerCleanup(appName string, forceCleanup bool) error {
if !forceCleanup {
@@ -331,7 +448,30 @@ func DirectoryExists(filePath string) bool {
return fi.IsDir()
}
// DockerInspect runs an inspect command with a given format against a container id
// DockerContainerCreate creates a new container and returns the container ID
func DockerContainerCreate(image string, containerCreateArgs []string) (string, error) {
cmd := []string{
DockerBin(),
"container",
"create",
}
cmd = append(cmd, containerCreateArgs...)
cmd = append(cmd, image)
var stderr bytes.Buffer
containerCreateCmd := NewShellCmd(strings.Join(cmd, " "))
containerCreateCmd.ShowOutput = false
containerCreateCmd.Command.Stderr = &stderr
b, err := containerCreateCmd.Output()
if err != nil {
return "", errors.New(strings.TrimSpace(stderr.String()))
}
return strings.TrimSpace(string(b[:])), nil
}
// DockerInspect runs an inspect command with a given format against a container or image ID
func DockerInspect(containerOrImageID, format string) (output string, err error) {
b, err := sh.Command(DockerBin(), "inspect", "--format", format, containerOrImageID).Output()
if err != nil {
@@ -424,6 +564,11 @@ func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) {
return
}
// IsAbsPath returns 0 if input path is absolute
func IsAbsPath(path string) bool {
return strings.HasPrefix(path, "/")
}
// IsDeployed returns true if given app has a running container
func IsDeployed(appName string) bool {
files, err := ioutil.ReadDir(AppRoot(appName))

View File

@@ -541,7 +541,7 @@ is_app_running() {
declare deprecated=true
local APP="$1"
dokku_log_warn "Deprecated: ps#fn-is-app-running"
dokku_log_warn "Deprecated: ps#fn-ps-is-app-running"
verify_app_name "$APP"
local APP_RUNNING_CONTAINER_IDS=$(get_app_running_container_ids "$APP" 2>/dev/null)

View File

@@ -3,6 +3,7 @@ package common
import (
"fmt"
"os"
"strings"
)
// LogFail is the failure log formatter
@@ -71,12 +72,45 @@ func LogVerboseQuiet(text string) {
}
}
// LogVerboseQuietContainerLogs is the verbose log formatter for container logs
func LogVerboseQuietContainerLogs(containerID string) {
sc := NewShellCmdWithArgs(DockerBin(), "container", "logs", containerID)
sc.ShowOutput = false
b, err := sc.CombinedOutput()
if err != nil {
LogExclaim(fmt.Sprintf("Failed to fetch container logs: %s", containerID))
}
output := strings.TrimSpace(string(b))
if len(output) == 0 {
return
}
for _, line := range strings.Split(output, "\n") {
if line != "" {
LogVerboseQuiet(line)
}
}
}
// LogWarn is the warning log formatter
func LogWarn(text string) {
fmt.Fprintln(os.Stderr, fmt.Sprintf(" ! %s", text))
}
// LogExclaim is the log exclaim formatter
func LogExclaim(text string) {
fmt.Println(fmt.Sprintf(" ! %s", text))
}
// LogStderr is the stderr log formatter
func LogStderr(text string) {
fmt.Fprintln(os.Stderr, text)
}
// LogDebug is the debug log formatter
func LogDebug(text string) {
if os.Getenv("DOKKU_TRACE") == "" {
fmt.Fprintln(os.Stderr, fmt.Sprintf(" ? %s", text))
}
}

View File

@@ -11,8 +11,7 @@ get_cmd_from_procfile() {
local COMMAND
verify_app_name "$APP"
[[ ! -f $DOKKU_PROCFILE ]] && return
procfile-util show --procfile "$DOKKU_PROCFILE" --process-type "$PROC_TYPE" --default-port "$PORT"
fn-procfile-get-command "$DOKKU_PROCFILE" "$PROC_TYPE" "$PORT"
}
print_dokku_scale_file() {
@@ -31,11 +30,18 @@ extract_procfile() {
verify_app_name "$APP"
remove_procfile "$APP"
copy_from_image "$IMAGE" "Procfile" "$DOKKU_PROCFILE" 2>/dev/null || true
if [[ -f "$DOKKU_PROCFILE" ]]; then
fn-procfile-extract "$IMAGE" "$DOKKU_PROCFILE"
}
fn-procfile-extract() {
declare desc="extract procfile from a specified docker image"
declare IMAGE="$1" PROCFILE_PATH="$2"
copy_from_image "$IMAGE" "Procfile" "$PROCFILE_PATH" 2>/dev/null || true
if [[ -f "$PROCFILE_PATH" ]]; then
dokku_log_info1_quiet "App Procfile file found"
# shellcheck disable=SC2069
PROCFILE_ERRORS="$(procfile-util check --procfile "$DOKKU_PROCFILE" 2>&1 >/dev/null || true)"
PROCFILE_ERRORS="$(procfile-util check --procfile "$PROCFILE_PATH" 2>&1 >/dev/null || true)"
if [[ -n "$PROCFILE_ERRORS" ]]; then
dokku_log_fail "${PROCFILE_ERRORS}"
fi
@@ -44,6 +50,17 @@ extract_procfile() {
fi
}
fn-procfile-get-command() {
declare desc="Fetch a command from a Procfile"
declare PROCFILE_PATH="$1" PROCESS_TYPE="$2" PORT="$3"
if [[ ! -f "$PROCFILE_PATH" ]]; then
return
fi
procfile-util show --procfile "$PROCFILE_PATH" --process-type "$PROCESS_TYPE" --default-port "$PORT" 2>/dev/null
}
remove_procfile() {
declare desc="removes DOKKU_PROCFILE"
local APP="$1"

View File

@@ -12,6 +12,9 @@ trigger-ps-post-create() {
local passed_phases=(deploy)
add_passed_docker_option passed_phases[@] "--restart=on-failure:10"
mkdir -p "${DOKKU_LIB_ROOT}/data/ps/${APP}"
chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/ps/${APP}"
}
trigger-ps-post-create "$@"

25
plugins/ps/procfile-extract Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-procfile-extract() {
declare desc="ps procfile-extract plugin trigger"
declare trigger="procfile-extract"
declare desc="extract procfile from a specified docker image"
declare APP="$1" IMAGE="$2" FORCE_EXTRACT="$3" PROCFILE_PATH="$4"
if [[ -z "$PROCFILE_PATH" ]]; then
PROCFILE_PATH="$DOKKU_LIB_ROOT/data/ps/$APP/Procfile.extracted"
mkdir -p "${DOKKU_LIB_ROOT}/data/ps/${APP}"
chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/ps/${APP}"
fi
if [[ "$FORCE_EXTRACT" == "true" ]]; then
plugn trigger procfile-remove "$APP" "$PROCFILE_PATH"
fi
fn-procfile-extract "$IMAGE" "$PROCFILE_PATH"
}
trigger-ps-procfile-extract "$@"

19
plugins/ps/procfile-get-command Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-procfile-get-command() {
declare desc="ps procfile-get-command plugin trigger"
declare trigger="procfile-get-command"
declare desc="extract command from an app procfile"
declare APP="$1" PROCESS_TYPE="$2" PORT="$3" PROCFILE_PATH="$4"
if [[ -z "$PROCFILE_PATH" ]]; then
PROCFILE_PATH="$DOKKU_LIB_ROOT/data/ps/$APP/Procfile.extracted"
fi
fn-procfile-get-command "$PROCFILE_PATH" "$PROCESS_TYPE" "$PORT"
}
trigger-ps-procfile-get-command "$@"

22
plugins/ps/procfile-remove Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
trigger-ps-procfile-remove() {
declare desc="ps procfile-remove plugin trigger"
declare trigger="procfile-remove"
declare desc="remove procfile for a given app"
declare APP="$1" PROCFILE_PATH="$2"
if [[ -z "$PROCFILE_PATH" ]]; then
PROCFILE_PATH="$DOKKU_LIB_ROOT/data/ps/$APP/Procfile.extracted"
fi
if [[ ! -f "$PROCFILE_PATH" ]]; then
return
fi
rm -f "$PROCFILE_PATH" 2>/dev/null
}
trigger-ps-procfile-remove "$@"