Files
dokku/plugins/common/common.go

766 lines
18 KiB
Go
Raw Normal View History

2017-01-03 22:27:20 -08:00
package common
import (
"bufio"
"bytes"
"errors"
2017-01-03 22:27:20 -08:00
"fmt"
"io/ioutil"
2017-01-03 22:27:20 -08:00
"os"
"os/exec"
"path/filepath"
"reflect"
2017-01-03 22:27:20 -08:00
"regexp"
"sort"
2017-01-03 22:27:20 -08:00
"strings"
2017-10-04 00:48:02 -04:00
"unicode"
2017-01-03 22:27:20 -08:00
sh "github.com/codeskyblue/go-sh"
)
type errfunc func() error
// ShellCmd represents a shell command to be run for dokku
type ShellCmd struct {
2017-01-03 22:27:20 -08:00
Env map[string]string
Command *exec.Cmd
CommandString string
Args []string
ShowOutput bool
}
// NewShellCmd returns a new ShellCmd struct
func NewShellCmd(command string) *ShellCmd {
2017-01-03 22:27:20 -08:00
items := strings.Split(command, " ")
cmd := items[0]
args := items[1:]
return &ShellCmd{
2017-01-03 22:27:20 -08:00
Command: exec.Command(cmd, args...),
CommandString: command,
2017-01-03 22:27:20 -08:00
Args: args,
ShowOutput: true,
}
}
// Execute is a lightweight wrapper around exec.Command
func (sc *ShellCmd) Execute() bool {
2017-01-03 22:27:20 -08:00
env := os.Environ()
for k, v := range sc.Env {
2017-01-03 22:27:20 -08:00
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
sc.Command.Env = env
if sc.ShowOutput {
sc.Command.Stdout = os.Stdout
sc.Command.Stderr = os.Stderr
2017-01-03 22:27:20 -08:00
}
2017-10-02 16:50:05 -07:00
if err := sc.Command.Run(); err != nil {
2017-01-03 22:27:20 -08:00
return false
}
return true
}
// Output is a lightweight wrapper around exec.Command.Output()
func (sc *ShellCmd) Output() ([]byte, error) {
env := os.Environ()
for k, v := range sc.Env {
env = append(env, fmt.Sprintf("%s=%s", k, v))
2017-01-03 22:27:20 -08:00
}
sc.Command.Env = env
if sc.ShowOutput {
sc.Command.Stdout = os.Stdout
sc.Command.Stderr = os.Stderr
2017-01-03 22:27:20 -08:00
}
return sc.Command.Output()
2017-01-03 22:27:20 -08:00
}
// AppRoot returns the app root path
func AppRoot(appName string) string {
dokkuRoot := MustGetEnv("DOKKU_ROOT")
return fmt.Sprintf("%v/%v", dokkuRoot, appName)
}
2020-03-10 23:56:16 -04:00
// 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
}
// DockerCleanup cleans up all exited/dead containers and removes all dangling images
func DockerCleanup(appName string, forceCleanup bool) error {
if !forceCleanup {
skipCleanup := false
if appName != "" {
b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SKIP_CLEANUP"}...)
output := string(b[:])
if output == "true" {
skipCleanup = true
}
}
if skipCleanup || os.Getenv("DOKKU_SKIP_CLEANUP") == "true" {
LogInfo1("DOKKU_SKIP_CLEANUP set. Skipping dokku cleanup")
return nil
}
}
LogInfo1("Cleaning up...")
scheduler := GetAppScheduler(appName)
if appName == "--global" {
appName = ""
}
forceCleanupArg := "false"
if forceCleanup {
forceCleanupArg = "true"
}
if err := PlugnTrigger("scheduler-docker-cleanup", []string{scheduler, appName, forceCleanupArg}...); err != nil {
return fmt.Errorf("Failure while cleaning up app: %s", err)
}
// delete all non-running and dead containers
exitedContainerIDs, _ := listContainers("exited", appName)
deadContainerIDs, _ := listContainers("dead", appName)
containerIDs := append(exitedContainerIDs, deadContainerIDs...)
if len(containerIDs) > 0 {
removeContainers(containerIDs)
}
// delete dangling images
imageIDs, _ := listDanglingImages(appName)
if len(imageIDs) > 0 {
RemoveImages(imageIDs)
}
if appName != "" {
// delete unused images
pruneUnusedImages(appName)
}
return nil
}
// GetAppScheduler fetches the scheduler for a given application
func GetAppScheduler(appName string) string {
if appName == "--global" {
appName = ""
}
b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SCHEDULER"}...)
value := string(b[:])
if value != "" {
return value
}
b, _ = PlugnTriggerOutput("config-get-global", []string{"DOKKU_SCHEDULER"}...)
value = string(b[:])
if value != "" {
return value
}
return "docker-local"
}
2017-01-03 22:27:20 -08:00
// GetDeployingAppImageName returns deploying image identifier for a given app, tag tuple. validate if tag is presented
func GetDeployingAppImageName(appName, imageTag, imageRepo string) (imageName string) {
2017-01-03 22:27:20 -08:00
if appName == "" {
LogFail("(GetDeployingAppImageName) APP must not be empty")
}
b, err := PlugnTriggerOutput("deployed-app-repository", []string{appName}...)
2017-01-03 22:27:20 -08:00
if err != nil {
LogFail(err.Error())
}
imageRemoteRepository := string(b[:])
b, err = PlugnTriggerOutput("deployed-app-image-tag", []string{appName}...)
2017-01-03 22:27:20 -08:00
if err != nil {
LogFail(err.Error())
}
newImageTag := string(b[:])
b, err = PlugnTriggerOutput("deployed-app-image-repo", []string{appName}...)
2017-01-03 22:27:20 -08:00
if err != nil {
LogFail(err.Error())
}
newImageRepo := string(b[:])
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) {
LogFail(fmt.Sprintf("App image (%s) not found", imageName))
2017-01-03 22:27:20 -08:00
}
return
}
// 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
if err := VerifyAppName(appName); err != nil {
return containerIDs, err
}
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 err := VerifyAppName(appName); err != nil {
return runningContainerIDs, err
}
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) {
if err := VerifyAppName(appName); err != nil {
return "", err
}
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")
}
// ContainerIsRunning checks to see if a container is running
2017-07-22 15:32:14 -06:00
func ContainerIsRunning(containerID string) bool {
b, err := DockerInspect(containerID, "'{{.State.Running}}'")
if err != nil {
return false
}
2017-04-26 18:49:19 -06:00
return strings.TrimSpace(string(b[:])) == "true"
}
// DirectoryExists returns if a path exists and is a directory
func DirectoryExists(filePath string) bool {
fi, err := os.Stat(filePath)
if err != nil {
return false
}
return fi.IsDir()
}
// DockerInspect runs an inspect command with a given format against a container id
func DockerInspect(containerOrImageID, format string) (output string, err error) {
b, err := sh.Command(DockerBin(), "inspect", "--format", format, containerOrImageID).Output()
if err != nil {
return "", err
}
2017-10-02 16:50:05 -07:00
output = strings.TrimSpace(string(b[:]))
2017-07-22 15:32:14 -06:00
if strings.HasPrefix(output, "'") && strings.HasSuffix(output, "'") {
output = strings.TrimSuffix(strings.TrimPrefix(output, "'"), "'")
}
2017-10-02 16:50:05 -07:00
return
}
// DokkuApps returns a list of all local apps
2017-10-02 16:50:05 -07:00
func DokkuApps() (apps []string, err error) {
dokkuRoot := MustGetEnv("DOKKU_ROOT")
files, err := ioutil.ReadDir(dokkuRoot)
if err != nil {
2017-10-02 16:50:05 -07:00
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 {
2017-10-02 16:50:05 -07:00
err = fmt.Errorf("You haven't deployed any applications yet")
return
}
2017-10-02 16:50:05 -07:00
return
}
// FileToSlice reads in all the lines from a file into a string slice
2017-10-02 16:50:05 -07:00
func FileToSlice(filePath string) (lines []string, err error) {
f, err := os.Open(filePath)
if err != nil {
2017-10-02 16:50:05 -07:00
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text == "" {
continue
}
lines = append(lines, text)
}
err = scanner.Err()
2017-10-02 16:50:05 -07:00
return
}
// FileExists returns if a path exists and is a file
func FileExists(filePath string) bool {
fi, err := os.Stat(filePath)
if err != nil {
return false
}
return fi.Mode().IsRegular()
}
2017-04-26 18:49:19 -06:00
// GetAppImageName returns image identifier for a given app, tag tuple. validate if tag is presented
func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) {
err := VerifyAppName(appName)
if err != nil {
LogFail(err.Error())
2017-01-03 22:27:20 -08:00
}
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
}
2017-07-22 15:32:14 -06:00
// IsDeployed returns true if given app has a running container
func IsDeployed(appName string) bool {
files, err := ioutil.ReadDir(AppRoot(appName))
if err != nil {
return false
}
for _, f := range files {
if f.Name() == "CONTAINER" || strings.HasPrefix(f.Name(), "CONTAINER.") {
return true
}
}
2017-01-03 22:27:20 -08:00
return false
}
// IsImageHerokuishBased returns true if app image is based on herokuish
func IsImageHerokuishBased(image string, appName string) bool {
output, err := DockerInspect(image, "{{range .Config.Env}}{{if eq . \"USER=herokuishuser\" }}{{println .}}{{end}}{{end}}")
if err != nil {
return false
}
return output != ""
}
2017-09-03 19:34:44 -04:00
// MustGetEnv returns env variable or fails if it's not set
2017-10-02 16:50:05 -07:00
func MustGetEnv(key string) (val string) {
val = os.Getenv(key)
if val == "" {
2017-09-03 19:34:44 -04:00
LogFail(fmt.Sprintf("%s not set!", key))
}
2017-10-02 16:50:05 -07:00
return
2017-09-03 19:34:44 -04:00
}
// 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
}
// ReadFirstLine gets the first line of a file that has contents and returns it
// if there are no contents, an empty string is returned
// will also return an empty string if the file does not exist
2017-10-02 16:50:05 -07:00
func ReadFirstLine(filename string) (text string) {
if !FileExists(filename) {
2017-10-02 16:50:05 -07:00
return
}
f, err := os.Open(filename)
if err != nil {
2017-10-02 16:50:05 -07:00
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
2017-10-02 16:50:05 -07:00
if text = strings.TrimSpace(scanner.Text()); text == "" {
continue
}
2017-10-02 16:50:05 -07:00
return
}
2017-10-02 16:50:05 -07:00
return
}
// ReportSingleApp is an internal function that displays a report for an app
func ReportSingleApp(reportType string, appName string, infoFlag string, infoFlags map[string]string, trimPrefix bool, uppercaseFirstCharacter bool) error {
flags := []string{}
2020-02-09 20:47:20 -05:00
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 := infoFlags[k]
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 := infoFlags[k]
fmt.Println(v)
return nil
}
}
keys := reflect.ValueOf(infoFlags).MapKeys()
strkeys := make([]string, len(keys))
for i := 0; i < len(keys); i++ {
strkeys[i] = keys[i].String()
}
2020-02-22 16:56:35 -05:00
return fmt.Errorf("Invalid flag passed, valid flags: %s", strings.Join(strkeys, ", "))
}
// 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))
}
2020-06-02 00:51:45 -04:00
// 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 {
2017-10-02 16:50:05 -07:00
bytes := []byte(text)
re := regexp.MustCompile("(?s)#.*")
bytes = re.ReplaceAll(bytes, nil)
return strings.TrimSpace(string(bytes))
}
// 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"
}
2017-09-03 19:34:44 -04:00
2017-10-04 00:48:02 -04:00
// 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 app name format
func IsValidAppName(appName string) error {
2017-09-03 19:34:44 -04:00
if appName == "" {
return fmt.Errorf("APP must not be null")
2017-09-03 19:34:44 -04:00
}
r, _ := regexp.Compile("^[a-z0-9][^/:A-Z]*$")
if r.MatchString(appName) {
return nil
}
appRoot := AppRoot(appName)
if DirectoryExists(appRoot) {
os.RemoveAll(appRoot)
}
return errors.New("App name must begin with lowercase alphanumeric character, and cannot include uppercase characters or colons")
}
// VerifyAppName verifies app name format and app existence"
func VerifyAppName(appName string) error {
if err := IsValidAppName(appName); err != nil {
return err
2017-09-03 19:34:44 -04:00
}
appRoot := AppRoot(appName)
if !DirectoryExists(appRoot) {
return fmt.Errorf("App %s does not exist", appName)
}
return nil
2017-09-03 19:34:44 -04:00
}
2017-01-03 22:27:20 -08:00
// VerifyImage returns true if docker image exists in local repo
func VerifyImage(image string) bool {
imageCmd := NewShellCmd(strings.Join([]string{DockerBin(), "image", "inspect", image}, " "))
2017-01-03 22:27:20 -08:00
imageCmd.ShowOutput = false
2017-09-03 19:34:44 -04:00
return imageCmd.Execute()
2017-01-03 22:27:20 -08:00
}
// DockerBin returns a string which contains a path to the current docker binary
func DockerBin() string {
dockerBin := os.Getenv("DOCKER_BIN")
if dockerBin == "" {
dockerBin = "docker"
}
return dockerBin
}
// PlugnTrigger fire the given plugn trigger with the given args
func PlugnTrigger(triggerName string, args ...string) error {
return PlugnTriggerSetup(triggerName, args...).Run()
}
// PlugnTriggerOutput fire the given plugn trigger with the given args
func PlugnTriggerOutput(triggerName string, args ...string) ([]byte, error) {
return PlugnTriggerSetup(triggerName, args...).Output()
}
// PlugnTriggerSetup sets up a plugn trigger call
func PlugnTriggerSetup(triggerName string, args ...string) *sh.Session {
shellArgs := make([]interface{}, len(args)+2)
shellArgs[0] = "trigger"
shellArgs[1] = triggerName
for i, arg := range args {
shellArgs[i+2] = arg
}
return sh.Command("plugn", shellArgs...)
}
func times(str string, n int) (out string) {
for i := 0; i < n; i++ {
out += str
}
return
}
func listContainers(status string, appName string) ([]string, error) {
command := []string{
DockerBin(),
"container",
"list",
"--quiet",
"--all",
"--filter",
fmt.Sprintf("status=%v", status),
"--filter",
fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")),
}
if appName != "" {
command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...)
}
var stderr bytes.Buffer
listCmd := NewShellCmd(strings.Join(command, " "))
listCmd.ShowOutput = false
listCmd.Command.Stderr = &stderr
b, err := listCmd.Output()
if err != nil {
return []string{}, errors.New(strings.TrimSpace(stderr.String()))
}
output := strings.Split(strings.TrimSpace(string(b[:])), "\n")
return output, nil
}
func listDanglingImages(appName string) ([]string, error) {
command := []string{
DockerBin(),
"image",
"list",
"--quiet",
"--filter",
"dangling=true",
}
if appName != "" {
command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...)
}
var stderr bytes.Buffer
listCmd := NewShellCmd(strings.Join(command, " "))
listCmd.ShowOutput = false
listCmd.Command.Stderr = &stderr
b, err := listCmd.Output()
if err != nil {
return []string{}, errors.New(strings.TrimSpace(stderr.String()))
}
output := strings.Split(strings.TrimSpace(string(b[:])), "\n")
return output, nil
}
func removeContainers(containerIDs []string) {
command := []string{
DockerBin(),
"container",
"rm",
}
command = append(command, containerIDs...)
var stderr bytes.Buffer
rmCmd := NewShellCmd(strings.Join(command, " "))
rmCmd.ShowOutput = false
rmCmd.Command.Stderr = &stderr
rmCmd.Execute()
}
2020-03-10 23:56:16 -04:00
// RemoveImages removes images by ID
func RemoveImages(imageIDs []string) {
command := []string{
DockerBin(),
"image",
"rm",
}
command = append(command, imageIDs...)
var stderr bytes.Buffer
rmCmd := NewShellCmd(strings.Join(command, " "))
rmCmd.ShowOutput = false
rmCmd.Command.Stderr = &stderr
rmCmd.Execute()
}
func pruneUnusedImages(appName string) {
command := []string{
DockerBin(),
"image",
"prune",
"--all",
"--force",
"--filter",
fmt.Sprintf("label=com.dokku.app-name=%v", appName),
}
var stderr bytes.Buffer
pruneCmd := NewShellCmd(strings.Join(command, " "))
pruneCmd.ShowOutput = false
pruneCmd.Command.Stderr = &stderr
pruneCmd.Execute()
}