mirror of
https://github.com/dokku/dokku.git
synced 2025-12-29 00:25:08 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user