refactor: use helper go functions to handle extracting files from a repository

This standardizes the extraction code and makes it easier to reuse in future implementations of file extraction.

Additionally, this adds a way to extract folders from codebases.
This commit is contained in:
Jose Diaz-Gonzalez
2025-07-23 17:55:45 -04:00
parent 3116880b75
commit 577f46153f
7 changed files with 483 additions and 198 deletions

View File

@@ -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
}
}
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))
validator := func(appName string, path string) error {
if !common.FileExists(path) {
return nil
}
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

View File

@@ -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{}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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 {
validator := func(appName string, path string) error {
if !common.FileExists(path) {
return nil
}
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "procfile-util",
Args: []string{"check", "-P", path},
})
if err != nil {
return err
}
if result.ExitCode != 0 {
return fmt.Errorf("Invalid Procfile: %s", result.StderrContents())
}
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"},
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,
},
},
})
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())
}
}
} else {
if err := common.CopyFromImage(appName, appSourceImage, procfilePath, processSpecificProcfile); err != nil {
return common.TouchFile(fmt.Sprintf("%s.missing", processSpecificProcfile))
}
}
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "procfile-util",
Args: []string{"check", "-P", processSpecificProcfile},
})
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