Files
dokku/plugins/plugin/plugin.go
Jose Diaz-Gonzalez 9c819cfebc feat: add --format json support to plugin:list
Adds a `--format json` flag to `plugin:list` whose output includes each plugin's install source - for git-based third-party plugins, the git remote URL, the checked-out commit, and the followed branch - so the set of installed plugins can be reconstructed elsewhere.

Closes #8798.
2026-07-07 05:17:40 -04:00

137 lines
3.6 KiB
Go

package plugin
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/dokku/dokku/plugins/common"
"golang.org/x/sync/errgroup"
)
// PluginInfo describes an installed dokku plugin
type PluginInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
Core bool `json:"core"`
Description string `json:"description"`
SourceURL string `json:"source_url"`
Committish string `json:"committish"`
Branch string `json:"branch"`
}
// CommandList lists all installed plugins in the specified format
func CommandList(format string) error {
if format != "json" {
return fmt.Errorf("Invalid output format specified, supported formats: json")
}
plugins, err := listPlugins()
if err != nil {
return err
}
out, err := json.Marshal(plugins)
if err != nil {
return err
}
common.Log(string(out))
return nil
}
// listPlugins returns metadata for every installed plugin, enriched with git
// source information for git-based third-party plugins
func listPlugins() ([]PluginInfo, error) {
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "plugn",
Args: []string{"list"},
})
if err != nil {
return nil, err
}
coreEnabledPath := common.MustGetEnv("PLUGIN_CORE_ENABLED_PATH")
availablePath := common.MustGetEnv("PLUGIN_AVAILABLE_PATH")
plugins := parsePlugnList(result.StdoutContents())
errs := new(errgroup.Group)
for i := range plugins {
i := i
errs.Go(func() error {
plugins[i].Core = common.IsSymlink(filepath.Join(coreEnabledPath, plugins[i].Name))
enrichGitSource(&plugins[i], availablePath)
return nil
})
}
if err := errs.Wait(); err != nil {
return nil, err
}
return plugins, nil
}
// parsePlugnList parses the output of `plugn list` into plugin metadata. Each
// data line is formatted as `<name> <version> <enabled|disabled> <description>`,
// preceded by a `plugn: <treeish>` header line that is skipped.
func parsePlugnList(output string) []PluginInfo {
plugins := []PluginInfo{}
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) < 3 || fields[0] == "plugn:" {
continue
}
description := ""
if len(fields) > 3 {
description = strings.Join(fields[3:], " ")
}
plugins = append(plugins, PluginInfo{
Name: fields[0],
Version: fields[1],
Enabled: fields[2] == "enabled",
Description: description,
})
}
return plugins
}
// enrichGitSource populates the git source fields for a git-based plugin,
// leaving them empty for core, tarball, or file-based installs
func enrichGitSource(info *PluginInfo, availablePath string) {
pluginDir := filepath.Join(availablePath, info.Name)
if _, err := os.Stat(filepath.Join(pluginDir, ".git")); err != nil {
return
}
info.SourceURL = gitOutput(pluginDir, "remote", "get-url", "origin")
info.Committish = gitOutput(pluginDir, "rev-parse", "HEAD")
if branch := gitOutput(pluginDir, "rev-parse", "--abbrev-ref", "HEAD"); branch != "HEAD" {
info.Branch = branch
}
}
// gitOutput runs a git command in the given directory and returns its trimmed
// stdout, or an empty string on error. The directory is marked as safe so the
// lookup works regardless of which user invokes plugin:list.
func gitOutput(dir string, args ...string) string {
gitArgs := append([]string{"-c", "safe.directory=" + dir}, args...)
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "git",
Args: gitArgs,
WorkingDirectory: dir,
})
if err != nil {
return ""
}
return result.StdoutContents()
}