cli/api: Update plugin listing to always include version info in the response (#17347)
This commit is contained in:
parent
e9c2332ee5
commit
12ca81bc9b
|
@ -32,7 +32,7 @@ type ListPluginsResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginDetails struct {
|
type PluginDetails struct {
|
||||||
Type string `json:"string"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
Builtin bool `json:"builtin"`
|
Builtin bool `json:"builtin"`
|
||||||
|
@ -50,25 +50,7 @@ func (c *Sys) ListPluginsWithContext(ctx context.Context, i *ListPluginsInput) (
|
||||||
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
|
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
|
||||||
defer cancelFunc()
|
defer cancelFunc()
|
||||||
|
|
||||||
path := ""
|
resp, err := c.c.rawRequestWithContext(ctx, c.c.NewRequest(http.MethodGet, "/v1/sys/plugins/catalog"))
|
||||||
method := ""
|
|
||||||
if i.Type == consts.PluginTypeUnknown {
|
|
||||||
path = "/v1/sys/plugins/catalog"
|
|
||||||
method = http.MethodGet
|
|
||||||
} else {
|
|
||||||
path = fmt.Sprintf("/v1/sys/plugins/catalog/%s", i.Type)
|
|
||||||
method = "LIST"
|
|
||||||
}
|
|
||||||
|
|
||||||
req := c.c.NewRequest(method, path)
|
|
||||||
if method == "LIST" {
|
|
||||||
// Set this for broader compatibility, but we use LIST above to be able
|
|
||||||
// to handle the wrapping lookup function
|
|
||||||
req.Method = http.MethodGet
|
|
||||||
req.Params.Set("list", "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.c.rawRequestWithContext(ctx, req)
|
|
||||||
if err != nil && resp == nil {
|
if err != nil && resp == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -77,27 +59,6 @@ func (c *Sys) ListPluginsWithContext(ctx context.Context, i *ListPluginsInput) (
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// We received an Unsupported Operation response from Vault, indicating
|
|
||||||
// Vault of an older version that doesn't support the GET method yet;
|
|
||||||
// switch it to a LIST.
|
|
||||||
if resp.StatusCode == 405 {
|
|
||||||
req.Params.Set("list", "true")
|
|
||||||
resp, err := c.c.rawRequestWithContext(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
var result struct {
|
|
||||||
Data struct {
|
|
||||||
Keys []string `json:"keys"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
if err := resp.DecodeJSON(&result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ListPluginsResponse{Names: result.Data.Keys}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
secret, err := ParseSecret(resp.Body)
|
secret, err := ParseSecret(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -108,9 +69,9 @@ func (c *Sys) ListPluginsWithContext(ctx context.Context, i *ListPluginsInput) (
|
||||||
|
|
||||||
result := &ListPluginsResponse{
|
result := &ListPluginsResponse{
|
||||||
PluginsByType: make(map[consts.PluginType][]string),
|
PluginsByType: make(map[consts.PluginType][]string),
|
||||||
Details: []PluginDetails{},
|
|
||||||
}
|
}
|
||||||
if i.Type == consts.PluginTypeUnknown {
|
switch i.Type {
|
||||||
|
case consts.PluginTypeUnknown:
|
||||||
for _, pluginType := range consts.PluginTypes {
|
for _, pluginType := range consts.PluginTypes {
|
||||||
pluginsRaw, ok := secret.Data[pluginType.String()]
|
pluginsRaw, ok := secret.Data[pluginType.String()]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -132,18 +93,36 @@ func (c *Sys) ListPluginsWithContext(ctx context.Context, i *ListPluginsInput) (
|
||||||
}
|
}
|
||||||
result.PluginsByType[pluginType] = plugins
|
result.PluginsByType[pluginType] = plugins
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
|
pluginsRaw, ok := secret.Data[i.Type.String()]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no %s entry in returned data", i.Type.String())
|
||||||
|
}
|
||||||
|
|
||||||
var respKeys []string
|
var respKeys []string
|
||||||
if err := mapstructure.Decode(secret.Data["keys"], &respKeys); err != nil {
|
if err := mapstructure.Decode(pluginsRaw, &respKeys); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result.PluginsByType[i.Type] = respKeys
|
result.PluginsByType[i.Type] = respKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
if detailed, ok := secret.Data["detailed"]; ok {
|
if detailed, ok := secret.Data["detailed"]; ok {
|
||||||
if err := mapstructure.Decode(detailed, &result.Details); err != nil {
|
var details []PluginDetails
|
||||||
|
if err := mapstructure.Decode(detailed, &details); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch i.Type {
|
||||||
|
case consts.PluginTypeUnknown:
|
||||||
|
result.Details = details
|
||||||
|
default:
|
||||||
|
// Filter for just the queried type.
|
||||||
|
for _, entry := range details {
|
||||||
|
if entry.Type == i.Type.String() {
|
||||||
|
result.Details = append(result.Details, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/strutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRegisterPlugin(t *testing.T) {
|
func TestRegisterPlugin(t *testing.T) {
|
||||||
|
@ -39,27 +40,78 @@ func TestListPlugins(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Sys().ListPluginsWithContext(context.Background(), &ListPluginsInput{})
|
for name, tc := range map[string]struct {
|
||||||
if err != nil {
|
input ListPluginsInput
|
||||||
t.Fatal(err)
|
expectedPlugins map[consts.PluginType][]string
|
||||||
}
|
}{
|
||||||
|
"no type specified": {
|
||||||
expectedPlugins := map[consts.PluginType][]string{
|
input: ListPluginsInput{},
|
||||||
consts.PluginTypeCredential: {"alicloud"},
|
expectedPlugins: map[consts.PluginType][]string{
|
||||||
consts.PluginTypeDatabase: {"cassandra-database-plugin"},
|
consts.PluginTypeCredential: {"alicloud"},
|
||||||
consts.PluginTypeSecrets: {"ad", "alicloud"},
|
consts.PluginTypeDatabase: {"cassandra-database-plugin"},
|
||||||
}
|
consts.PluginTypeSecrets: {"ad", "alicloud"},
|
||||||
|
},
|
||||||
for pluginType, expected := range expectedPlugins {
|
},
|
||||||
actualPlugins := resp.PluginsByType[pluginType]
|
"only auth plugins": {
|
||||||
if len(expected) != len(actualPlugins) {
|
input: ListPluginsInput{Type: consts.PluginTypeCredential},
|
||||||
t.Fatal("Wrong number of plugins", expected, actualPlugins)
|
expectedPlugins: map[consts.PluginType][]string{
|
||||||
}
|
consts.PluginTypeCredential: {"alicloud"},
|
||||||
for i := range actualPlugins {
|
},
|
||||||
if expected[i] != actualPlugins[i] {
|
},
|
||||||
t.Fatalf("Expected %q but got %q", expected[i], actualPlugins[i])
|
"only database plugins": {
|
||||||
|
input: ListPluginsInput{Type: consts.PluginTypeDatabase},
|
||||||
|
expectedPlugins: map[consts.PluginType][]string{
|
||||||
|
consts.PluginTypeDatabase: {"cassandra-database-plugin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"only secret plugins": {
|
||||||
|
input: ListPluginsInput{Type: consts.PluginTypeSecrets},
|
||||||
|
expectedPlugins: map[consts.PluginType][]string{
|
||||||
|
consts.PluginTypeSecrets: {"ad", "alicloud"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
resp, err := client.Sys().ListPluginsWithContext(context.Background(), &tc.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
for pluginType, expected := range tc.expectedPlugins {
|
||||||
|
actualPlugins := resp.PluginsByType[pluginType]
|
||||||
|
if len(expected) != len(actualPlugins) {
|
||||||
|
t.Fatal("Wrong number of plugins", expected, actualPlugins)
|
||||||
|
}
|
||||||
|
for i := range actualPlugins {
|
||||||
|
if expected[i] != actualPlugins[i] {
|
||||||
|
t.Fatalf("Expected %q but got %q", expected[i], actualPlugins[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedPlugin := range expected {
|
||||||
|
found := false
|
||||||
|
for _, plugin := range resp.Details {
|
||||||
|
if plugin.Type == pluginType.String() && plugin.Name == expectedPlugin {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Expected to find %s plugin %s but not found in details: %#v", pluginType.String(), expectedPlugin, resp.Details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, actual := range resp.Details {
|
||||||
|
pluginType, err := consts.ParsePluginType(actual.Type)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strutil.StrListContains(tc.expectedPlugins[pluginType], actual.Name) {
|
||||||
|
t.Errorf("Did not expect to find %s in details", actual.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +142,36 @@ const listUntypedResponse = `{
|
||||||
{
|
{
|
||||||
"arbitraryData": 7
|
"arbitraryData": 7
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"detailed": [
|
||||||
|
{
|
||||||
|
"type": "auth",
|
||||||
|
"name": "alicloud",
|
||||||
|
"version": "v0.13.0+builtin",
|
||||||
|
"builtin": true,
|
||||||
|
"deprecation_status": "supported"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "database",
|
||||||
|
"name": "cassandra-database-plugin",
|
||||||
|
"version": "v1.13.0+builtin.vault",
|
||||||
|
"builtin": true,
|
||||||
|
"deprecation_status": "supported"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "secret",
|
||||||
|
"name": "ad",
|
||||||
|
"version": "v0.14.0+builtin",
|
||||||
|
"builtin": true,
|
||||||
|
"deprecation_status": "supported"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "secret",
|
||||||
|
"name": "alicloud",
|
||||||
|
"version": "v0.13.0+builtin",
|
||||||
|
"builtin": true,
|
||||||
|
"deprecation_status": "supported"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"wrap_info": null,
|
"wrap_info": null,
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
```release-note:change
|
||||||
|
api: Exclusively use `GET /sys/plugins/catalog` endpoint for listing plugins, and add `details` field to list responses.
|
||||||
|
```
|
||||||
|
```release-note:improvement
|
||||||
|
cli: `vault plugin list` now has a `details` field in JSON format, and version and type information in table format.
|
||||||
|
```
|
|
@ -2,7 +2,6 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
|
@ -128,31 +127,34 @@ func (c *PluginListCommand) Run(args []string) int {
|
||||||
c.UI.Output(tableOutput(c.detailedResponse(resp), nil))
|
c.UI.Output(tableOutput(c.detailedResponse(resp), nil))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
c.UI.Output(tableOutput(c.simpleResponse(resp), nil))
|
c.UI.Output(tableOutput(c.simpleResponse(resp, pluginType), nil))
|
||||||
return 0
|
return 0
|
||||||
default:
|
default:
|
||||||
res := make(map[string]interface{})
|
res := make(map[string]interface{})
|
||||||
for k, v := range resp.PluginsByType {
|
for k, v := range resp.PluginsByType {
|
||||||
res[k.String()] = v
|
res[k.String()] = v
|
||||||
}
|
}
|
||||||
|
res["details"] = resp.Details
|
||||||
return OutputData(c.UI, res)
|
return OutputData(c.UI, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PluginListCommand) simpleResponse(plugins *api.ListPluginsResponse) []string {
|
func (c *PluginListCommand) simpleResponse(plugins *api.ListPluginsResponse, pluginType consts.PluginType) []string {
|
||||||
var flattenedNames []string
|
var out []string
|
||||||
namesAdded := make(map[string]bool)
|
switch pluginType {
|
||||||
for _, names := range plugins.PluginsByType {
|
case consts.PluginTypeUnknown:
|
||||||
for _, name := range names {
|
out = []string{"Name | Type | Version"}
|
||||||
if ok := namesAdded[name]; !ok {
|
for _, plugin := range plugins.Details {
|
||||||
flattenedNames = append(flattenedNames, name)
|
out = append(out, fmt.Sprintf("%s | %s | %s", plugin.Name, plugin.Type, plugin.Version))
|
||||||
namesAdded[name] = true
|
}
|
||||||
}
|
default:
|
||||||
|
out = []string{"Name | Version"}
|
||||||
|
for _, plugin := range plugins.Details {
|
||||||
|
out = append(out, fmt.Sprintf("%s | %s", plugin.Name, plugin.Version))
|
||||||
}
|
}
|
||||||
sort.Strings(flattenedNames)
|
|
||||||
}
|
}
|
||||||
list := append([]string{"Plugins"}, flattenedNames...)
|
|
||||||
return list
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PluginListCommand) detailedResponse(plugins *api.ListPluginsResponse) []string {
|
func (c *PluginListCommand) detailedResponse(plugins *api.ListPluginsResponse) []string {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -36,7 +37,7 @@ func TestPluginListCommand_Run(t *testing.T) {
|
||||||
{
|
{
|
||||||
"lists",
|
"lists",
|
||||||
nil,
|
nil,
|
||||||
"Plugins",
|
"Name\\s+Type\\s+Version",
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -62,7 +63,8 @@ func TestPluginListCommand_Run(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
|
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||||
if !strings.Contains(combined, tc.out) {
|
matcher := regexp.MustCompile(tc.out)
|
||||||
|
if !matcher.MatchString(combined) {
|
||||||
t.Errorf("expected %q to contain %q", combined, tc.out)
|
t.Errorf("expected %q to contain %q", combined, tc.out)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -426,7 +426,7 @@ func (b *SystemBackend) handlePluginCatalogUntypedList(ctx context.Context, _ *l
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort for consistent ordering
|
// Sort for consistent ordering
|
||||||
sortVersionedPlugins(versionedPlugins)
|
sortVersionedPlugins(versioned)
|
||||||
|
|
||||||
versionedPlugins = append(versionedPlugins, versioned...)
|
versionedPlugins = append(versionedPlugins, versioned...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,16 +21,15 @@ List all available plugins in the catalog.
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ vault plugin list
|
$ vault plugin list
|
||||||
|
Name Type Version
|
||||||
Plugins
|
---- ---- -------
|
||||||
-------
|
alicloud auth v0.13.0+builtin
|
||||||
my-custom-plugin
|
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
$ vault plugin list database
|
$ vault plugin list database
|
||||||
Plugins
|
Name Version
|
||||||
-------
|
---- -------
|
||||||
cassandra-database-plugin
|
cassandra-database-plugin v1.13.0+builtin.vault
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -44,6 +43,7 @@ alicloud auth v0.12.0+builtin
|
||||||
app-id auth v1.12.0+builtin.vault pending removal
|
app-id auth v1.12.0+builtin.vault pending removal
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
The following flags are available in addition to the [standard set of
|
The following flags are available in addition to the [standard set of
|
||||||
|
|
Loading…
Reference in New Issue