Skip to content

Commit dc5c3e8

Browse files
authoredDec 13, 2023
New database plugin API to reload by plugin name (#24472)
1 parent 486df81 commit dc5c3e8

File tree

5 files changed

+206
-16
lines changed

5 files changed

+206
-16
lines changed
 

‎builtin/logical/database/backend.go

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func Backend(conf *logical.BackendConfig) *databaseBackend {
107107
pathListPluginConnection(&b),
108108
pathConfigurePluginConnection(&b),
109109
pathResetConnection(&b),
110+
pathReloadPlugin(&b),
110111
},
111112
pathListRoles(&b),
112113
pathRoles(&b),

‎builtin/logical/database/backend_test.go

+60-11
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,23 @@ func TestBackend_connectionCrud(t *testing.T) {
626626
t.Fatalf("err:%s resp:%#v\n", err, resp)
627627
}
628628

629+
// Configure a second connection to confirm below it doesn't get restarted.
630+
data = map[string]interface{}{
631+
"connection_url": "test",
632+
"plugin_name": "hana-database-plugin",
633+
"verify_connection": false,
634+
}
635+
req = &logical.Request{
636+
Operation: logical.UpdateOperation,
637+
Path: "config/plugin-test-hana",
638+
Storage: config.StorageView,
639+
Data: data,
640+
}
641+
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
642+
if err != nil || (resp != nil && resp.IsError()) {
643+
t.Fatalf("err:%s resp:%#v\n", err, resp)
644+
}
645+
629646
// Create a role
630647
data = map[string]interface{}{
631648
"db_name": "plugin-test",
@@ -717,17 +734,49 @@ func TestBackend_connectionCrud(t *testing.T) {
717734
t.Fatal(diff)
718735
}
719736

720-
// Reset Connection
721-
data = map[string]interface{}{}
722-
req = &logical.Request{
723-
Operation: logical.UpdateOperation,
724-
Path: "reset/plugin-test",
725-
Storage: config.StorageView,
726-
Data: data,
727-
}
728-
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
729-
if err != nil || (resp != nil && resp.IsError()) {
730-
t.Fatalf("err:%s resp:%#v\n", err, resp)
737+
// Test endpoints for reloading plugins.
738+
for _, reloadPath := range []string{
739+
"reset/plugin-test",
740+
"reload/postgresql-database-plugin",
741+
} {
742+
getConnectionID := func(name string) string {
743+
t.Helper()
744+
dbBackend, ok := b.(*databaseBackend)
745+
if !ok {
746+
t.Fatal("could not convert logical.Backend to databaseBackend")
747+
}
748+
dbi := dbBackend.connections.Get(name)
749+
if dbi == nil {
750+
t.Fatal("no plugin-test dbi")
751+
}
752+
return dbi.ID()
753+
}
754+
initialID := getConnectionID("plugin-test")
755+
hanaID := getConnectionID("plugin-test-hana")
756+
req = &logical.Request{
757+
Operation: logical.UpdateOperation,
758+
Path: reloadPath,
759+
Storage: config.StorageView,
760+
Data: map[string]interface{}{},
761+
}
762+
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
763+
if err != nil || (resp != nil && resp.IsError()) {
764+
t.Fatalf("err:%s resp:%#v\n", err, resp)
765+
}
766+
if initialID == getConnectionID("plugin-test") {
767+
t.Fatal("ID unchanged after connection reset")
768+
}
769+
if hanaID != getConnectionID("plugin-test-hana") {
770+
t.Fatal("hana plugin got restarted but shouldn't have been")
771+
}
772+
if strings.HasPrefix(reloadPath, "reload/") {
773+
if expected := 1; expected != resp.Data["count"] {
774+
t.Fatalf("expected %d but got %d", expected, resp.Data["count"])
775+
}
776+
if expected := []string{"plugin-test"}; !reflect.DeepEqual(expected, resp.Data["connections"]) {
777+
t.Fatalf("expected %v but got %v", expected, resp.Data["connections"])
778+
}
779+
}
731780
}
732781

733782
// Get creds

‎builtin/logical/database/path_config_connection.go

+106-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"net/url"
1111
"sort"
12+
"strings"
1213

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

97-
// Close plugin and delete the entry in the connections cache.
98-
if err := b.ClearConnection(name); err != nil {
98+
if err := b.reloadConnection(ctx, req.Storage, name); err != nil {
9999
return nil, err
100100
}
101101

102-
// Execute plugin again, we don't need the object so throw away.
103-
if _, err := b.GetConnection(ctx, req.Storage, name); err != nil {
102+
return nil, nil
103+
}
104+
}
105+
106+
func (b *databaseBackend) reloadConnection(ctx context.Context, storage logical.Storage, name string) error {
107+
// Close plugin and delete the entry in the connections cache.
108+
if err := b.ClearConnection(name); err != nil {
109+
return err
110+
}
111+
112+
// Execute plugin again, we don't need the object so throw away.
113+
if _, err := b.GetConnection(ctx, storage, name); err != nil {
114+
return err
115+
}
116+
117+
return nil
118+
}
119+
120+
// pathReloadPlugin reloads all connections using a named plugin.
121+
func pathReloadPlugin(b *databaseBackend) *framework.Path {
122+
return &framework.Path{
123+
Pattern: fmt.Sprintf("reload/%s", framework.GenericNameRegex("plugin_name")),
124+
125+
DisplayAttrs: &framework.DisplayAttributes{
126+
OperationPrefix: operationPrefixDatabase,
127+
OperationVerb: "reload",
128+
OperationSuffix: "plugin",
129+
},
130+
131+
Fields: map[string]*framework.FieldSchema{
132+
"plugin_name": {
133+
Type: framework.TypeString,
134+
Description: "Name of the database plugin",
135+
},
136+
},
137+
138+
Callbacks: map[logical.Operation]framework.OperationFunc{
139+
logical.UpdateOperation: b.reloadPlugin(),
140+
},
141+
142+
HelpSynopsis: pathReloadPluginHelpSyn,
143+
HelpDescription: pathReloadPluginHelpDesc,
144+
}
145+
}
146+
147+
// reloadPlugin reloads all instances of a named plugin by closing the existing
148+
// instances and creating new ones.
149+
func (b *databaseBackend) reloadPlugin() framework.OperationFunc {
150+
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
151+
pluginName := data.Get("plugin_name").(string)
152+
if pluginName == "" {
153+
return logical.ErrorResponse(respErrEmptyPluginName), nil
154+
}
155+
156+
connNames, err := req.Storage.List(ctx, "config/")
157+
if err != nil {
104158
return nil, err
105159
}
160+
reloaded := []string{}
161+
for _, connName := range connNames {
162+
entry, err := req.Storage.Get(ctx, fmt.Sprintf("config/%s", connName))
163+
if err != nil {
164+
return nil, fmt.Errorf("failed to read connection configuration: %w", err)
165+
}
166+
if entry == nil {
167+
continue
168+
}
106169

107-
return nil, nil
170+
var config DatabaseConfig
171+
if err := entry.DecodeJSON(&config); err != nil {
172+
return nil, err
173+
}
174+
if config.PluginName == pluginName {
175+
if err := b.reloadConnection(ctx, req.Storage, connName); err != nil {
176+
var successfullyReloaded string
177+
if len(reloaded) > 0 {
178+
successfullyReloaded = fmt.Sprintf("successfully reloaded %d connection(s): %s; ",
179+
len(reloaded),
180+
strings.Join(reloaded, ", "))
181+
}
182+
return nil, fmt.Errorf("%sfailed to reload connection %q: %w", successfullyReloaded, connName, err)
183+
}
184+
reloaded = append(reloaded, connName)
185+
}
186+
}
187+
188+
resp := &logical.Response{
189+
Data: map[string]interface{}{
190+
"connections": reloaded,
191+
"count": len(reloaded),
192+
},
193+
}
194+
195+
if len(reloaded) == 0 {
196+
resp.AddWarning(fmt.Sprintf("no connections were found with plugin_name %q", pluginName))
197+
}
198+
199+
return resp, nil
108200
}
109201
}
110202

@@ -551,3 +643,12 @@ const pathResetConnectionHelpDesc = `
551643
This path resets the database connection by closing the existing database plugin
552644
instance and running a new one.
553645
`
646+
647+
const pathReloadPluginHelpSyn = `
648+
Reloads all connections using a named database plugin.
649+
`
650+
651+
const pathReloadPluginHelpDesc = `
652+
This path resets each database connection using a named plugin by closing each
653+
existing database plugin instance and running a new one.
654+
`

‎changelog/24472.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
secrets/database: Add new reload/:plugin_name API to reload database plugins by name for a specific mount.
3+
```

‎website/content/api-docs/secret/databases/index.mdx

+36
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,42 @@ $ curl \
250250
http://127.0.0.1:8200/v1/database/reset/mysql
251251
```
252252

253+
## Reload plugin
254+
255+
This endpoint performs the same operation as
256+
[reset connection](/vault/api-docs/secret/databases#reset-connection) but for
257+
all connections that reference a specific plugin name. This can be useful to
258+
restart a specific plugin after it's been upgraded in the plugin catalog.
259+
260+
| Method | Path |
261+
| :----- | :------------------------------ |
262+
| `POST` | `/database/reload/:plugin_name` |
263+
264+
### Parameters
265+
266+
- `plugin_name` `(string: <required>)` – Specifies the name of the plugin for
267+
which all connections should be reset. This is specified as part of the URL.
268+
269+
### Sample request
270+
271+
```shell-session
272+
$ curl \
273+
--header "X-Vault-Token: ..." \
274+
--request POST \
275+
http://127.0.0.1:8200/v1/database/reload/postgresql-database-plugin
276+
```
277+
278+
### Sample response
279+
280+
```json
281+
{
282+
"data": {
283+
"connections": ["pg1", "pg2"],
284+
"count": 2
285+
}
286+
}
287+
```
288+
253289
## Rotate root credentials
254290

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

0 commit comments

Comments
 (0)
Please sign in to comment.