Merge pull request #4117 from dokku/go-app-json
This commit is contained in:
@@ -32,6 +32,8 @@ Each "phase" has different expectations and limitations:
|
||||
- Priming or invalidating cache stores
|
||||
- Running database migrations
|
||||
|
||||
Additionally, if using a Dockerfile with an `ENTRYPOINT`, the deployment task is passed to that entrypoint as is.
|
||||
|
||||
Please keep the above in mind when utilizing deployment tasks.
|
||||
|
||||
> To execute commands on the host during a release phase, see the [plugin creation documentation](/docs/development/plugin-creation.md) docs for more information on building your own custom plugin.
|
||||
|
||||
@@ -110,9 +110,7 @@ The following config variables have special meanings and can be set in a variety
|
||||
| `DOKKU_DISABLE_ANSI_PREFIX_REMOVAL` | none | `dokku config:set` <br /> `/etc/environment` <br /> `~dokku/.dokkurc` <br /> `~dokku/.dokkurc/*` | Disables removal of the ANSI prefix during deploys. Can be used in cases where the client deployer does not understand ansi escape codes. |
|
||||
| `DOKKU_DISABLE_APP_AUTOCREATION` | none | `dokku config:set` | Disables automatic creation of a non-existent app on deploy. |
|
||||
| `DOKKU_DOCKER_STOP_TIMEOUT` | `10` | `dokku config:set` | Configurable grace period given to the `docker stop` command. If a container has not stopped by this time, a `kill -9` signal or equivalent is sent in order to force-terminate the container. Both the `ps:stop` and `apps:destroy` commands _also_ respect this value. If not specified, the docker defaults for the [docker stop command](https://docs.docker.com/engine/reference/commandline/stop/) will be used.|
|
||||
| `DOKKU_DOCKERFILE_CMD` | dockerfile cmd | `dokku config:set` | |
|
||||
| `DOKKU_DOCKERFILE_CACHE_BUILD` | none | `dokku config:set` | |
|
||||
| `DOKKU_DOCKERFILE_ENTRYPOINT` | dockerfile entrypoint | `dokku config:set` | |
|
||||
| `DOKKU_DOCKERFILE_PORTS` | dockerfile ports | `dokku config:set` | |
|
||||
| `DOKKU_DOCKERFILE_START_CMD` | none | `dokku config:set` | |
|
||||
| `DOKKU_PARALLEL_ARGUMENTS`. | none | `dokku config:set` | Allows passing custom arguments to parallel for `ps:*all` commands |
|
||||
|
||||
@@ -164,7 +164,7 @@ dokku buildpacks:report node-js-app --buildpacks-list
|
||||
If an application was previously deployed via Dockerfile, the following commands should be run before a buildpack deploy will succeed:
|
||||
|
||||
```shell
|
||||
dokku config:unset --no-restart node-js-app DOKKU_DOCKERFILE_CMD DOKKU_DOCKERFILE_ENTRYPOINT DOKKU_PROXY_PORT_MAP
|
||||
dokku config:unset --no-restart node-js-app DOKKU_PROXY_PORT_MAP
|
||||
```
|
||||
|
||||
### Using a specific buildpack version
|
||||
|
||||
@@ -1447,6 +1447,51 @@ APP="$1";
|
||||
curl "https://dokku.me/starting/${APP}" || true
|
||||
```
|
||||
|
||||
### `procfile-extract`
|
||||
|
||||
- Description: Extracts a Procfile from a given image to a path
|
||||
- Invoked by: `internally`
|
||||
- Arguments: `$APP $IMAGE $FORCE_EXTRACT $PROCFILE_PATH`
|
||||
- Example:
|
||||
|
||||
```shell
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
# TODO
|
||||
```
|
||||
|
||||
### `procfile-get-command`
|
||||
|
||||
- Description: Fetches the command for a specific process type
|
||||
- Invoked by: `internally`
|
||||
- Arguments: `$APP $PROCESS_TYPE $PORT $PROCFILE_PATH`
|
||||
- Example:
|
||||
|
||||
```shell
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
# TODO
|
||||
```
|
||||
|
||||
### `procfile-remove`
|
||||
|
||||
- Description: Removes the extracted Procfile
|
||||
- Invoked by: `internally`
|
||||
- Arguments: `$APP $PROCFILE_PATH`
|
||||
- Example:
|
||||
|
||||
```shell
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
# TODO
|
||||
```
|
||||
|
||||
### `proxy-build-config`
|
||||
|
||||
- Description: Builds the proxy implementation configuration for a given app
|
||||
|
||||
1
plugins/20_events/procfile-extract
Symbolic link
1
plugins/20_events/procfile-extract
Symbolic link
@@ -0,0 +1 @@
|
||||
hook
|
||||
1
plugins/20_events/procfile-get-command
Symbolic link
1
plugins/20_events/procfile-get-command
Symbolic link
@@ -0,0 +1 @@
|
||||
hook
|
||||
1
plugins/20_events/procfile-remove
Symbolic link
1
plugins/20_events/procfile-remove
Symbolic link
@@ -0,0 +1 @@
|
||||
hook
|
||||
1
plugins/20_events/profile-get-command
Symbolic link
1
plugins/20_events/profile-get-command
Symbolic link
@@ -0,0 +1 @@
|
||||
hook
|
||||
4
plugins/app-json/.gitignore
vendored
Normal file
4
plugins/app-json/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/triggers/*
|
||||
/triggers
|
||||
/pre-*
|
||||
/post-*
|
||||
5
plugins/app-json/Makefile
Normal file
5
plugins/app-json/Makefile
Normal 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
380
plugins/app-json/appjson.go
Normal 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
12
plugins/app-json/go.mod
Normal 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
8
plugins/app-json/go.sum
Normal 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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
36
plugins/app-json/src/triggers/triggers.go
Normal file
36
plugins/app-json/src/triggers/triggers.go
Normal 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())
|
||||
}
|
||||
}
|
||||
21
plugins/app-json/triggers.go
Normal file
21
plugins/app-json/triggers.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
25
plugins/ps/procfile-extract
Executable 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
19
plugins/ps/procfile-get-command
Executable 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
22
plugins/ps/procfile-remove
Executable 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 "$@"
|
||||
10
tests/apps/dockerfile-entrypoint/Dockerfile
Normal file
10
tests/apps/dockerfile-entrypoint/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM ruby:2.5.1
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
ADD Procfile Procfile
|
||||
ADD entrypoint entrypoint
|
||||
ADD app.json app.json
|
||||
|
||||
ENTRYPOINT ["./entrypoint"]
|
||||
1
tests/apps/dockerfile-entrypoint/Procfile
Normal file
1
tests/apps/dockerfile-entrypoint/Procfile
Normal file
@@ -0,0 +1 @@
|
||||
web: sleep 1000
|
||||
7
tests/apps/dockerfile-entrypoint/app.json
Normal file
7
tests/apps/dockerfile-entrypoint/app.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dokku": {
|
||||
"predeploy": "touch /app/predeploy.test"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
tests/apps/dockerfile-entrypoint/entrypoint
Executable file
9
tests/apps/dockerfile-entrypoint/entrypoint
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -o pipefail
|
||||
|
||||
if [[ "$1" == touch* ]]; then
|
||||
echo "entrypoint script started with arguments $@"
|
||||
touch "$(echo $@ | awk '{print $2}')"
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
||||
8
tests/apps/dockerfile-procfile/app.json
Normal file
8
tests/apps/dockerfile-procfile/app.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dokku": {
|
||||
"predeploy": "touch /app/predeploy.test",
|
||||
"postdeploy": "touch /app/postdeploy.test"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
tests/apps/dockerfile-release/Dockerfile
Normal file
19
tests/apps/dockerfile-release/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.8-buster
|
||||
|
||||
RUN pip install --user pipenv
|
||||
ENV PATH="${PATH}:/root/.local/bin"
|
||||
ENV PIPENV_VENV_IN_PROJECT=0
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN ls -lah
|
||||
|
||||
RUN rm -rf .venv
|
||||
|
||||
RUN mkdir .venv
|
||||
|
||||
RUN pipenv install
|
||||
|
||||
CMD pipenv run gunicorn -b 0.0.0.0:8000 --workers=3 --threads=5 project.wsgi:application
|
||||
17
tests/apps/dockerfile-release/Pipfile
Normal file
17
tests/apps/dockerfile-release/Pipfile
Normal file
@@ -0,0 +1,17 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
django = "*"
|
||||
gunicorn = "*"
|
||||
djangorestframework = "*"
|
||||
whitenoise = "*"
|
||||
psycopg2-binary = "*"
|
||||
dj-database-url = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
120
tests/apps/dockerfile-release/Pipfile.lock
generated
Normal file
120
tests/apps/dockerfile-release/Pipfile.lock
generated
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "6e4ab3d777dd8ead8bc254ab4bb72ee80c93d1c09537b30ad23beb21c8bfdba9"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.8"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
|
||||
"sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
|
||||
],
|
||||
"version": "==3.2.3"
|
||||
},
|
||||
"dj-database-url": {
|
||||
"hashes": [
|
||||
"sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163",
|
||||
"sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:4f2c913303be4f874015993420bf0bd8fd2097a9c88e6b49c6a92f9bdd3fb13a",
|
||||
"sha256:8c3575f81e11390893860d97e1e0154c47512f180ea55bd84ce8fa69ba8051ca"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"djangorestframework": {
|
||||
"hashes": [
|
||||
"sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4",
|
||||
"sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.11.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
|
||||
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.0.4"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
"sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29",
|
||||
"sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03",
|
||||
"sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039",
|
||||
"sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881",
|
||||
"sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309",
|
||||
"sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed",
|
||||
"sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b",
|
||||
"sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3",
|
||||
"sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7",
|
||||
"sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b",
|
||||
"sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03",
|
||||
"sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103",
|
||||
"sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d",
|
||||
"sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35",
|
||||
"sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b",
|
||||
"sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49",
|
||||
"sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70",
|
||||
"sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e",
|
||||
"sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e",
|
||||
"sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e",
|
||||
"sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103",
|
||||
"sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6",
|
||||
"sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1",
|
||||
"sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9",
|
||||
"sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e",
|
||||
"sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f",
|
||||
"sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd",
|
||||
"sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8",
|
||||
"sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f",
|
||||
"sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4",
|
||||
"sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964",
|
||||
"sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.4"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||
],
|
||||
"version": "==2019.3"
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
"sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
|
||||
"sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"
|
||||
],
|
||||
"version": "==0.3.0"
|
||||
},
|
||||
"whitenoise": {
|
||||
"hashes": [
|
||||
"sha256:0f9137f74bd95fa54329ace88d8dc695fbe895369a632e35f7a136e003e41d73",
|
||||
"sha256:62556265ec1011bd87113fb81b7516f52688887b7a010ee899ff1fd18fd22700"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.0.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
1
tests/apps/dockerfile-release/Procfile
Normal file
1
tests/apps/dockerfile-release/Procfile
Normal file
@@ -0,0 +1 @@
|
||||
release: pipenv run python manage.py migrate
|
||||
0
tests/apps/dockerfile-release/app/__init__.py
Normal file
0
tests/apps/dockerfile-release/app/__init__.py
Normal file
3
tests/apps/dockerfile-release/app/admin.py
Normal file
3
tests/apps/dockerfile-release/app/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
tests/apps/dockerfile-release/app/apps.py
Normal file
5
tests/apps/dockerfile-release/app/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AppMainConfig(AppConfig):
|
||||
name = 'app'
|
||||
3
tests/apps/dockerfile-release/app/models.py
Normal file
3
tests/apps/dockerfile-release/app/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
tests/apps/dockerfile-release/app/tests.py
Normal file
3
tests/apps/dockerfile-release/app/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
tests/apps/dockerfile-release/app/views.py
Normal file
3
tests/apps/dockerfile-release/app/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
11
tests/apps/dockerfile-release/manage.py
Executable file
11
tests/apps/dockerfile-release/manage.py
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
print("SECRET_KEY: {0}".format(os.getenv('SECRET_KEY')))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
tests/apps/dockerfile-release/project/__init__.py
Normal file
0
tests/apps/dockerfile-release/project/__init__.py
Normal file
16
tests/apps/dockerfile-release/project/asgi.py
Normal file
16
tests/apps/dockerfile-release/project/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for project project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
12
tests/apps/dockerfile-release/project/settings/__init__.py
Normal file
12
tests/apps/dockerfile-release/project/settings/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import os
|
||||
|
||||
environment = os.getenv('ENVIRONMENT')
|
||||
|
||||
if environment == 'dev':
|
||||
from .dev import *
|
||||
elif environment == 'staging':
|
||||
from .staging import *
|
||||
elif environment == 'production':
|
||||
from .staging import *
|
||||
else:
|
||||
raise Exception("Invalid environment")
|
||||
135
tests/apps/dockerfile-release/project/settings/defaults.py
Normal file
135
tests/apps/dockerfile-release/project/settings/defaults.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
Django settings for project project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.0.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
import dj_database_url
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||
|
||||
if not SECRET_KEY:
|
||||
raise Exception("Secret Key Required")
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
"*",
|
||||
]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'whitenoise.runserver_nostatic',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'project.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'project.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': dj_database_url.config(conn_max_age=500, ssl_require=True)
|
||||
}
|
||||
|
||||
del DATABASES['default']['OPTIONS']['sslmode']
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||
|
||||
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'staticfiles')
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Extra places for collectstatic to find static files.
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(PROJECT_ROOT, 'static'),
|
||||
]
|
||||
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
4
tests/apps/dockerfile-release/project/settings/dev.py
Normal file
4
tests/apps/dockerfile-release/project/settings/dev.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .defaults import *
|
||||
|
||||
|
||||
DEBUG = True
|
||||
@@ -0,0 +1 @@
|
||||
from .defaults import *
|
||||
@@ -0,0 +1 @@
|
||||
from .defaults import *
|
||||
21
tests/apps/dockerfile-release/project/urls.py
Normal file
21
tests/apps/dockerfile-release/project/urls.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""project URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
16
tests/apps/dockerfile-release/project/wsgi.py
Normal file
16
tests/apps/dockerfile-release/project/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for project project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -13,7 +13,16 @@ teardown() {
|
||||
}
|
||||
|
||||
@test "(app-json) app.json scripts" {
|
||||
deploy_app nodejs-express
|
||||
run deploy_app nodejs-express
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run docker inspect "${TEST_APP}.web.1" --format "{{json .Config.Cmd}}"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output '["/start","web"]'
|
||||
|
||||
run /bin/bash -c "dokku --rm run $TEST_APP ls /app/prebuild.test"
|
||||
echo "output: $output"
|
||||
@@ -28,15 +37,80 @@ teardown() {
|
||||
CID=$(docker ps -a -q -f "ancestor=dokku/${TEST_APP}" -f "label=dokku_phase_script=postdeploy")
|
||||
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=$TEST_APP")
|
||||
IMAGE_ID=$(docker commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" $CID dokku-test/${TEST_APP})
|
||||
run /bin/bash -c "docker run -ti $IMAGE_ID ls /app/postdeploy.test"
|
||||
run /bin/bash -c "docker run --rm -ti $IMAGE_ID ls /app/postdeploy.test"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
}
|
||||
|
||||
|
||||
@test "(app-json) app.json scripts missing" {
|
||||
deploy_app nodejs-express-noappjson
|
||||
run deploy_app nodejs-express-noappjson
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "(app-json) app.json dockerfile cmd" {
|
||||
run deploy_app dockerfile-procfile
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run docker inspect "dokku/${TEST_APP}:latest" --format "{{json .Config.Cmd}}"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output '["/bin/sh","-c","npm start"]'
|
||||
|
||||
run docker inspect "dokku/${TEST_APP}:latest" --format "{{json .Config.Entrypoint}}"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output 'null'
|
||||
}
|
||||
|
||||
@test "(app-json) app.json dockerfile entrypoint" {
|
||||
run deploy_app dockerfile-entrypoint
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run docker inspect "dokku/${TEST_APP}:latest" --format "{{json .Config.Cmd}}"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output 'null'
|
||||
|
||||
run docker inspect "dokku/${TEST_APP}:latest" --format "{{json .Config.Entrypoint}}"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output '["./entrypoint"]'
|
||||
}
|
||||
|
||||
@test "(app-json) app.json dockerfile release" {
|
||||
run /bin/bash -c "dokku config:set --no-restart $TEST_APP SECRET_KEY=fjdkslafjdk ENVIRONMENT=dev"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app dockerfile-release
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_output_contains "Executing release task from Procfile"
|
||||
assert_output_contains "SECRET_KEY: fjdkslafjdk"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "(app-json) app.json dockerfile entrypoint" {
|
||||
run deploy_app dockerfile-entrypoint
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_output_contains "Executing predeploy task from app.json"
|
||||
assert_output_contains "entrypoint script started with arguments touch /app/predeploy.test"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku --rm run $TEST_APP ls /app/predeploy.test"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
Reference in New Issue
Block a user