mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +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,17 +734,49 @@ 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{ | ||||||
| 	req = &logical.Request{ | 		"reset/plugin-test", | ||||||
| 		Operation: logical.UpdateOperation, | 		"reload/postgresql-database-plugin", | ||||||
| 		Path:      "reset/plugin-test", | 	} { | ||||||
| 		Storage:   config.StorageView, | 		getConnectionID := func(name string) string { | ||||||
| 		Data:      data, | 			t.Helper() | ||||||
| 	} | 			dbBackend, ok := b.(*databaseBackend) | ||||||
| 	resp, err = b.HandleRequest(namespace.RootContext(nil), req) | 			if !ok { | ||||||
| 	if err != nil || (resp != nil && resp.IsError()) { | 				t.Fatal("could not convert logical.Backend to databaseBackend") | ||||||
| 		t.Fatalf("err:%s resp:%#v\n", err, resp) | 			} | ||||||
|  | 			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 | 	// Get creds | ||||||
|   | |||||||
| @@ -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
	 Tom Proctor
					Tom Proctor