mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
New database plugin API to reload by plugin name (#24472)
This commit is contained in:
@@ -107,6 +107,7 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
|
|||||||
pathListPluginConnection(&b),
|
pathListPluginConnection(&b),
|
||||||
pathConfigurePluginConnection(&b),
|
pathConfigurePluginConnection(&b),
|
||||||
pathResetConnection(&b),
|
pathResetConnection(&b),
|
||||||
|
pathReloadPlugin(&b),
|
||||||
},
|
},
|
||||||
pathListRoles(&b),
|
pathListRoles(&b),
|
||||||
pathRoles(&b),
|
pathRoles(&b),
|
||||||
|
|||||||
@@ -626,6 +626,23 @@ func TestBackend_connectionCrud(t *testing.T) {
|
|||||||
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure a second connection to confirm below it doesn't get restarted.
|
||||||
|
data = map[string]interface{}{
|
||||||
|
"connection_url": "test",
|
||||||
|
"plugin_name": "hana-database-plugin",
|
||||||
|
"verify_connection": false,
|
||||||
|
}
|
||||||
|
req = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/plugin-test-hana",
|
||||||
|
Storage: config.StorageView,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
// Create a role
|
// Create a role
|
||||||
data = map[string]interface{}{
|
data = map[string]interface{}{
|
||||||
"db_name": "plugin-test",
|
"db_name": "plugin-test",
|
||||||
@@ -717,18 +734,50 @@ func TestBackend_connectionCrud(t *testing.T) {
|
|||||||
t.Fatal(diff)
|
t.Fatal(diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Connection
|
// Test endpoints for reloading plugins.
|
||||||
data = map[string]interface{}{}
|
for _, reloadPath := range []string{
|
||||||
|
"reset/plugin-test",
|
||||||
|
"reload/postgresql-database-plugin",
|
||||||
|
} {
|
||||||
|
getConnectionID := func(name string) string {
|
||||||
|
t.Helper()
|
||||||
|
dbBackend, ok := b.(*databaseBackend)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("could not convert logical.Backend to databaseBackend")
|
||||||
|
}
|
||||||
|
dbi := dbBackend.connections.Get(name)
|
||||||
|
if dbi == nil {
|
||||||
|
t.Fatal("no plugin-test dbi")
|
||||||
|
}
|
||||||
|
return dbi.ID()
|
||||||
|
}
|
||||||
|
initialID := getConnectionID("plugin-test")
|
||||||
|
hanaID := getConnectionID("plugin-test-hana")
|
||||||
req = &logical.Request{
|
req = &logical.Request{
|
||||||
Operation: logical.UpdateOperation,
|
Operation: logical.UpdateOperation,
|
||||||
Path: "reset/plugin-test",
|
Path: reloadPath,
|
||||||
Storage: config.StorageView,
|
Storage: config.StorageView,
|
||||||
Data: data,
|
Data: map[string]interface{}{},
|
||||||
}
|
}
|
||||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
||||||
}
|
}
|
||||||
|
if initialID == getConnectionID("plugin-test") {
|
||||||
|
t.Fatal("ID unchanged after connection reset")
|
||||||
|
}
|
||||||
|
if hanaID != getConnectionID("plugin-test-hana") {
|
||||||
|
t.Fatal("hana plugin got restarted but shouldn't have been")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(reloadPath, "reload/") {
|
||||||
|
if expected := 1; expected != resp.Data["count"] {
|
||||||
|
t.Fatalf("expected %d but got %d", expected, resp.Data["count"])
|
||||||
|
}
|
||||||
|
if expected := []string{"plugin-test"}; !reflect.DeepEqual(expected, resp.Data["connections"]) {
|
||||||
|
t.Fatalf("expected %v but got %v", expected, resp.Data["connections"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get creds
|
// Get creds
|
||||||
data = map[string]interface{}{}
|
data = map[string]interface{}{}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/structs"
|
"github.com/fatih/structs"
|
||||||
"github.com/hashicorp/go-uuid"
|
"github.com/hashicorp/go-uuid"
|
||||||
@@ -94,13 +95,7 @@ func (b *databaseBackend) pathConnectionReset() framework.OperationFunc {
|
|||||||
return logical.ErrorResponse(respErrEmptyName), nil
|
return logical.ErrorResponse(respErrEmptyName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close plugin and delete the entry in the connections cache.
|
if err := b.reloadConnection(ctx, req.Storage, name); err != nil {
|
||||||
if err := b.ClearConnection(name); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute plugin again, we don't need the object so throw away.
|
|
||||||
if _, err := b.GetConnection(ctx, req.Storage, name); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +103,103 @@ func (b *databaseBackend) pathConnectionReset() framework.OperationFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) reloadConnection(ctx context.Context, storage logical.Storage, name string) error {
|
||||||
|
// Close plugin and delete the entry in the connections cache.
|
||||||
|
if err := b.ClearConnection(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute plugin again, we don't need the object so throw away.
|
||||||
|
if _, err := b.GetConnection(ctx, storage, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathReloadPlugin reloads all connections using a named plugin.
|
||||||
|
func pathReloadPlugin(b *databaseBackend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: fmt.Sprintf("reload/%s", framework.GenericNameRegex("plugin_name")),
|
||||||
|
|
||||||
|
DisplayAttrs: &framework.DisplayAttributes{
|
||||||
|
OperationPrefix: operationPrefixDatabase,
|
||||||
|
OperationVerb: "reload",
|
||||||
|
OperationSuffix: "plugin",
|
||||||
|
},
|
||||||
|
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"plugin_name": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Name of the database plugin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.UpdateOperation: b.reloadPlugin(),
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathReloadPluginHelpSyn,
|
||||||
|
HelpDescription: pathReloadPluginHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reloadPlugin reloads all instances of a named plugin by closing the existing
|
||||||
|
// instances and creating new ones.
|
||||||
|
func (b *databaseBackend) reloadPlugin() framework.OperationFunc {
|
||||||
|
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
pluginName := data.Get("plugin_name").(string)
|
||||||
|
if pluginName == "" {
|
||||||
|
return logical.ErrorResponse(respErrEmptyPluginName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
connNames, err := req.Storage.List(ctx, "config/")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reloaded := []string{}
|
||||||
|
for _, connName := range connNames {
|
||||||
|
entry, err := req.Storage.Get(ctx, fmt.Sprintf("config/%s", connName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read connection configuration: %w", err)
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var config DatabaseConfig
|
||||||
|
if err := entry.DecodeJSON(&config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if config.PluginName == pluginName {
|
||||||
|
if err := b.reloadConnection(ctx, req.Storage, connName); err != nil {
|
||||||
|
var successfullyReloaded string
|
||||||
|
if len(reloaded) > 0 {
|
||||||
|
successfullyReloaded = fmt.Sprintf("successfully reloaded %d connection(s): %s; ",
|
||||||
|
len(reloaded),
|
||||||
|
strings.Join(reloaded, ", "))
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%sfailed to reload connection %q: %w", successfullyReloaded, connName, err)
|
||||||
|
}
|
||||||
|
reloaded = append(reloaded, connName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"connections": reloaded,
|
||||||
|
"count": len(reloaded),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reloaded) == 0 {
|
||||||
|
resp.AddWarning(fmt.Sprintf("no connections were found with plugin_name %q", pluginName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// pathConfigurePluginConnection returns a configured framework.Path setup to
|
// pathConfigurePluginConnection returns a configured framework.Path setup to
|
||||||
// operate on plugins.
|
// operate on plugins.
|
||||||
func pathConfigurePluginConnection(b *databaseBackend) *framework.Path {
|
func pathConfigurePluginConnection(b *databaseBackend) *framework.Path {
|
||||||
@@ -551,3 +643,12 @@ const pathResetConnectionHelpDesc = `
|
|||||||
This path resets the database connection by closing the existing database plugin
|
This path resets the database connection by closing the existing database plugin
|
||||||
instance and running a new one.
|
instance and running a new one.
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const pathReloadPluginHelpSyn = `
|
||||||
|
Reloads all connections using a named database plugin.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathReloadPluginHelpDesc = `
|
||||||
|
This path resets each database connection using a named plugin by closing each
|
||||||
|
existing database plugin instance and running a new one.
|
||||||
|
`
|
||||||
|
|||||||
3
changelog/24472.txt
Normal file
3
changelog/24472.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
secrets/database: Add new reload/:plugin_name API to reload database plugins by name for a specific mount.
|
||||||
|
```
|
||||||
@@ -250,6 +250,42 @@ $ curl \
|
|||||||
http://127.0.0.1:8200/v1/database/reset/mysql
|
http://127.0.0.1:8200/v1/database/reset/mysql
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Reload plugin
|
||||||
|
|
||||||
|
This endpoint performs the same operation as
|
||||||
|
[reset connection](/vault/api-docs/secret/databases#reset-connection) but for
|
||||||
|
all connections that reference a specific plugin name. This can be useful to
|
||||||
|
restart a specific plugin after it's been upgraded in the plugin catalog.
|
||||||
|
|
||||||
|
| Method | Path |
|
||||||
|
| :----- | :------------------------------ |
|
||||||
|
| `POST` | `/database/reload/:plugin_name` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `plugin_name` `(string: <required>)` – Specifies the name of the plugin for
|
||||||
|
which all connections should be reset. This is specified as part of the URL.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
$ curl \
|
||||||
|
--header "X-Vault-Token: ..." \
|
||||||
|
--request POST \
|
||||||
|
http://127.0.0.1:8200/v1/database/reload/postgresql-database-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"connections": ["pg1", "pg2"],
|
||||||
|
"count": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Rotate root credentials
|
## Rotate root credentials
|
||||||
|
|
||||||
This endpoint is used to rotate the "root" user credentials stored for
|
This endpoint is used to rotate the "root" user credentials stored for
|
||||||
|
|||||||
Reference in New Issue
Block a user