feat: rewrite the storage plugin in golang

Refs #6814
This commit is contained in:
Jose Diaz-Gonzalez
2026-01-08 01:51:28 -05:00
parent b99e0b303d
commit 8af070ac00
26 changed files with 911 additions and 375 deletions

View 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)
}