diff --git a/plugins/app-json/triggers.go b/plugins/app-json/triggers.go index 08bd24404e8..06b5126e079 100644 --- a/plugins/app-json/triggers.go +++ b/plugins/app-json/triggers.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" "os" - "path" - "path/filepath" "strings" "github.com/dokku/dokku/plugins/common" @@ -59,95 +57,39 @@ func TriggerAppJSONGetContent(appName string) error { return nil } -// TriggerCorePostDeploy sets a property to -// allow the app to be restored on boot +// TriggerCorePostDeploy moves the extracted app.json to the app data directory +// allowing the app to be restored on boot func TriggerCorePostDeploy(appName string) error { - existingAppJSON := getAppJSONPath(appName) - processSpecificAppJSON := fmt.Sprintf("%s.%s", existingAppJSON, os.Getenv("DOKKU_PID")) - if common.FileExists(processSpecificAppJSON) { - if err := os.Rename(processSpecificAppJSON, existingAppJSON); err != nil { - return err - } - } else if common.FileExists(fmt.Sprintf("%s.missing", processSpecificAppJSON)) { - if err := os.Remove(fmt.Sprintf("%s.missing", processSpecificAppJSON)); err != nil { - return err - } - - if common.FileExists(existingAppJSON) { - if err := os.Remove(existingAppJSON); err != nil { - return err - } - } - } - - return nil + return common.CorePostDeploy(common.CorePostDeployInput{ + AppName: appName, + Destination: common.GetAppDataDirectory("app-json", appName), + PluginName: "app-json", + ExtractedPaths: []common.CorePostDeployPath{ + {Path: "app.json", IsDirectory: false}, + }, + }) } // TriggerCorePostExtract ensures that the main app.json is the one specified by app-json-path func TriggerCorePostExtract(appName string, sourceWorkDir string) error { + destination := common.GetAppDataDirectory("app-json", appName) appJSONPath := strings.Trim(reportComputedAppjsonpath(appName), "/") if appJSONPath == "" { appJSONPath = "app.json" } - existingAppJSON := getAppJSONPath(appName) - files, err := filepath.Glob(fmt.Sprintf("%s.*", existingAppJSON)) - if err != nil { - return err - } - for _, f := range files { - if err := os.Remove(f); err != nil { - return err + validator := func(appName string, path string) error { + if !common.FileExists(path) { + return nil } - } - processSpecificAppJSON := fmt.Sprintf("%s.%s", existingAppJSON, os.Getenv("DOKKU_PID")) - results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{ - Trigger: "git-get-property", - Args: []string{appName, "source-image"}, - }) - appSourceImage := results.StdoutContents() - - results, _ = common.CallPlugnTrigger(common.PlugnTriggerInput{ - Trigger: "builder-get-property", - Args: []string{appName, "build-dir"}, - }) - buildDir := results.StdoutContents() - - repoDefaultAppJSONPath := path.Join(sourceWorkDir, "app.json") - if appSourceImage == "" { - repoAppJSONPath := path.Join(sourceWorkDir, buildDir, appJSONPath) - if !common.FileExists(repoAppJSONPath) { - if appJSONPath != "app.json" && common.FileExists(repoDefaultAppJSONPath) { - if err := os.Remove(repoDefaultAppJSONPath); err != nil { - return err - } - } - return common.TouchFile(fmt.Sprintf("%s.missing", processSpecificAppJSON)) - } - - if err := common.Copy(repoAppJSONPath, processSpecificAppJSON); err != nil { - return fmt.Errorf("Unable to extract app.json: %v", err.Error()) - } - - if appJSONPath != "app.json" { - if err := common.Copy(repoAppJSONPath, repoDefaultAppJSONPath); err != nil { - return fmt.Errorf("Unable to move app.json into place: %v", err.Error()) - } - } - } else { - if err := common.CopyFromImage(appName, appSourceImage, path.Join(buildDir, appJSONPath), processSpecificAppJSON); err != nil { - return common.TouchFile(fmt.Sprintf("%s.missing", processSpecificAppJSON)) - } - } - - if common.FileExists(processSpecificAppJSON) { result, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "app-json-is-valid", - Args: []string{appName, processSpecificAppJSON}, + Args: []string{appName, path}, StreamStdout: true, StreamStderr: true, }) + if err != nil { if result.StderrContents() != "" { return errors.New(result.StderrContents()) @@ -155,10 +97,30 @@ func TriggerCorePostExtract(appName string, sourceWorkDir string) error { return err } + return nil } - // TODO: add validation to app.json file by ensuring it can be deserialized - return nil + results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{ + Trigger: "builder-get-property", + Args: []string{appName, "build-dir"}, + }) + buildDir := results.StdoutContents() + return common.CorePostExtract(common.CorePostExtractInput{ + AppName: appName, + BuildDir: buildDir, + Destination: destination, + PluginName: "app-json", + SourceWorkDir: sourceWorkDir, + ToExtract: []common.CorePostExtractToExtract{ + { + Path: appJSONPath, + IsDirectory: false, + Name: "app.json", + Destination: "app.json", + Validator: validator, + }, + }, + }) } // TriggerInstall initializes app-json directory structures diff --git a/plugins/common/common.go b/plugins/common/common.go index 5a101831a24..a8ae3e662ec 100644 --- a/plugins/common/common.go +++ b/plugins/common/common.go @@ -14,6 +14,7 @@ import ( "strings" "unicode" + "github.com/otiai10/copy" "github.com/ryanuber/columnize" "golang.org/x/sync/errgroup" ) @@ -74,6 +75,298 @@ func CommandUsage(helpHeader string, helpContent string) { fmt.Println(columnize.Format(content, config)) } +// CorePostDeployPath is a file or directory that was extracted +type CorePostDeployPath struct { + // IsDirectory is whether the source is a directory + IsDirectory bool + + // Path is the name of the file or directory + Path string +} + +// CorePostDeployInput is the input for the CorePostDeploy function +type CorePostDeployInput struct { + // AppName is the name of the app + AppName string + + // Destination is the destination directory + Destination string + + // PluginName is the name of the plugin that is deploying the file or directory + PluginName string + + // ExtractedPaths is the list of paths that were extracted + ExtractedPaths []CorePostDeployPath +} + +// CorePostDeploy moves extracted paths to the destination directory +// and removes any existing files or directories that were not extracted +// +// CorePostDeploy(CorePostDeployInput{ +// AppName: "my-app", +// Destination: "/var/lib/dokku/data/my-app", +// ExtractedPaths: []CorePostDeployPath{ +// {Name: "app.json", IsDirectory: false}, +// {Name: "kustomization", IsDirectory: true}, +// }, +// }) +func CorePostDeploy(input CorePostDeployInput) error { + if input.PluginName == "" { + return fmt.Errorf("Missing required PluginName in CorePostDeploy") + } + + if input.AppName == "" { + return fmt.Errorf("Missing required AppName in CorePostDeploy for plugin %v", input.PluginName) + } + + if input.Destination == "" { + return fmt.Errorf("Missing required Destination in CorePostDeploy for plugin %v", input.PluginName) + } + + for i, extractedPath := range input.ExtractedPaths { + if extractedPath.Path == "" { + return fmt.Errorf("Missing required Name in CorePostDeploy for index %v for plugin %v", i, input.PluginName) + } + + existingPath := filepath.Join(input.Destination, extractedPath.Path) + processSpecificPath := fmt.Sprintf("%s.%s", existingPath, os.Getenv("DOKKU_PID")) + + if extractedPath.IsDirectory { + if DirectoryExists(processSpecificPath) { + if err := os.RemoveAll(existingPath); err != nil { + return err + } + + if err := os.Rename(processSpecificPath, existingPath); err != nil { + return err + } + } else if DirectoryExists(fmt.Sprintf("%s.missing", processSpecificPath)) { + if err := os.RemoveAll(fmt.Sprintf("%s.missing", processSpecificPath)); err != nil { + return err + } + + if DirectoryExists(existingPath) { + if err := os.RemoveAll(existingPath); err != nil { + return err + } + } + } + } else { + if FileExists(processSpecificPath) { + if err := os.Rename(processSpecificPath, existingPath); err != nil { + return err + } + } else if FileExists(fmt.Sprintf("%s.missing", processSpecificPath)) { + if err := os.Remove(fmt.Sprintf("%s.missing", processSpecificPath)); err != nil { + return err + } + + if FileExists(existingPath) { + if err := os.Remove(existingPath); err != nil { + return err + } + } + } + } + } + + return nil +} + +// CorePostExtractValidator is a function that validates the file or directory +type CorePostExtractValidator func(appName string, path string) error + +// CorePostExtractToExtract is a file or directory to extract +type CorePostExtractToExtract struct { + // Destination is an optional alias destination path + // If not provided, the Path will be used as the destination + Destination string + + // IsDirectory is whether the source is a directory + IsDirectory bool + + // Name is the common name of the file or directory to extract + Name string + + // Path is the path to the file or directory to extract + Path string + + // Validator is a function that validates the file or directory + Validator CorePostExtractValidator +} + +// CorePostExtractInput is the input for the CorePostExtract function +type CorePostExtractInput struct { + // AppName is the name of the app + AppName string + + // BuildDir is the optional build directory to extract from + BuildDir string + + // DestinationDir is the destination directory + Destination string + + // PluginName is the name of the plugin that is extracting the file or directory + PluginName string + + // SourceWorkDir is the source work directory + SourceWorkDir string + + // ToExtract is a list of files or directories to extract + ToExtract []CorePostExtractToExtract +} + +// CorePostExtract extracts files or directories from a source work directory to a destination directory +// +// CorePostExtract(CorePostExtractInput{ +// AppName: "my-app", +// SourceWorkDir: "/tmp/my-app-source", +// Destination: "/var/lib/dokku/data/my-app", +// ToExtract: []CorePostExtractToExtract{ +// {Path: "app2.json", IsDirectory: false, Name: "app.json"}, +// {Path: "config/kustomize", IsDirectory: true, Destination: "kustomization"}, +// }, +// }) +func CorePostExtract(input CorePostExtractInput) error { + if input.PluginName == "" { + return fmt.Errorf("Missing required PluginName in CorePostExtract") + } + + if input.AppName == "" { + return fmt.Errorf("Missing required AppName in CorePostExtract for plugin %v", input.PluginName) + } + + if input.Destination == "" { + return fmt.Errorf("Missing required Destination in CorePostExtract for plugin %v", input.PluginName) + } + + if input.SourceWorkDir == "" { + return fmt.Errorf("Missing required SourceWorkDir in CorePostExtract for plugin %v", input.PluginName) + } + + results, _ := CallPlugnTrigger(PlugnTriggerInput{ + Trigger: "git-get-property", + Args: []string{input.AppName, "source-image"}, + }) + sourceImage := results.StdoutContents() + + for i, toExtract := range input.ToExtract { + if toExtract.Name == "" { + return fmt.Errorf("Name is required for index %v in CorePostExtract for plugin %v", i, input.PluginName) + } + + if toExtract.Path == "" { + return fmt.Errorf("Path is required for index %v in CorePostExtract for plugin %v", i, input.PluginName) + } + + if toExtract.Destination == "" { + toExtract.Destination = toExtract.Path + } + + sourcePath := filepath.Join(input.SourceWorkDir, toExtract.Path) + repoDefaultSourcePath := filepath.Join(input.SourceWorkDir, toExtract.Name) + imageSourcePath := toExtract.Path + if input.BuildDir != "" { + sourcePath = filepath.Join(input.SourceWorkDir, input.BuildDir, toExtract.Path) + repoDefaultSourcePath = filepath.Join(input.SourceWorkDir, input.BuildDir, toExtract.Name) + imageSourcePath = filepath.Join(input.BuildDir, toExtract.Path) + } + + destination := filepath.Join(input.Destination, toExtract.Destination) + processSpecificDestination := fmt.Sprintf("%s.%s", destination, os.Getenv("DOKKU_PID")) + missingDestination := fmt.Sprintf("%s.missing", processSpecificDestination) + files, err := filepath.Glob(fmt.Sprintf("%s.*", destination)) + if err != nil { + return err + } + for _, f := range files { + if err := os.Remove(f); err != nil { + return err + } + } + + // ignore if the path is empty + if toExtract.Path == "" { + if err := TouchFile(missingDestination); err != nil { + return err + } + continue + } + + if sourceImage == "" { + // ignore if the file does not exist + if toExtract.IsDirectory { + if !DirectoryExists(sourcePath) { + if sourcePath != repoDefaultSourcePath && DirectoryExists(repoDefaultSourcePath) { + if err := os.RemoveAll(repoDefaultSourcePath); err != nil { + return fmt.Errorf("Unable to remove existing %v: %s", toExtract.Name, err.Error()) + } + } + + if err := TouchFile(missingDestination); err != nil { + return err + } + continue + } + + if err := Copy(sourcePath, processSpecificDestination); err != nil { + return fmt.Errorf("Unable to extract %v from %v: %s", toExtract.Name, toExtract.Path, err.Error()) + } + + if sourcePath != repoDefaultSourcePath { + if err := Copy(sourcePath, repoDefaultSourcePath); err != nil { + return fmt.Errorf("Unable to move %v into place: %s", toExtract.Name, err.Error()) + } + } + } else { + if !FileExists(sourcePath) { + // delete the existing file if the user tried to override it with a non-existent file + if sourcePath != repoDefaultSourcePath && FileExists(repoDefaultSourcePath) { + if err := os.Remove(repoDefaultSourcePath); err != nil { + return err + } + } + if err := TouchFile(missingDestination); err != nil { + return err + } + continue + } + + if err := Copy(sourcePath, processSpecificDestination); err != nil { + return fmt.Errorf("Unable to extract %v from %v: %v", toExtract.Name, toExtract.Path, err.Error()) + } + + if sourcePath != repoDefaultSourcePath { + // ensure the file in the repo is the same as the one the user specified + if err := copy.Copy(sourcePath, repoDefaultSourcePath); err != nil { + return fmt.Errorf("Unable to move %v into place: %v", toExtract.Name, err.Error()) + } + } + } + } else { + if toExtract.IsDirectory { + + if err := CopyDirFromImage(input.AppName, sourceImage, imageSourcePath, processSpecificDestination); err != nil { + return TouchFile(missingDestination) + } + } else { + if err := CopyFromImage(input.AppName, sourceImage, imageSourcePath, processSpecificDestination); err != nil { + return TouchFile(missingDestination) + } + } + } + + // validate the file + if toExtract.Validator != nil { + if err := toExtract.Validator(input.AppName, processSpecificDestination); err != nil { + return err + } + } + } + + return nil +} + // EnvWrap wraps a func with a setenv call and resets the value at the end func EnvWrap(fn func() error, environ map[string]string) error { oldEnviron := map[string]string{} diff --git a/plugins/common/docker.go b/plugins/common/docker.go index bf3cef0cd14..11a0c1a3943 100644 --- a/plugins/common/docker.go +++ b/plugins/common/docker.go @@ -151,6 +151,98 @@ func ContainerWaitTilReady(containerID string, timeout time.Duration) error { return nil } +// CopyDirFromImage copies a directory from named image to destination +func CopyDirFromImage(appName string, image string, source string, destination string) error { + if !VerifyImage(image) { + return fmt.Errorf("Invalid docker image for copying content") + } + + if !IsAbsPath(source) { + workDir := GetWorkingDir(appName, image) + if workDir != "" { + source = fmt.Sprintf("%s/%s", workDir, source) + } + } + + tmpDir, err := os.MkdirTemp("", fmt.Sprintf("dokku-%s-%s", MustGetEnv("DOKKU_PID"), "CopyFromImage")) + if err != nil { + return fmt.Errorf("Error creating temporary directory: %v", err) + } + + defer func() { + if err := os.RemoveAll(tmpDir); err != nil { + LogWarn(fmt.Sprintf("Error removing temporary directory %s: %v\n", tmpDir, err)) + } + }() + + 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) + } + defer ContainerRemove(containerID) + + // 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 + result, err := CallExecCommand(ExecCommandInput{ + Command: DockerBin(), + Args: []string{"container", "cp", "--quiet", fmt.Sprintf("%s:%s", containerID, source), tmpDir}, + }) + if err != nil { + return fmt.Errorf("Unable to copy file %s from image: %w", source, err) + } + if result.ExitCode != 0 { + return fmt.Errorf("Unable to copy file %s from image: %v", source, result.StderrContents()) + } + + if !DirectoryExists(tmpDir) { + return fmt.Errorf("Unable to copy file %s from image: %v", source, result.StderrContents()) + } + + files, err := os.ReadDir(tmpDir) + if err != nil { + return fmt.Errorf("Unable to read temporary directory: %v", err) + } + + for _, file := range files { + if file.IsDir() { + continue + } + + sourceFile := fmt.Sprintf("%s/%s", tmpDir, file.Name()) + destinationFile := fmt.Sprintf("%s/%s", destination, file.Name()) + // workaround when owner is root. seems to only happen when running inside docker + CallExecCommand(ExecCommandInput{ + Command: "dos2unix", + Args: []string{"-l", "-n", sourceFile, destinationFile}, + }) // nolint: errcheck + + // add trailing newline for certain places where file parsing depends on it + result, err = CallExecCommand(ExecCommandInput{ + Command: "tail", + Args: []string{"-c1", destination}, + }) + if err != nil || result.ExitCode != 0 { + return fmt.Errorf("Unable to append trailing newline to copied file: %v", result.Stderr) + } + + if result.Stdout != "" { + 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 +} + // CopyFromImage copies a file from named image to destination func CopyFromImage(appName string, image string, source string, destination string) error { if !VerifyImage(image) { diff --git a/plugins/common/functions b/plugins/common/functions index 73beec9c1ed..f98068aac37 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -488,7 +488,6 @@ parse_args() { copy_from_image() { declare desc="copy file from named image to destination" declare IMAGE="$1" SRC_FILE="$2" DST_FILE="$3" - local WORK_DIR="" if ! "$PLUGIN_CORE_AVAILABLE_PATH/common/common" copy-from-image "$APP" "$IMAGE" "$SRC_FILE" "$DST_FILE"; then return 1 @@ -498,56 +497,8 @@ copy_from_image() { copy_dir_from_image() { declare desc="copy a directory from named image to destination" declare IMAGE="$1" SRC_DIR="$2" DST_DIR="$3" - local WORK_DIR="" - local DOCKER_CREATE_LABEL_ARGS="--label=com.dokku.app-name=$APP" - - if verify_image "$IMAGE"; then - if ! is_abs_path "$SRC_DIR"; then - if is_image_cnb_based "$IMAGE"; then - WORKDIR="/workspace" - elif is_image_herokuish_based "$IMAGE" "$APP"; then - WORKDIR="/app" - else - WORKDIR="$("$DOCKER_BIN" image inspect --format '{{.Config.WorkingDir}}' "$IMAGE")" - fi - - if [[ -n "$WORKDIR" ]]; then - SRC_DIR="${WORKDIR}/${SRC_DIR}" - fi - fi - - TMP_DIR_COMMAND_OUTPUT=$(mktemp -d "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX") - trap "rm -rf '$TMP_DIR_COMMAND_OUTPUT' &>/dev/null || true" RETURN - - local CID=$("$DOCKER_BIN" container create "${DOCKER_CREATE_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS "$IMAGE") - "$DOCKER_BIN" container cp "$CID:$SRC_DIR" "$TMP_DIR_COMMAND_OUTPUT" 2>/dev/null || true - "$DOCKER_BIN" container rm --force "$CID" &>/dev/null - plugn trigger scheduler-register-retired "$APP" "$CID" - - # 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 - if [[ ! -d "$TMP_DIR_COMMAND_OUTPUT" ]]; then - return 1 - fi - - pushd "$TMP_DIR_COMMAND_OUTPUT" >/dev/null - for filename in *; do - if [[ ! -f "$TMP_DIR_COMMAND_OUTPUT/$filename" ]]; then - continue - fi - - # workaround for CHECKS file when owner is root. seems to only happen when running inside docker - dos2unix -l <"$TMP_DIR_COMMAND_OUTPUT/$filename" >"$DST_DIR/$filename" - - # add trailing newline for certain places where file parsing depends on it - if [[ "$(tail -c1 "$DST_DIR/$filename")" != "" ]]; then - echo "" >>"$DST_DIR/$filename" - fi - done - popd &>/dev/null || pushd "/tmp" >/dev/null - else + if ! "$PLUGIN_CORE_AVAILABLE_PATH/common/common" copy-dir-from-image "$APP" "$IMAGE" "$SRC_DIR" "$DST_DIR"; then return 1 fi } diff --git a/plugins/common/io.go b/plugins/common/io.go index 5ad0cf33bb1..1bafce8ca4a 100644 --- a/plugins/common/io.go +++ b/plugins/common/io.go @@ -195,23 +195,33 @@ func SetPermissions(input SetPermissionInput) error { return os.Chown(input.Filename, uid, gid) } +// TouchDir creates an empty directory at the specified path +func TouchDir(filename string) error { + mode := os.FileMode(0700) + return os.MkdirAll(filename, mode) +} + // TouchFile creates an empty file at the specified path func TouchFile(filename string) error { mode := os.FileMode(0600) file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) if err != nil { - return err + return fmt.Errorf("Error opening file %v for creation: %v", filename, err) } defer file.Close() if err := file.Chmod(mode); err != nil { - return err + return fmt.Errorf("Error setting chown for new file %v: %v", filename, err) } - return SetPermissions(SetPermissionInput{ + if err := SetPermissions(SetPermissionInput{ Filename: filename, Mode: mode, - }) + }); err != nil { + return fmt.Errorf("Error setting permissions for new file %v: %v", filename, err) + } + + return nil } // WriteSliceToFile writes a slice of strings to a file diff --git a/plugins/common/src/common/common.go b/plugins/common/src/common/common.go index 7d59e39d429..5c95746aa37 100644 --- a/plugins/common/src/common/common.go +++ b/plugins/common/src/common/common.go @@ -34,6 +34,12 @@ func main() { ProjectName: projectName, ComposeFile: composeFile, }) + case "copy-dir-from-image": + appName := flag.Arg(1) + image := flag.Arg(2) + source := flag.Arg(3) + destination := flag.Arg(4) + err = common.CopyDirFromImage(appName, image, source, destination) case "copy-from-image": appName := flag.Arg(1) image := flag.Arg(2) diff --git a/plugins/ps/triggers.go b/plugins/ps/triggers.go index 4d788827c2a..ad69a9f61f9 100644 --- a/plugins/ps/triggers.go +++ b/plugins/ps/triggers.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "os" - "path" "path/filepath" "strconv" "strings" @@ -22,22 +21,16 @@ func TriggerAppRestart(appName string) error { // TriggerCorePostDeploy sets a property to // allow the app to be restored on boot func TriggerCorePostDeploy(appName string) error { - existingProcfile := getProcfilePath(appName) - processSpecificProcfile := fmt.Sprintf("%s.%s", existingProcfile, os.Getenv("DOKKU_PID")) - if common.FileExists(processSpecificProcfile) { - if err := os.Rename(processSpecificProcfile, existingProcfile); err != nil { - return err - } - } else if common.FileExists(fmt.Sprintf("%s.missing", processSpecificProcfile)) { - if err := os.Remove(fmt.Sprintf("%s.missing", processSpecificProcfile)); err != nil { - return err - } - - if common.FileExists(existingProcfile) { - if err := os.Remove(existingProcfile); err != nil { - return err - } - } + err := common.CorePostDeploy(common.CorePostDeployInput{ + AppName: appName, + Destination: common.GetAppDataDirectory("ps", appName), + PluginName: "ps", + ExtractedPaths: []common.CorePostDeployPath{ + {Path: "Procfile", IsDirectory: false}, + }, + }) + if err != nil { + return err } if err := common.PropertyDelete("ps", appName, "scale.old"); err != nil { @@ -55,67 +48,45 @@ func TriggerCorePostDeploy(appName string) error { // TriggerCorePostExtract ensures that the main Procfile is the one specified by procfile-path func TriggerCorePostExtract(appName string, sourceWorkDir string) error { + destination := common.GetAppDataDirectory("ps", appName) procfilePath := strings.Trim(reportComputedProcfilePath(appName), "/") if procfilePath == "" { procfilePath = "Procfile" } - existingProcfile := getProcfilePath(appName) - files, err := filepath.Glob(fmt.Sprintf("%s.*", existingProcfile)) - if err != nil { - return err - } - for _, f := range files { - if err := os.Remove(f); err != nil { - return err + validator := func(appName string, path string) error { + if !common.FileExists(path) { + return nil } - } - processSpecificProcfile := fmt.Sprintf("%s.%s", existingProcfile, os.Getenv("DOKKU_PID")) - results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{ - Trigger: "git-get-property", - Args: []string{appName, "source-image"}, - }) - appSourceImage := results.StdoutContents() - - repoDefaultProcfilePath := path.Join(sourceWorkDir, "Procfile") - if appSourceImage == "" { - repoProcfilePath := path.Join(sourceWorkDir, procfilePath) - if !common.FileExists(repoProcfilePath) { - if procfilePath != "Procfile" && common.FileExists(repoDefaultProcfilePath) { - if err := os.Remove(repoDefaultProcfilePath); err != nil { - return err - } - } - return common.TouchFile(fmt.Sprintf("%s.missing", processSpecificProcfile)) - } - - if err := common.Copy(repoProcfilePath, processSpecificProcfile); err != nil { - return fmt.Errorf("Unable to extract Procfile: %v", err.Error()) - } - - if procfilePath != "Procfile" { - if err := common.Copy(repoProcfilePath, repoDefaultProcfilePath); err != nil { - return fmt.Errorf("Unable to move Procfile into place: %v", err.Error()) - } + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: "procfile-util", + Args: []string{"check", "-P", path}, + }) + if err != nil { + return err } - } else { - if err := common.CopyFromImage(appName, appSourceImage, procfilePath, processSpecificProcfile); err != nil { - return common.TouchFile(fmt.Sprintf("%s.missing", processSpecificProcfile)) + if result.ExitCode != 0 { + return fmt.Errorf("Invalid Procfile: %s", result.StderrContents()) } + return nil } - result, err := common.CallExecCommand(common.ExecCommandInput{ - Command: "procfile-util", - Args: []string{"check", "-P", processSpecificProcfile}, + return common.CorePostExtract(common.CorePostExtractInput{ + AppName: appName, + Destination: destination, + PluginName: "ps", + SourceWorkDir: sourceWorkDir, + ToExtract: []common.CorePostExtractToExtract{ + { + Path: procfilePath, + IsDirectory: false, + Name: "Procfile", + Destination: "Procfile", + Validator: validator, + }, + }, }) - if err != nil { - return fmt.Errorf(result.StderrContents()) - } - if result.ExitCode != 0 { - return fmt.Errorf("Invalid Procfile: %s", result.StderrContents()) - } - return nil } // TriggerInstall initializes app restart policies