refactor: first pass at rewriting apps plugin in go

This commit is contained in:
Jose Diaz-Gonzalez
2020-03-10 14:14:37 -04:00
parent 48dc5d8cc1
commit b968ced6e9
67 changed files with 3829 additions and 471 deletions

View File

@@ -2,6 +2,8 @@ package common
import (
"bufio"
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
@@ -96,6 +98,83 @@ func AskForDestructiveConfirmation(name string, objectType string) error {
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"
}
// GetDeployingAppImageName returns deploying image identifier for a given app, tag tuple. validate if tag is presented
func GetDeployingAppImageName(appName, imageTag, imageRepo string) (imageName string) {
if appName == "" {
@@ -152,8 +231,7 @@ func GetAppContainerIDs(appName string, containerType string) ([]string, error)
return containerIDs, err
}
dokkuRoot := MustGetEnv("DOKKU_ROOT")
appRoot := fmt.Sprintf("%v/%v", dokkuRoot, appName)
appRoot := AppRoot(appName)
containerFilePath := fmt.Sprintf("%v/CONTAINER", appRoot)
_, err := os.Stat(containerFilePath)
if !os.IsNotExist(err) {
@@ -200,6 +278,26 @@ func GetAppRunningContainerIDs(appName string, containerType string) ([]string,
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
func ContainerIsRunning(containerID string) bool {
b, err := DockerInspect(containerID, "'{{.State.Running}}'")
@@ -242,7 +340,7 @@ func DokkuApps() (apps []string, err error) {
}
for _, f := range files {
appRoot := strings.Join([]string{dokkuRoot, f.Name()}, "/")
appRoot := AppRoot(f.Name())
if !DirectoryExists(appRoot) {
continue
}
@@ -314,9 +412,7 @@ func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) {
// IsDeployed returns true if given app has a running container
func IsDeployed(appName string) bool {
dokkuRoot := MustGetEnv("DOKKU_ROOT")
appRoot := strings.Join([]string{dokkuRoot, appName}, "/")
files, err := ioutil.ReadDir(appRoot)
files, err := ioutil.ReadDir(AppRoot(appName))
if err != nil {
return false
}
@@ -450,21 +546,37 @@ func UcFirst(str string) string {
return ""
}
// VerifyAppName verifies app name format and app existence"
func VerifyAppName(appName string) (err error) {
// IsValidAppName verifies app name format
func IsValidAppName(appName string) error {
if appName == "" {
return fmt.Errorf("App name must not be null")
}
dokkuRoot := MustGetEnv("DOKKU_ROOT")
appRoot := strings.Join([]string{dokkuRoot, appName}, "/")
if !DirectoryExists(appRoot) {
return fmt.Errorf("app %s does not exist", appName)
return fmt.Errorf("APP must not be null")
}
r, _ := regexp.Compile("^[a-z0-9].*")
if !r.MatchString(appName) {
return fmt.Errorf("app name (%s) must begin with lowercase alphanumeric character", appName)
if r.MatchString(appName) {
return nil
}
return err
appRoot := AppRoot(appName)
if DirectoryExists(appRoot) {
os.RemoveAll(appRoot)
}
return errors.New("App name must begin with lowercase alphanumeric character")
}
// VerifyAppName verifies app name format and app existence"
func VerifyAppName(appName string) error {
if err := IsValidAppName(appName); err != nil {
return err
}
appRoot := AppRoot(appName)
if !DirectoryExists(appRoot) {
return fmt.Errorf("App %s does not exist", appName)
}
return nil
}
// VerifyImage returns true if docker image exists in local repo
@@ -486,24 +598,23 @@ func DockerBin() string {
// PlugnTrigger fire the given plugn trigger with the given args
func PlugnTrigger(triggerName string, args ...string) error {
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...).Run()
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...).Output()
return sh.Command("plugn", shellArgs...)
}
func times(str string, n int) (out string) {
@@ -512,3 +623,112 @@ func times(str string, n int) (out string) {
}
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()
}
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()
}

View File

@@ -12,6 +12,27 @@ func LogFail(text string) {
os.Exit(1)
}
// LogFailQuiet is the failure log formatter (with quiet option)
// prints text to stderr and exits with status 1
func LogFailQuiet(text string) {
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
fmt.Fprintln(os.Stderr, fmt.Sprintf("FAILED: %s", text))
}
os.Exit(1)
}
// Log is the log formatter
func Log(text string) {
fmt.Println(text)
}
// LogQuiet is the log formatter (with quiet option)
func LogQuiet(text string) {
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
fmt.Println(text)
}
}
// LogInfo1 is the info1 header formatter
func LogInfo1(text string) {
fmt.Println(fmt.Sprintf("-----> %s", text))