Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New database plugin API to reload by plugin name #24472

Merged
merged 5 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading