241
plugins/storage/subcommands.go
Normal file
241
plugins/storage/subcommands.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
|
||||
)
|
||||
|
||||
const (
|
||||
helpHeader = `Usage: dokku storage[:COMMAND]
|
||||
|
||||
Manage mounted volumes
|
||||
|
||||
Additional commands:`
|
||||
|
||||
helpContent = `
|
||||
storage:ensure-directory [--chown option] <directory>, Creates a persistent storage directory in the recommended storage path
|
||||
storage:list <app> [--format text|json], List bind mounts for app's container(s) (host:container)
|
||||
storage:mount <app> <host-dir:container-dir>, Create a new bind mount
|
||||
storage:report [<app>] [<flag>], Displays a storage report for one or more apps
|
||||
storage:unmount <app> <host-dir:container-dir>, Remove an existing bind mount`
|
||||
)
|
||||
|
||||
// CommandHelp displays help for the storage plugin
|
||||
func CommandHelp() error {
|
||||
common.CommandUsage(helpHeader, helpContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommandEnsureDirectory creates a persistent storage directory
|
||||
func CommandEnsureDirectory(directory string, chownFlag string) error {
|
||||
if err := ValidateDirectoryName(directory); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chownID, err := resolveChownID(chownFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storageDirectory := filepath.Join(GetStorageDirectory(), directory)
|
||||
common.LogInfo1(fmt.Sprintf("Ensuring %s exists", storageDirectory))
|
||||
|
||||
if err := os.MkdirAll(storageDirectory, 0755); err != nil {
|
||||
return fmt.Errorf("Unable to create directory: %s", err.Error())
|
||||
}
|
||||
|
||||
if chownID != "false" {
|
||||
common.LogVerboseQuiet(fmt.Sprintf("Setting directory ownership to %s:%s", chownID, chownID))
|
||||
|
||||
pluginPath := common.MustGetEnv("PLUGIN_AVAILABLE_PATH")
|
||||
chownScript := filepath.Join(pluginPath, "storage", "bin", "chown-storage-dir")
|
||||
|
||||
result, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: "sudo",
|
||||
Args: []string{chownScript, directory, chownID},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to set directory ownership: %s", err.Error())
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("Unable to set directory ownership: %s", result.StderrContents())
|
||||
}
|
||||
}
|
||||
|
||||
common.LogVerboseQuiet("Directory ready for mounting")
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveChownID converts a chown flag value to a numeric UID
|
||||
func resolveChownID(chownFlag string) (string, error) {
|
||||
var chownID string
|
||||
|
||||
switch chownFlag {
|
||||
case "herokuish":
|
||||
chownID = "32767"
|
||||
case "heroku":
|
||||
chownID = "1000"
|
||||
case "packeto":
|
||||
common.LogVerbose("Detected deprecated chown flag 'packeto'. Using 'paketo' instead. Please update your configuration.")
|
||||
chownID = "2000"
|
||||
case "paketo":
|
||||
chownID = "2000"
|
||||
case "root":
|
||||
chownID = "0"
|
||||
case "false":
|
||||
return "false", nil
|
||||
default:
|
||||
return "", errors.New("Unsupported chown permissions")
|
||||
}
|
||||
|
||||
userns, err := isUserNamespacesEnabled()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if userns && chownID != "false" {
|
||||
uid := 0
|
||||
fmt.Sscanf(chownID, "%d", &uid)
|
||||
uid += 165536
|
||||
chownID = fmt.Sprintf("%d", uid)
|
||||
}
|
||||
|
||||
return chownID, nil
|
||||
}
|
||||
|
||||
// isUserNamespacesEnabled checks if Docker user namespaces are enabled
|
||||
func isUserNamespacesEnabled() (bool, error) {
|
||||
result, err := common.CallExecCommand(common.ExecCommandInput{
|
||||
Command: common.DockerBin(),
|
||||
Args: []string{"info", "-f", "{{range .SecurityOptions}}{{if eq . \"name=userns\"}}true{{end}}{{end}}"},
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.TrimSpace(result.StdoutContents()) == "true", nil
|
||||
}
|
||||
|
||||
// CommandMount creates a new bind mount for an app
|
||||
func CommandMount(appName string, mountPath string) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := VerifyPaths(mountPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if CheckIfPathExists(appName, mountPath, MountPhases) {
|
||||
return errors.New("Mount path already exists.")
|
||||
}
|
||||
|
||||
return dockeroptions.AddDockerOptionToPhases(appName, MountPhases, fmt.Sprintf("-v %s", mountPath))
|
||||
}
|
||||
|
||||
// CommandUnmount removes an existing bind mount from an app
|
||||
func CommandUnmount(appName string, mountPath string) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := VerifyPaths(mountPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !CheckIfPathExists(appName, mountPath, MountPhases) {
|
||||
return errors.New("Mount path does not exist.")
|
||||
}
|
||||
|
||||
return dockeroptions.RemoveDockerOptionFromPhases(appName, MountPhases, fmt.Sprintf("-v %s", mountPath))
|
||||
}
|
||||
|
||||
// CommandList lists all bind mounts for an app
|
||||
func CommandList(appName string, format string) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if format != "text" && format != "json" {
|
||||
return errors.New("Invalid --format value specified")
|
||||
}
|
||||
|
||||
if format == "text" {
|
||||
common.LogInfo1Quiet(fmt.Sprintf("%s volume bind-mounts:", appName))
|
||||
}
|
||||
|
||||
results, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "storage-list",
|
||||
Args: []string{appName, "deploy", format},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output := results.StdoutContents()
|
||||
if format == "text" && output != "" {
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
if os.Getenv("DOKKU_QUIET_OUTPUT") != "" {
|
||||
fmt.Println(line)
|
||||
} else {
|
||||
common.LogVerbose(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if output != "" {
|
||||
fmt.Println(output)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommandReport displays a storage report for one or more apps
|
||||
func CommandReport(appName string, infoFlag string) error {
|
||||
if appName == "" {
|
||||
apps, err := common.DokkuApps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, app := range apps {
|
||||
if err := reportSingle(app, infoFlag, "stdout"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return reportSingle(appName, infoFlag, "stdout")
|
||||
}
|
||||
|
||||
// CommandReportSingleApp displays a storage report for a single app with format support
|
||||
func CommandReportSingleApp(appName string, infoFlag string, format string) error {
|
||||
return reportSingle(appName, infoFlag, format)
|
||||
}
|
||||
|
||||
func reportSingle(appName string, infoFlag string, format string) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
infoFlags := map[string]string{
|
||||
"--storage-build-mounts": GetBindMountsForDisplay(appName, "build"),
|
||||
"--storage-deploy-mounts": GetBindMountsForDisplay(appName, "deploy"),
|
||||
"--storage-run-mounts": GetBindMountsForDisplay(appName, "run"),
|
||||
}
|
||||
|
||||
infoFlagKeys := []string{
|
||||
"--storage-build-mounts",
|
||||
"--storage-deploy-mounts",
|
||||
"--storage-run-mounts",
|
||||
}
|
||||
|
||||
return common.ReportSingleApp("storage", appName, infoFlag, infoFlags, infoFlagKeys, format, true, true)
|
||||
}
|
||||
Reference in New Issue
Block a user