From 34beea85fbe1fa90cc49d81e1ba8a95c3700b288 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 27 Mar 2018 01:40:33 +0800 Subject: [PATCH] Add API functions and completions for plugins (#4194) --- api/sys_plugins.go | 117 +++++++++++++++++++++++++++++++++++ command/auth_enable.go | 2 +- command/base_predict.go | 28 +++++++++ command/base_predict_test.go | 54 ++++++++++++++++ command/secrets_enable.go | 2 +- 5 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 api/sys_plugins.go diff --git a/api/sys_plugins.go b/api/sys_plugins.go new file mode 100644 index 0000000000..8183b10f5b --- /dev/null +++ b/api/sys_plugins.go @@ -0,0 +1,117 @@ +package api + +import ( + "fmt" + "net/http" +) + +// ListPluginsInput is used as input to the ListPlugins function. +type ListPluginsInput struct{} + +// ListPluginsResponse is the response from the ListPlugins call. +type ListPluginsResponse struct { + // Names is the list of names of the plugins. + Names []string +} + +// ListPlugins lists all plugins in the catalog and returns their names as a +// list of strings. +func (c *Sys) ListPlugins(i *ListPluginsInput) (*ListPluginsResponse, error) { + path := "/v1/sys/plugins/catalog" + req := c.c.NewRequest("LIST", path) + resp, err := c.c.RawRequest(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 +} + +// GetPluginInput is used as input to the GetPlugin function. +type GetPluginInput struct { + Name string `json:"-"` +} + +// GetPluginResponse is the response from the GetPlugin call. +type GetPluginResponse struct { + Args []string `json:"args"` + Builtin bool `json:"builtin"` + Command string `json:"command"` + Name string `json:"name"` + SHA256 string `json:"sha256"` +} + +func (c *Sys) GetPlugin(i *GetPluginInput) (*GetPluginResponse, error) { + path := fmt.Sprintf("/v1/sys/plugins/catalog/%s", i.Name) + req := c.c.NewRequest(http.MethodGet, path) + resp, err := c.c.RawRequest(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result GetPluginResponse + err = resp.DecodeJSON(&result) + if err != nil { + return nil, err + } + return &result, err +} + +// RegisterPluginInput is used as input to the RegisterPlugin function. +type RegisterPluginInput struct { + // Name is the name of the plugin. Required. + Name string `json:"-"` + + // Args is the list of args to spawn the process with. + Args []string `json:"args,omitempty"` + + // Command is the command to run. + Command string `json:"command,omitempty"` + + // SHA256 is the shasum of the plugin. + SHA256 string `json:"sha256,omitempty"` +} + +// RegisterPlugin registers the plugin with the given information. +func (c *Sys) RegisterPlugin(i *RegisterPluginInput) error { + path := fmt.Sprintf("/v1/sys/plugins/catalog/%s", i.Name) + req := c.c.NewRequest(http.MethodPut, path) + if err := req.SetJSONBody(i); err != nil { + return err + } + + resp, err := c.c.RawRequest(req) + if err == nil { + defer resp.Body.Close() + } + return err +} + +// DeregisterPluginInput is used as input to the DeregisterPlugin function. +type DeregisterPluginInput struct { + // Name is the name of the plugin. Required. + Name string `json:"-"` +} + +// DeregisterPlugin removes the plugin with the given name from the plugin +// catalog. +func (c *Sys) DeregisterPlugin(i *DeregisterPluginInput) error { + path := fmt.Sprintf("/v1/sys/plugins/catalog/%s", i.Name) + req := c.c.NewRequest(http.MethodDelete, path) + resp, err := c.c.RawRequest(req) + if err == nil { + defer resp.Body.Close() + } + return err +} diff --git a/command/auth_enable.go b/command/auth_enable.go index 0f556275ba..345da2116b 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -123,7 +123,7 @@ func (c *AuthEnableCommand) Flags() *FlagSets { f.StringVar(&StringVar{ Name: "plugin-name", Target: &c.flagPluginName, - Completion: complete.PredictAnything, + Completion: c.PredictVaultPlugins(), Usage: "Name of the auth method plugin. This plugin name must already " + "exist in the Vault server's plugin catalog.", }) diff --git a/command/base_predict.go b/command/base_predict.go index f71c255c8e..4897ad6c05 100644 --- a/command/base_predict.go +++ b/command/base_predict.go @@ -139,6 +139,11 @@ func (b *BaseCommand) PredictVaultAuths() complete.Predictor { return NewPredict().VaultAuths() } +// PredictVaultPlugins returns a predictor for installed plugins. +func (b *BaseCommand) PredictVaultPlugins() complete.Predictor { + return NewPredict().VaultPlugins() +} + // PredictVaultPolicies returns a predictor for "folders". See PredictVaultFiles // for more information and restrictions. func (b *BaseCommand) PredictVaultPolicies() complete.Predictor { @@ -177,6 +182,13 @@ func (p *Predict) VaultAuths() complete.Predictor { return p.filterFunc(p.auths) } +// VaultPlugins returns a predictor for Vault's plugin catalog. This is a public +// API for consumers, but you probably want BaseCommand.PredictVaultPlugins +// instead. +func (p *Predict) VaultPlugins() complete.Predictor { + return p.filterFunc(p.plugins) +} + // VaultPolicies returns a predictor for Vault "folders". This is a public API for // consumers, but you probably want BaseCommand.PredictVaultPolicies instead. func (p *Predict) VaultPolicies() complete.Predictor { @@ -310,6 +322,22 @@ func (p *Predict) auths() []string { return list } +// plugins returns a sorted list of the plugins in the catalog. +func (p *Predict) plugins() []string { + client := p.Client() + if client == nil { + return nil + } + + result, err := client.Sys().ListPlugins(nil) + if err != nil { + return nil + } + plugins := result.Names + sort.Strings(plugins) + return plugins +} + // policies returns a sorted list of the policies stored in this Vault // server. func (p *Predict) policies() []string { diff --git a/command/base_predict_test.go b/command/base_predict_test.go index 93e291e502..c9ad1891b2 100644 --- a/command/base_predict_test.go +++ b/command/base_predict_test.go @@ -299,6 +299,60 @@ func TestPredict_Mounts(t *testing.T) { }) } +func TestPredict_Plugins(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + badClient, badCloser := testVaultServerBad(t) + defer badCloser() + + cases := []struct { + name string + client *api.Client + exp []string + }{ + { + "not_connected_client", + badClient, + nil, + }, + { + "good_path", + client, + []string{ + "cassandra-database-plugin", + "hana-database-plugin", + "mongodb-database-plugin", + "mssql-database-plugin", + "mysql-aurora-database-plugin", + "mysql-database-plugin", + "mysql-legacy-database-plugin", + "mysql-rds-database-plugin", + "postgresql-database-plugin", + }, + }, + } + + t.Run("group", func(t *testing.T) { + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + p := NewPredict() + p.client = tc.client + + act := p.plugins() + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + }) +} + func TestPredict_Policies(t *testing.T) { t.Parallel() diff --git a/command/secrets_enable.go b/command/secrets_enable.go index ab0ba8dea7..f5e2068783 100644 --- a/command/secrets_enable.go +++ b/command/secrets_enable.go @@ -140,7 +140,7 @@ func (c *SecretsEnableCommand) Flags() *FlagSets { f.StringVar(&StringVar{ Name: "plugin-name", Target: &c.flagPluginName, - Completion: complete.PredictAnything, + Completion: c.PredictVaultPlugins(), Usage: "Name of the secrets engine plugin. This plugin name must already " + "exist in Vault's plugin catalog.", })