Files
dokku/plugins/common/common.go
Jose Diaz-Gonzalez 1e915968b5 fix: correctly handle is-deployed check
Previously, checking if an app was deployed actually checked if there were any running processes. This is not only incorrect, but also fails to take into account applications that do not have running processes and are only used for one-off commands.

This fix migrates apps to the new "deployed" property which is set in core-post-deploy. The result is a slightly faster "deployed" check that is correct and allows non-scaled apps to actually work.

Closes #4398
2021-02-13 00:46:35 -05:00

541 lines
14 KiB
Go

package common
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"unicode"
"github.com/ryanuber/columnize"
"golang.org/x/sync/errgroup"
)
type errfunc func() error
var (
// DefaultProperties is a map of all valid common properties with corresponding default property values
DefaultProperties = map[string]string{
"deployed": "false",
}
// GlobalProperties is a map of all valid global common properties
GlobalProperties = map[string]bool{}
)
// 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")
LogWarn(fmt.Sprintf("This command will destroy %v %v.", objectType, name))
LogWarn(fmt.Sprintf("To proceed, type \"%v\"", name))
fmt.Print("> ")
var response string
_, err := fmt.Scanln(&response)
if err != nil {
return err
}
if response != name {
LogStderr("Confirmation did not match test. Aborted.")
os.Exit(1)
return nil
}
return nil
}
// CommandUsage outputs help for a command
func CommandUsage(helpHeader string, helpContent string) {
config := columnize.DefaultConfig()
config.Delim = ","
config.Prefix = " "
config.Empty = ""
content := strings.Split(helpContent, "\n")[1:]
fmt.Println(helpHeader)
fmt.Println(columnize.Format(content, config))
}
// GetAppScheduler fetches the scheduler for a given application
func GetAppScheduler(appName string) string {
appScheduler := ""
globalScheduler := ""
ctx := context.Background()
errs, ctx := errgroup.WithContext(ctx)
if appName != "--global" {
errs.Go(func() error {
appScheduler = getAppScheduler(appName)
return nil
})
}
errs.Go(func() error {
globalScheduler = GetGlobalScheduler()
return nil
})
errs.Wait()
if appScheduler == "" {
appScheduler = globalScheduler
}
return appScheduler
}
func getAppScheduler(appName string) string {
b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SCHEDULER"}...)
value := strings.TrimSpace(string(b[:]))
if value != "" {
return value
}
return ""
}
// GetGlobalScheduler fetchs the global scheduler
func GetGlobalScheduler() string {
b, _ := PlugnTriggerOutput("config-get-global", []string{"DOKKU_SCHEDULER"}...)
value := strings.TrimSpace(string(b[:]))
if value != "" {
return value
}
return "docker-local"
}
// GetDeployingAppImageName returns deploying image identifier for a given app, tag tuple. validate if tag is presented
func GetDeployingAppImageName(appName, imageTag, imageRepo string) (string, error) {
imageRemoteRepository := ""
newImageTag := ""
newImageRepo := ""
ctx := context.Background()
errs, ctx := errgroup.WithContext(ctx)
errs.Go(func() error {
b, err := PlugnTriggerOutput("deployed-app-repository", []string{appName}...)
if err == nil {
imageRemoteRepository = strings.TrimSpace(string(b[:]))
}
return err
})
errs.Go(func() error {
b, err := PlugnTriggerOutput("deployed-app-image-tag", []string{appName}...)
if err == nil {
newImageTag = strings.TrimSpace(string(b[:]))
}
return err
})
errs.Go(func() error {
b, err := PlugnTriggerOutput("deployed-app-image-repo", []string{appName}...)
if err == nil {
newImageRepo = strings.TrimSpace(string(b[:]))
}
return err
})
if err := errs.Wait(); err != nil {
return "", err
}
if newImageRepo != "" {
imageRepo = newImageRepo
}
if newImageTag != "" {
imageTag = newImageTag
}
if imageRepo == "" {
imageRepo = GetAppImageRepo(appName)
}
if imageTag == "" {
imageTag = "latest"
}
imageName := fmt.Sprintf("%s%s:%s", imageRemoteRepository, imageRepo, imageTag)
if !VerifyImage(imageName) {
return "", fmt.Errorf("App image (%s) not found", imageName)
}
return imageName, nil
}
// GetAppImageRepo is the central definition of a dokku image repo pattern
func GetAppImageRepo(appName string) string {
return strings.Join([]string{"dokku", appName}, "/")
}
// GetAppContainerIDs returns a list of docker container ids for given app and optional container_type
func GetAppContainerIDs(appName string, containerType string) ([]string, error) {
var containerIDs []string
appRoot := AppRoot(appName)
containerFilePath := fmt.Sprintf("%v/CONTAINER", appRoot)
_, err := os.Stat(containerFilePath)
if !os.IsNotExist(err) {
containerIDs = append(containerIDs, ReadFirstLine(containerFilePath))
}
containerPattern := fmt.Sprintf("%v/CONTAINER.*", appRoot)
if containerType != "" {
containerPattern = fmt.Sprintf("%v/CONTAINER.%v.*", appRoot, containerType)
if strings.Contains(".", containerType) {
containerPattern = fmt.Sprintf("%v/CONTAINER.%v", appRoot, containerType)
}
}
files, _ := filepath.Glob(containerPattern)
for _, containerFile := range files {
containerIDs = append(containerIDs, ReadFirstLine(containerFile))
}
return containerIDs, nil
}
// GetAppRunningContainerIDs return a list of running docker container ids for given app and optional container_type
func GetAppRunningContainerIDs(appName string, containerType string) ([]string, error) {
var runningContainerIDs []string
if !IsDeployed(appName) {
LogFail(fmt.Sprintf("App %v has not been deployed", appName))
}
containerIDs, err := GetAppContainerIDs(appName, containerType)
if err != nil {
return runningContainerIDs, nil
}
for _, containerID := range containerIDs {
if ContainerIsRunning(containerID) {
runningContainerIDs = append(runningContainerIDs, containerID)
}
}
return runningContainerIDs, nil
}
// GetRunningImageTag retrieves current image tag for a given app and returns empty string if no deployed containers are found
func GetRunningImageTag(appName string) (string, error) {
containerIDs, err := GetAppContainerIDs(appName, "")
if err != nil {
return "", err
}
for _, containerID := range containerIDs {
if image, err := DockerInspect(containerID, "{{ .Config.Image }}"); err == nil {
return strings.Split(image, ":")[1], nil
}
}
return "", errors.New("No image tag found")
}
// DokkuApps returns a list of all local apps
func DokkuApps() (apps []string, err error) {
dokkuRoot := MustGetEnv("DOKKU_ROOT")
files, err := ioutil.ReadDir(dokkuRoot)
if err != nil {
err = fmt.Errorf("You haven't deployed any applications yet")
return
}
for _, f := range files {
appRoot := AppRoot(f.Name())
if !DirectoryExists(appRoot) {
continue
}
if strings.HasPrefix(f.Name(), ".") {
continue
}
apps = append(apps, f.Name())
}
if len(apps) == 0 {
err = fmt.Errorf("You haven't deployed any applications yet")
return
}
return
}
// GetAppImageName returns image identifier for a given app, tag tuple. validate if tag is presented
func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) {
if imageRepo == "" {
imageRepo = GetAppImageRepo(appName)
}
if imageTag == "" {
imageName = fmt.Sprintf("%v:latest", imageRepo)
} else {
imageName = fmt.Sprintf("%v:%v", imageRepo, imageTag)
if !VerifyImage(imageName) {
LogFail(fmt.Sprintf("App image (%s) not found", imageName))
}
}
return
}
// IsDeployed returns true if given app has a running container
func IsDeployed(appName string) bool {
deployed := PropertyGetDefault("common", appName, "deployed", "")
if deployed == "" {
deployed = "false"
scheduler := GetAppScheduler(appName)
_, err := PlugnTriggerOutput("scheduler-is-deployed", []string{scheduler, appName}...)
if err == nil {
deployed = "true"
}
CommandPropertySet("common", appName, "deployed", deployed, DefaultProperties, GlobalProperties)
}
return deployed == "true"
}
// MustGetEnv returns env variable or fails if it's not set
func MustGetEnv(key string) (val string) {
val = os.Getenv(key)
if val == "" {
LogFail(fmt.Sprintf("%s not set!", key))
}
return
}
// GetenvWithDefault returns env variable or defaultValue if it's not set
func GetenvWithDefault(key string, defaultValue string) (val string) {
val = os.Getenv(key)
if val == "" {
val = defaultValue
}
return
}
// ParseReportArgs splits out flags from non-flags for input into report commands
func ParseReportArgs(pluginName string, arguments []string) ([]string, string, error) {
osArgs := []string{}
infoFlags := []string{}
skipNext := false
for i, argument := range arguments {
if skipNext {
skipNext = false
continue
}
if argument == "--format" {
osArgs = append(osArgs, argument, arguments[i+1])
skipNext = true
continue
}
if strings.HasPrefix(argument, "--") {
infoFlags = append(infoFlags, argument)
} else {
osArgs = append(osArgs, argument)
}
}
if len(infoFlags) == 0 {
return osArgs, "", nil
}
if len(infoFlags) == 1 {
return osArgs, infoFlags[0], nil
}
return osArgs, "", fmt.Errorf("%s:report command allows only a single flag", pluginName)
}
// ReportSingleApp is an internal function that displays a report for an app
func ReportSingleApp(reportType string, appName string, infoFlag string, infoFlags map[string]string, infoFlagKeys []string, format string, trimPrefix bool, uppercaseFirstCharacter bool) error {
if format != "stdout" && infoFlag != "" {
return errors.New("--format flag cannot be specified when specifying an info flag")
}
if format == "json" {
data := map[string]string{}
for key, value := range infoFlags {
prefix := "--"
if trimPrefix {
prefix = fmt.Sprintf("--%v-", reportType)
}
// key = strings.Replace(strings.Replace(strings.TrimPrefix(key, prefix), "-", " ", -1), ".", " ", -1)
data[strings.TrimPrefix(key, prefix)] = value
}
out, err := json.Marshal(data)
if err != nil {
return err
}
Log(string(out))
return nil
}
flags := []string{}
for key := range infoFlags {
flags = append(flags, key)
}
sort.Strings(flags)
if len(infoFlag) == 0 {
LogInfo2Quiet(fmt.Sprintf("%s %v information", appName, reportType))
for _, k := range flags {
v, ok := infoFlags[k]
if !ok {
continue
}
prefix := "--"
if trimPrefix {
prefix = fmt.Sprintf("--%v-", reportType)
}
key := strings.Replace(strings.Replace(strings.TrimPrefix(k, prefix), "-", " ", -1), ".", " ", -1)
if uppercaseFirstCharacter {
key = UcFirst(key)
}
LogVerbose(fmt.Sprintf("%s%s", RightPad(fmt.Sprintf("%s:", key), 31, " "), v))
}
return nil
}
for _, k := range flags {
if infoFlag == k {
v, ok := infoFlags[k]
if !ok {
continue
}
fmt.Println(v)
return nil
}
}
sort.Strings(infoFlagKeys)
return fmt.Errorf("Invalid flag passed, valid flags: %s", strings.Join(infoFlagKeys, ", "))
}
// RightPad right-pads the string with pad up to len runes
func RightPad(str string, length int, pad string) string {
return str + times(pad, length-len(str))
}
// ShiftString removes the first and returns that entry as well as the rest of the list
func ShiftString(a []string) (string, []string) {
if len(a) == 0 {
return "", a
}
return a[0], a[1:]
}
// StripInlineComments removes bash-style comment from input line
func StripInlineComments(text string) string {
b := []byte(text)
re := regexp.MustCompile("(?s)#.*")
b = re.ReplaceAll(b, nil)
return strings.TrimSpace(string(b))
}
// SuppressOutput suppresses the output of a function unless there is an error
func SuppressOutput(f errfunc) error {
rescueStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
err := f()
w.Close()
out, _ := ioutil.ReadAll(r)
os.Stdout = rescueStdout
if err != nil {
fmt.Printf(string(out[:]))
}
return err
}
// ToBool returns a bool value for a given string
func ToBool(s string) bool {
return s == "true"
}
// ToInt returns an int value for a given string
func ToInt(s string, defaultValue int) int {
i, err := strconv.Atoi(s)
if err != nil {
return defaultValue
}
return i
}
// UcFirst uppercases the first character in a string
func UcFirst(str string) string {
for i, v := range str {
return string(unicode.ToUpper(v)) + str[i+1:]
}
return ""
}
// IsValidAppName verifies that the app name matches naming restrictions
func IsValidAppName(appName string) error {
if appName == "" {
return errors.New("Please specify an app to run the command on")
}
r, _ := regexp.Compile("^[a-z0-9][^/:_A-Z]*$")
if r.MatchString(appName) {
return nil
}
return errors.New("App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, colons, or underscores")
}
// isValidAppNameOld verifies that the app name matches the old naming restrictions
func isValidAppNameOld(appName string) error {
if appName == "" {
return errors.New("Please specify an app to run the command on")
}
r, _ := regexp.Compile("^[a-z0-9][^/:A-Z]*$")
if r.MatchString(appName) {
return nil
}
return errors.New("App name must begin with lowercase alphanumeric character, and cannot include uppercase characters, or colons")
}
// VerifyAppName checks if an app conforming to either the old or new
// naming conventions exists
func VerifyAppName(appName string) error {
newErr := IsValidAppName(appName)
oldErr := isValidAppNameOld(appName)
if newErr != nil && oldErr != nil {
return newErr
}
appRoot := AppRoot(appName)
if !DirectoryExists(appRoot) {
return fmt.Errorf("App %s does not exist", appName)
}
return nil
}
func times(str string, n int) (out string) {
for i := 0; i < n; i++ {
out += str
}
return
}