diff --git a/docs/appendices/0.30.0-migration-guide.md b/docs/appendices/0.30.0-migration-guide.md index e84e4c75a..3e6035fb8 100644 --- a/docs/appendices/0.30.0-migration-guide.md +++ b/docs/appendices/0.30.0-migration-guide.md @@ -1,5 +1,9 @@ # 0.30.0 Migration Guide +## Changes + +- The `app.json` file is now extracted from the source code instead of the built image. For deploys via `git:from-image`, the file is extracted from the built image. + ## Removals - Support for [SPDY](https://en.wikipedia.org/wiki/SPDY) has been removed. No major browser supports it as of 2021. Custom `nginx.conf.sigil` templates referencing spdy-related variables will continue to build until the 1.0.0 release. diff --git a/plugins/app-json/appjson.go b/plugins/app-json/appjson.go index 72568b95c..3c01ffe8c 100644 --- a/plugins/app-json/appjson.go +++ b/plugins/app-json/appjson.go @@ -48,7 +48,7 @@ func GetAppjsonDirectory(appName string) string { return common.GetAppDataDirectory("app-json", appName) } -// GetAppjsonPath returns the path to a given app's extracted app.json file +// GetAppjsonPath returns the path to a given app's extracted app.json file for use by other plugins func GetAppjsonPath(appName string) string { - return filepath.Join(GetAppjsonDirectory(appName), "app.json") + return getProcessSpecificAppJSONPath(appName) } diff --git a/plugins/app-json/functions.go b/plugins/app-json/functions.go index e47470ac3..25ae4308a 100644 --- a/plugins/app-json/functions.go +++ b/plugins/app-json/functions.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "strconv" "strings" @@ -80,11 +81,11 @@ func constructScript(command string, shell string, isHerokuishImage bool, isCnbI } func getAppJSON(appName string) (AppJSON, error) { - if !common.FileExists(GetAppjsonPath(appName)) { + if !hasAppJSON(appName) { return AppJSON{}, nil } - b, err := ioutil.ReadFile(GetAppjsonPath(appName)) + b, err := ioutil.ReadFile(getProcessSpecificAppJSONPath(appName)) if err != nil { return AppJSON{}, fmt.Errorf("Cannot read app.json file: %v", err) } @@ -101,6 +102,21 @@ func getAppJSON(appName string) (AppJSON, error) { return appJSON, nil } +func getAppJSONPath(appName string) string { + directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "app-json", appName) + return filepath.Join(directory, "app.json") +} + +func getProcessSpecificAppJSONPath(appName string) string { + existingAppJSON := getAppJSONPath(appName) + processSpecificAppJSON := fmt.Sprintf("%s.%s", existingAppJSON, os.Getenv("DOKKU_PID")) + if common.FileExists(processSpecificAppJSON) { + return processSpecificAppJSON + } + + return existingAppJSON +} + // getPhaseScript extracts app.json from app image and returns the appropriate json key/value func getPhaseScript(appName string, phase string) (string, error) { appJSON, err := getAppJSON(appName) @@ -155,6 +171,19 @@ func getDokkuAppShell(appName string) string { return shell } +func hasAppJSON(appName string) bool { + appJSONPath := getAppJSONPath(appName) + if common.FileExists(fmt.Sprintf("%s.%s.missing", appJSONPath, os.Getenv("DOKKU_PID"))) { + return false + } + + if common.FileExists(fmt.Sprintf("%s.%s", appJSONPath, os.Getenv("DOKKU_PID"))) { + return true + } + + return common.FileExists(appJSONPath) +} + func cleanupDeploymentContainer(appName string, containerID string, phase string) error { if phase != "predeploy" { os.Setenv("DOKKU_SKIP_IMAGE_RETIRE", "true") @@ -453,30 +482,6 @@ func createdContainerID(appName string, dockerArgs []string, image string, comma return containerID, err } -func refreshAppJSON(appName string, image string) error { - baseDirectory := common.GetDataDirectory("app-json") - if !common.DirectoryExists(baseDirectory) { - return errors.New("Run 'dokku plugin:install' to ensure the correct directories exist") - } - - directory := common.GetAppDataDirectory("app-json", appName) - if !common.DirectoryExists(directory) { - if err := os.MkdirAll(directory, 0755); err != nil { - return err - } - } - - appjsonPath := GetAppjsonPath(appName) - if common.FileExists(appjsonPath) { - if err := os.Remove(appjsonPath); err != nil { - return errors.New("Unable to remove previous app.json file") - } - } - - common.CopyFromImage(appName, image, reportComputedAppjsonpath(appName), appjsonPath) - return nil -} - func setScale(appName string, image string) error { appJSON, err := getAppJSON(appName) if err != nil { diff --git a/plugins/app-json/triggers.go b/plugins/app-json/triggers.go index 37a7fab34..f2f90a71d 100644 --- a/plugins/app-json/triggers.go +++ b/plugins/app-json/triggers.go @@ -3,8 +3,12 @@ package appjson import ( "fmt" "os" + "path" + "path/filepath" + "strings" "github.com/dokku/dokku/plugins/common" + "github.com/otiai10/copy" ) // TriggerAppJSONProcessDeployParallelism returns the max number of processes to deploy in parallel @@ -33,6 +37,83 @@ func TriggerAppJSONProcessDeployParallelism(appName string, processType string) return nil } +// TriggerCorePostDeploy sets a property to +// allow 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 +} + +// TriggerCorePostExtract ensures that the main app.json is the one specified by app-json-path +func TriggerCorePostExtract(appName string, sourceWorkDir string) error { + 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")) + b, _ := common.PlugnTriggerOutput("git-get-property", []string{appName, "source-image"}...) + appSourceImage := strings.TrimSpace(string(b[:])) + + repoDefaultAppJSONPath := path.Join(sourceWorkDir, "app.json") + if appSourceImage == "" { + repoAppJSONPath := path.Join(sourceWorkDir, 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 := copy.Copy(repoAppJSONPath, processSpecificAppJSON); err != nil { + return fmt.Errorf("Unable to extract app.json: %v", err.Error()) + } + + if appJSONPath != "app.json" { + if err := copy.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, appJSONPath, processSpecificAppJSON); err != nil { + return common.TouchFile(fmt.Sprintf("%s.missing", processSpecificAppJSON)) + } + } + + // TODO: add validation to app.json file by ensuring it can be deserialized + return nil +} + // TriggerInstall initializes app-json directory structures func TriggerInstall() error { if err := common.PropertySetup("app-json"); err != nil { @@ -104,10 +185,6 @@ func TriggerPostDeploy(appName string, imageTag string) error { // TriggerPreDeploy is a trigger to execute predeploy and release deployment tasks func TriggerPreDeploy(appName string, imageTag string) error { image := common.GetAppImageName(appName, imageTag, "") - if err := refreshAppJSON(appName, image); err != nil { - return err - } - if err := executeScript(appName, image, imageTag, "predeploy"); err != nil { return err }