Skip to content

Commit

Permalink
New database plugin API to reload by plugin name (#24472)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomhjp authored Dec 13, 2023
1 parent 486df81 commit dc5c3e8
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 16 deletions.
1 change: 1 addition & 0 deletions builtin/logical/database/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
pathListPluginConnection(&b),
pathConfigurePluginConnection(&b),
pathResetConnection(&b),
pathReloadPlugin(&b),
},
pathListRoles(&b),
pathRoles(&b),
Expand Down
71 changes: 60 additions & 11 deletions builtin/logical/database/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,23 @@ func TestBackend_connectionCrud(t *testing.T) {
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
data = map[string]interface{}{
"db_name": "plugin-test",
Expand Down Expand Up @@ -717,17 +734,49 @@ func TestBackend_connectionCrud(t *testing.T) {
t.Fatal(diff)
}

// Reset Connection
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "reset/plugin-test",
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)
// Test endpoints for reloading plugins.
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{
Operation: logical.UpdateOperation,
Path: reloadPath,
Storage: config.StorageView,
Data: map[string]interface{}{},
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
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
Expand Down
111 changes: 106 additions & 5 deletions builtin/logical/database/path_config_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"net/url"
"sort"
"strings"

"github.com/fatih/structs"
"github.com/hashicorp/go-uuid"
Expand Down Expand Up @@ -94,17 +95,108 @@ func (b *databaseBackend) pathConnectionReset() framework.OperationFunc {
return logical.ErrorResponse(respErrEmptyName), nil
}

// Close plugin and delete the entry in the connections cache.
if err := b.ClearConnection(name); err != nil {
if err := b.reloadConnection(ctx, req.Storage, 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, nil
}
}

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
}

return nil, nil
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
}
}

Expand Down Expand Up @@ -551,3 +643,12 @@ const pathResetConnectionHelpDesc = `
This path resets the database connection by closing the existing database plugin
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 changes: 3 additions & 0 deletions changelog/24472.txt
Original file line number Diff line number Diff line change
@@ -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.
```
36 changes: 36 additions & 0 deletions website/content/api-docs/secret/databases/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,42 @@ $ curl \
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

This endpoint is used to rotate the "root" user credentials stored for
Expand Down

0 comments on commit dc5c3e8

Please sign in to comment.