From d794d8ce4e62c244231a2d0114a992a5f21385f5 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Tue, 16 Jan 2024 14:35:18 +0000 Subject: [PATCH 1/2] New root namespace plugin reload API sys/plugins/reload/:type/:name --- api/plugin_types.go | 36 ++++- api/plugin_types_test.go | 101 +++++++++++++ api/sys_plugins.go | 29 +++- changelog/24878.txt | 6 + command/plugin_register_test.go | 6 +- command/plugin_reload.go | 71 +++++++-- command/plugin_reload_test.go | 12 ++ sdk/helper/consts/plugin_types.go | 36 ++++- sdk/helper/consts/plugin_types_test.go | 101 +++++++++++++ sdk/plugin/mock/backend.go | 48 ++++++- vault/external_tests/plugin/plugin_test.go | 75 ++++++---- vault/logical_system.go | 112 ++++++++++++++- vault/logical_system_helpers.go | 13 +- vault/logical_system_paths.go | 60 ++++++++ vault/plugin_reload.go | 135 ++++++++++-------- .../system/plugins-reload-backend.mdx | 50 ------- .../api-docs/system/plugins-reload.mdx | 121 ++++++++++++++++ website/content/docs/upgrading/plugins.mdx | 2 +- website/data/api-docs-nav-data.json | 4 +- website/redirects.js | 5 + 20 files changed, 851 insertions(+), 172 deletions(-) create mode 100644 api/plugin_types_test.go create mode 100644 changelog/24878.txt create mode 100644 sdk/helper/consts/plugin_types_test.go delete mode 100644 website/content/api-docs/system/plugins-reload-backend.mdx create mode 100644 website/content/api-docs/system/plugins-reload.mdx diff --git a/api/plugin_types.go b/api/plugin_types.go index 4c759a2decc5..c8f69ae404f2 100644 --- a/api/plugin_types.go +++ b/api/plugin_types.go @@ -7,7 +7,10 @@ package api // https://github.com/hashicorp/vault/blob/main/sdk/helper/consts/plugin_types.go // Any changes made should be made to both files at the same time. -import "fmt" +import ( + "encoding/json" + "fmt" +) var PluginTypes = []PluginType{ PluginTypeUnknown, @@ -64,3 +67,34 @@ func ParsePluginType(pluginType string) (PluginType, error) { return PluginTypeUnknown, fmt.Errorf("%q is not a supported plugin type", pluginType) } } + +// UnmarshalJSON implements json.Unmarshaler. It supports unmarshaling either a +// string or a uint32. All new serialization will be as a string, but we +// previously serialized as a uint32 so we need to support that for backwards +// compatibility. +func (p *PluginType) UnmarshalJSON(data []byte) error { + var asString string + err := json.Unmarshal(data, &asString) + if err == nil { + *p, err = ParsePluginType(asString) + return err + } + + var asUint32 uint32 + err = json.Unmarshal(data, &asUint32) + if err != nil { + return err + } + *p = PluginType(asUint32) + switch *p { + case PluginTypeUnknown, PluginTypeCredential, PluginTypeDatabase, PluginTypeSecrets: + return nil + default: + return fmt.Errorf("%d is not a supported plugin type", asUint32) + } +} + +// MarshalJSON implements json.Marshaler. +func (p PluginType) MarshalJSON() ([]byte, error) { + return json.Marshal(p.String()) +} diff --git a/api/plugin_types_test.go b/api/plugin_types_test.go new file mode 100644 index 000000000000..0b6085379b43 --- /dev/null +++ b/api/plugin_types_test.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package api + +// NOTE: this file was copied from +// https://github.com/hashicorp/vault/blob/main/sdk/helper/consts/plugin_types_test.go +// Any changes made should be made to both files at the same time. + +import ( + "encoding/json" + "testing" +) + +type testType struct { + PluginType PluginType `json:"plugin_type"` +} + +func TestPluginTypeJSONRoundTrip(t *testing.T) { + for _, pluginType := range PluginTypes { + original := testType{ + PluginType: pluginType, + } + asBytes, err := json.Marshal(original) + if err != nil { + t.Fatal(err) + } + + var roundTripped testType + err = json.Unmarshal(asBytes, &roundTripped) + if err != nil { + t.Fatal(err) + } + + if original != roundTripped { + t.Fatalf("expected %v, got %v", original, roundTripped) + } + } +} + +func TestPluginTypeJSONUnmarshal(t *testing.T) { + // Failure/unsupported cases. + for name, tc := range map[string]string{ + "unsupported": `{"plugin_type":"unsupported"}`, + "random string": `{"plugin_type":"foo"}`, + "boolean": `{"plugin_type":true}`, + "empty": `{"plugin_type":""}`, + "negative": `{"plugin_type":-1}`, + "out of range": `{"plugin_type":10}`, + } { + t.Run(name, func(t *testing.T) { + var result testType + err := json.Unmarshal([]byte(tc), &result) + if err == nil { + t.Fatal("expected error") + } + }) + } + + // Valid cases. + for name, tc := range map[string]struct { + json string + expected PluginType + }{ + "unknown": {`{"plugin_type":"unknown"}`, PluginTypeUnknown}, + "auth": {`{"plugin_type":"auth"}`, PluginTypeCredential}, + "secret": {`{"plugin_type":"secret"}`, PluginTypeSecrets}, + "database": {`{"plugin_type":"database"}`, PluginTypeDatabase}, + "absent": {`{}`, PluginTypeUnknown}, + "integer unknown": {`{"plugin_type":0}`, PluginTypeUnknown}, + "integer auth": {`{"plugin_type":1}`, PluginTypeCredential}, + "integer db": {`{"plugin_type":2}`, PluginTypeDatabase}, + "integer secret": {`{"plugin_type":3}`, PluginTypeSecrets}, + } { + t.Run(name, func(t *testing.T) { + var result testType + err := json.Unmarshal([]byte(tc.json), &result) + if err != nil { + t.Fatal(err) + } + if tc.expected != result.PluginType { + t.Fatalf("expected %v, got %v", tc.expected, result.PluginType) + } + }) + } +} + +func TestUnknownTypeExcludedWithOmitEmpty(t *testing.T) { + type testTypeOmitEmpty struct { + Type PluginType `json:"type,omitempty"` + } + bytes, err := json.Marshal(testTypeOmitEmpty{}) + if err != nil { + t.Fatal(err) + } + m := map[string]any{} + json.Unmarshal(bytes, &m) + if _, exists := m["type"]; exists { + t.Fatal("type should not be present") + } +} diff --git a/api/sys_plugins.go b/api/sys_plugins.go index f83bdb56f201..9d424d009ec9 100644 --- a/api/sys_plugins.go +++ b/api/sys_plugins.go @@ -274,6 +274,22 @@ func (c *Sys) DeregisterPluginWithContext(ctx context.Context, i *DeregisterPlug return err } +// RootReloadPluginInput is used as input to the RootReloadPlugin function. +type RootReloadPluginInput struct { + Plugin string `json:"-"` // Plugin name, as registered in the plugin catalog. + Type PluginType `json:"-"` // Plugin type: auth, secret, or database. + Scope string `json:"scope,omitempty"` // Empty to reload on current node, "global" for all nodes. +} + +// RootReloadPlugin reloads plugins, possibly returning reloadID for a global +// scoped reload. This is only available in the root namespace, and reloads +// plugins across all namespaces, whereas ReloadPlugin is available in all +// namespaces but only reloads plugins in use in the request's namespace. +func (c *Sys) RootReloadPlugin(ctx context.Context, i *RootReloadPluginInput) (string, error) { + path := fmt.Sprintf("/v1/sys/plugins/reload/%s/%s", i.Type.String(), i.Plugin) + return c.reloadPluginInternal(ctx, path, i, i.Scope == "global") +} + // ReloadPluginInput is used as input to the ReloadPlugin function. type ReloadPluginInput struct { // Plugin is the name of the plugin to reload, as registered in the plugin catalog @@ -292,15 +308,20 @@ func (c *Sys) ReloadPlugin(i *ReloadPluginInput) (string, error) { } // ReloadPluginWithContext reloads mounted plugin backends, possibly returning -// reloadId for a cluster scoped reload +// reloadID for a cluster scoped reload. It is limited to reloading plugins that +// are in use in the request's namespace. See RootReloadPlugin for an API that +// can reload plugins across all namespaces. func (c *Sys) ReloadPluginWithContext(ctx context.Context, i *ReloadPluginInput) (string, error) { + return c.reloadPluginInternal(ctx, "/v1/sys/plugins/reload/backend", i, i.Scope == "global") +} + +func (c *Sys) reloadPluginInternal(ctx context.Context, path string, body any, global bool) (string, error) { ctx, cancelFunc := c.c.withConfiguredTimeout(ctx) defer cancelFunc() - path := "/v1/sys/plugins/reload/backend" req := c.c.NewRequest(http.MethodPut, path) - if err := req.SetJSONBody(i); err != nil { + if err := req.SetJSONBody(body); err != nil { return "", err } @@ -310,7 +331,7 @@ func (c *Sys) ReloadPluginWithContext(ctx context.Context, i *ReloadPluginInput) } defer resp.Body.Close() - if i.Scope == "global" { + if global { // Get the reload id secret, parseErr := ParseSecret(resp.Body) if parseErr != nil { diff --git a/changelog/24878.txt b/changelog/24878.txt new file mode 100644 index 000000000000..d7f03e4d0532 --- /dev/null +++ b/changelog/24878.txt @@ -0,0 +1,6 @@ +```release-note:improvement +plugins: New API `sys/plugins/reload/:type/:name` available in the root namespace for reloading a specific plugin across all namespaces. +``` +```release-note:change +cli: Using `vault plugin reload` with `-plugin` in the root namespace will now reload the plugin across all namespaces instead of just the root namespace. +``` diff --git a/command/plugin_register_test.go b/command/plugin_register_test.go index c50644ae43c6..8d04b7733e48 100644 --- a/command/plugin_register_test.go +++ b/command/plugin_register_test.go @@ -249,7 +249,7 @@ func TestFlagParsing(t *testing.T) { pluginType: api.PluginTypeUnknown, name: "foo", sha256: "abc123", - expectedPayload: `{"type":0,"command":"foo","sha256":"abc123"}`, + expectedPayload: `{"type":"unknown","command":"foo","sha256":"abc123"}`, }, "full": { pluginType: api.PluginTypeCredential, @@ -261,14 +261,14 @@ func TestFlagParsing(t *testing.T) { sha256: "abc123", args: []string{"--a=b", "--b=c", "positional"}, env: []string{"x=1", "y=2"}, - expectedPayload: `{"type":1,"args":["--a=b","--b=c","positional"],"command":"cmd","sha256":"abc123","version":"v1.0.0","oci_image":"image","runtime":"runtime","env":["x=1","y=2"]}`, + expectedPayload: `{"type":"auth","args":["--a=b","--b=c","positional"],"command":"cmd","sha256":"abc123","version":"v1.0.0","oci_image":"image","runtime":"runtime","env":["x=1","y=2"]}`, }, "command remains empty if oci_image specified": { pluginType: api.PluginTypeCredential, name: "name", ociImage: "image", sha256: "abc123", - expectedPayload: `{"type":1,"sha256":"abc123","oci_image":"image"}`, + expectedPayload: `{"type":"auth","sha256":"abc123","oci_image":"image"}`, }, } { tc := tc diff --git a/command/plugin_reload.go b/command/plugin_reload.go index 57e32a57f14d..f7b1cc579f26 100644 --- a/command/plugin_reload.go +++ b/command/plugin_reload.go @@ -4,6 +4,7 @@ package command import ( + "context" "fmt" "strings" @@ -19,9 +20,10 @@ var ( type PluginReloadCommand struct { *BaseCommand - plugin string - mounts []string - scope string + plugin string + mounts []string + scope string + pluginType string } func (c *PluginReloadCommand) Synopsis() string { @@ -36,9 +38,16 @@ Usage: vault plugin reload [options] mount(s) must be provided, but not both. In case the plugin name is provided, all of its corresponding mounted paths that use the plugin backend will be reloaded. - Reload the plugin named "my-custom-plugin": + If run with a Vault namespace other than the root namespace, only plugins + running in the same namespace will be reloaded. - $ vault plugin reload -plugin=my-custom-plugin + Reload the secret plugin named "my-custom-plugin" on the current node: + + $ vault plugin reload -type=secret -plugin=my-custom-plugin + + Reload the secret plugin named "my-custom-plugin" across all nodes and replicated clusters: + + $ vault plugin reload -type=secret -plugin=my-custom-plugin -scope=global ` + c.Flags().Help() @@ -68,7 +77,14 @@ func (c *PluginReloadCommand) Flags() *FlagSets { Name: "scope", Target: &c.scope, Completion: complete.PredictAnything, - Usage: "The scope of the reload, omitted for local, 'global', for replicated reloads", + Usage: "The scope of the reload, omitted for local, 'global', for replicated reloads.", + }) + + f.StringVar(&StringVar{ + Name: "type", + Target: &c.pluginType, + Completion: complete.PredictAnything, + Usage: "The type of plugin to reload, one of auth, secret, or database. Mutually exclusive with -mounts.", }) return set @@ -103,6 +119,10 @@ func (c *PluginReloadCommand) Run(args []string) int { return 1 case c.scope != "" && c.scope != "global": c.UI.Error(fmt.Sprintf("Invalid reload scope: %s", c.scope)) + return 1 + case len(c.mounts) > 0 && c.pluginType != "": + c.UI.Error("Cannot specify -type with -mounts") + return 1 } client, err := c.Client() @@ -111,25 +131,46 @@ func (c *PluginReloadCommand) Run(args []string) int { return 2 } - rid, err := client.Sys().ReloadPlugin(&api.ReloadPluginInput{ - Plugin: c.plugin, - Mounts: c.mounts, - Scope: c.scope, - }) + var reloadID string + if client.Namespace() == "" { + pluginType := api.PluginTypeUnknown + pluginTypeStr := strings.TrimSpace(c.pluginType) + if pluginTypeStr != "" { + var err error + pluginType, err = api.ParsePluginType(pluginTypeStr) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing -type as a plugin type, must be unset or one of auth, secret, or database: %s", err)) + return 1 + } + } + + reloadID, err = client.Sys().RootReloadPlugin(context.Background(), &api.RootReloadPluginInput{ + Plugin: c.plugin, + Type: pluginType, + Scope: c.scope, + }) + } else { + reloadID, err = client.Sys().ReloadPlugin(&api.ReloadPluginInput{ + Plugin: c.plugin, + Mounts: c.mounts, + Scope: c.scope, + }) + } + if err != nil { c.UI.Error(fmt.Sprintf("Error reloading plugin/mounts: %s", err)) return 2 } if len(c.mounts) > 0 { - if rid != "" { - c.UI.Output(fmt.Sprintf("Success! Reloading mounts: %s, reload_id: %s", c.mounts, rid)) + if reloadID != "" { + c.UI.Output(fmt.Sprintf("Success! Reloading mounts: %s, reload_id: %s", c.mounts, reloadID)) } else { c.UI.Output(fmt.Sprintf("Success! Reloaded mounts: %s", c.mounts)) } } else { - if rid != "" { - c.UI.Output(fmt.Sprintf("Success! Reloading plugin: %s, reload_id: %s", c.plugin, rid)) + if reloadID != "" { + c.UI.Output(fmt.Sprintf("Success! Reloading plugin: %s, reload_id: %s", c.plugin, reloadID)) } else { c.UI.Output(fmt.Sprintf("Success! Reloaded plugin: %s", c.plugin)) } diff --git a/command/plugin_reload_test.go b/command/plugin_reload_test.go index edbca3e4e9ea..d84062d8d251 100644 --- a/command/plugin_reload_test.go +++ b/command/plugin_reload_test.go @@ -55,6 +55,18 @@ func TestPluginReloadCommand_Run(t *testing.T) { "Must specify exactly one of -plugin or -mounts", 1, }, + { + "type_and_mounts_mutually_exclusive", + []string{"-mounts", "bar", "-type", "secret"}, + "Cannot specify -type with -mounts", + 1, + }, + { + "invalid_type", + []string{"-plugin", "bar", "-type", "unsupported"}, + "Error parsing -type as a plugin type", + 1, + }, } for _, tc := range cases { diff --git a/sdk/helper/consts/plugin_types.go b/sdk/helper/consts/plugin_types.go index 6bc14b54f716..a7a383827312 100644 --- a/sdk/helper/consts/plugin_types.go +++ b/sdk/helper/consts/plugin_types.go @@ -7,7 +7,10 @@ package consts // https://github.com/hashicorp/vault/blob/main/api/plugin_types.go // Any changes made should be made to both files at the same time. -import "fmt" +import ( + "encoding/json" + "fmt" +) var PluginTypes = []PluginType{ PluginTypeUnknown, @@ -64,3 +67,34 @@ func ParsePluginType(pluginType string) (PluginType, error) { return PluginTypeUnknown, fmt.Errorf("%q is not a supported plugin type", pluginType) } } + +// UnmarshalJSON implements json.Unmarshaler. It supports unmarshaling either a +// string or a uint32. All new serialization will be as a string, but we +// previously serialized as a uint32 so we need to support that for backwards +// compatibility. +func (p *PluginType) UnmarshalJSON(data []byte) error { + var asString string + err := json.Unmarshal(data, &asString) + if err == nil { + *p, err = ParsePluginType(asString) + return err + } + + var asUint32 uint32 + err = json.Unmarshal(data, &asUint32) + if err != nil { + return err + } + *p = PluginType(asUint32) + switch *p { + case PluginTypeUnknown, PluginTypeCredential, PluginTypeDatabase, PluginTypeSecrets: + return nil + default: + return fmt.Errorf("%d is not a supported plugin type", asUint32) + } +} + +// MarshalJSON implements json.Marshaler. +func (p PluginType) MarshalJSON() ([]byte, error) { + return json.Marshal(p.String()) +} diff --git a/sdk/helper/consts/plugin_types_test.go b/sdk/helper/consts/plugin_types_test.go new file mode 100644 index 000000000000..ff1299f2e465 --- /dev/null +++ b/sdk/helper/consts/plugin_types_test.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consts + +// NOTE: this file has been copied to +// https://github.com/hashicorp/vault/blob/main/api/plugin_types_test.go +// Any changes made should be made to both files at the same time. + +import ( + "encoding/json" + "testing" +) + +type testType struct { + PluginType PluginType `json:"plugin_type"` +} + +func TestPluginTypeJSONRoundTrip(t *testing.T) { + for _, pluginType := range PluginTypes { + original := testType{ + PluginType: pluginType, + } + asBytes, err := json.Marshal(original) + if err != nil { + t.Fatal(err) + } + + var roundTripped testType + err = json.Unmarshal(asBytes, &roundTripped) + if err != nil { + t.Fatal(err) + } + + if original != roundTripped { + t.Fatalf("expected %v, got %v", original, roundTripped) + } + } +} + +func TestPluginTypeJSONUnmarshal(t *testing.T) { + // Failure/unsupported cases. + for name, tc := range map[string]string{ + "unsupported": `{"plugin_type":"unsupported"}`, + "random string": `{"plugin_type":"foo"}`, + "boolean": `{"plugin_type":true}`, + "empty": `{"plugin_type":""}`, + "negative": `{"plugin_type":-1}`, + "out of range": `{"plugin_type":10}`, + } { + t.Run(name, func(t *testing.T) { + var result testType + err := json.Unmarshal([]byte(tc), &result) + if err == nil { + t.Fatal("expected error") + } + }) + } + + // Valid cases. + for name, tc := range map[string]struct { + json string + expected PluginType + }{ + "unknown": {`{"plugin_type":"unknown"}`, PluginTypeUnknown}, + "auth": {`{"plugin_type":"auth"}`, PluginTypeCredential}, + "secret": {`{"plugin_type":"secret"}`, PluginTypeSecrets}, + "database": {`{"plugin_type":"database"}`, PluginTypeDatabase}, + "absent": {`{}`, PluginTypeUnknown}, + "integer unknown": {`{"plugin_type":0}`, PluginTypeUnknown}, + "integer auth": {`{"plugin_type":1}`, PluginTypeCredential}, + "integer db": {`{"plugin_type":2}`, PluginTypeDatabase}, + "integer secret": {`{"plugin_type":3}`, PluginTypeSecrets}, + } { + t.Run(name, func(t *testing.T) { + var result testType + err := json.Unmarshal([]byte(tc.json), &result) + if err != nil { + t.Fatal(err) + } + if tc.expected != result.PluginType { + t.Fatalf("expected %v, got %v", tc.expected, result.PluginType) + } + }) + } +} + +func TestUnknownTypeExcludedWithOmitEmpty(t *testing.T) { + type testTypeOmitEmpty struct { + Type PluginType `json:"type,omitempty"` + } + bytes, err := json.Marshal(testTypeOmitEmpty{}) + if err != nil { + t.Fatal(err) + } + m := map[string]any{} + json.Unmarshal(bytes, &m) + if _, exists := m["type"]; exists { + t.Fatal("type should not be present") + } +} diff --git a/sdk/plugin/mock/backend.go b/sdk/plugin/mock/backend.go index 9b3aa2c851e2..6ca6421830fb 100644 --- a/sdk/plugin/mock/backend.go +++ b/sdk/plugin/mock/backend.go @@ -5,13 +5,19 @@ package mock import ( "context" + "fmt" "os" + "testing" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) -const MockPluginVersionEnv = "TESTING_MOCK_VAULT_PLUGIN_VERSION" +const ( + MockPluginVersionEnv = "TESTING_MOCK_VAULT_PLUGIN_VERSION" + MockPluginDefaultInternalValue = "bar" +) // New returns a new backend as an interface. This func // is only necessary for builtin backend plugins. @@ -64,7 +70,7 @@ func Backend() *backend { Invalidate: b.invalidate, BackendType: logical.TypeLogical, } - b.internal = "bar" + b.internal = MockPluginDefaultInternalValue b.RunningVersion = "v0.0.0+mock" if version := os.Getenv(MockPluginVersionEnv); version != "" { b.RunningVersion = version @@ -75,7 +81,7 @@ func Backend() *backend { type backend struct { *framework.Backend - // internal is used to test invalidate + // internal is used to test invalidate and reloads. internal string } @@ -85,3 +91,39 @@ func (b *backend) invalidate(ctx context.Context, key string) { b.internal = "" } } + +// WriteInternalValue is a helper to set an in-memory value in the plugin, +// allowing tests to later assert that the plugin either has or hasn't been +// restarted. +func WriteInternalValue(t *testing.T, client *api.Client, mountPath, value string) { + t.Helper() + resp, err := client.Logical().Write(fmt.Sprintf("%s/internal", mountPath), map[string]interface{}{ + "value": value, + }) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil { + t.Fatalf("bad: %v", resp) + } +} + +// ExpectInternalValue checks the internal in-memory value. +func ExpectInternalValue(t *testing.T, client *api.Client, mountPath, expected string) { + t.Helper() + expectInternalValue(t, client, mountPath, expected) +} + +func expectInternalValue(t *testing.T, client *api.Client, mountPath, expected string) { + t.Helper() + resp, err := client.Logical().Read(fmt.Sprintf("%s/internal", mountPath)) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp == nil { + t.Fatalf("bad: response should not be nil") + } + if resp.Data["value"].(string) != expected { + t.Fatalf("expected %q but got %q", expected, resp.Data["value"].(string)) + } +} diff --git a/vault/external_tests/plugin/plugin_test.go b/vault/external_tests/plugin/plugin_test.go index affe8fa6b888..d0ad56b34a68 100644 --- a/vault/external_tests/plugin/plugin_test.go +++ b/vault/external_tests/plugin/plugin_test.go @@ -466,14 +466,21 @@ func TestSystemBackend_Plugin_SealUnseal(t *testing.T) { } func TestSystemBackend_Plugin_reload(t *testing.T) { + // Paths being tested. + const ( + reloadBackendPath = "sys/plugins/reload/backend" + rootReloadPath = "sys/plugins/reload/%s/%s" + ) testCases := []struct { name string backendType logical.BackendType + path string data map[string]interface{} }{ { name: "test plugin reload for type credential", backendType: logical.TypeCredential, + path: reloadBackendPath, data: map[string]interface{}{ "plugin": "mock-plugin", }, @@ -481,6 +488,7 @@ func TestSystemBackend_Plugin_reload(t *testing.T) { { name: "test mount reload for type credential", backendType: logical.TypeCredential, + path: reloadBackendPath, data: map[string]interface{}{ "mounts": "sys/auth/mock-0/,auth/mock-1/", }, @@ -488,6 +496,7 @@ func TestSystemBackend_Plugin_reload(t *testing.T) { { name: "test plugin reload for type secret", backendType: logical.TypeLogical, + path: reloadBackendPath, data: map[string]interface{}{ "plugin": "mock-plugin", }, @@ -495,21 +504,38 @@ func TestSystemBackend_Plugin_reload(t *testing.T) { { name: "test mount reload for type secret", backendType: logical.TypeLogical, + path: reloadBackendPath, data: map[string]interface{}{ "mounts": "mock-0/,mock-1", }, }, + { + name: "root plugin reload for type auth", + backendType: logical.TypeCredential, + path: fmt.Sprintf(rootReloadPath, "auth", "mock-plugin"), + }, + { + name: "root plugin reload for type secret", + backendType: logical.TypeLogical, + path: fmt.Sprintf(rootReloadPath, "secret", "mock-plugin"), + }, + { + name: "root plugin reload for unknown type", + backendType: logical.TypeUnknown, + path: fmt.Sprintf(rootReloadPath, "unknown", "mock-plugin"), + }, } for _, tc := range testCases { + tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - testSystemBackend_PluginReload(t, tc.data, tc.backendType) + testSystemBackend_PluginReload(t, tc.path, tc.data, tc.backendType) }) } } // Helper func to test different reload methods on plugin reload endpoint -func testSystemBackend_PluginReload(t *testing.T, reqData map[string]interface{}, backendType logical.BackendType) { +func testSystemBackend_PluginReload(t *testing.T, path string, reqData map[string]interface{}, backendType logical.BackendType) { testCases := []struct { pluginVersion string }{ @@ -532,25 +558,25 @@ func testSystemBackend_PluginReload(t *testing.T, reqData map[string]interface{} core := cluster.Cores[0] client := core.Client - pathPrefix := "mock-" + var mountPaths []string if backendType == logical.TypeCredential { - pathPrefix = "auth/" + pathPrefix + mountPaths = []string{"auth/mock-0", "auth/mock-1"} + } else { + mountPaths = []string{"mock-0", "mock-1"} } - for i := 0; i < 2; i++ { + + for _, mountPath := range mountPaths { // Update internal value in the backend - resp, err := client.Logical().Write(fmt.Sprintf("%s%d/internal", pathPrefix, i), map[string]interface{}{ - "value": "baz", - }) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp != nil { - t.Fatalf("bad: %v", resp) - } + mock.WriteInternalValue(t, client, mountPath, "baz") } - // Perform plugin reload - resp, err := client.Logical().Write("sys/plugins/reload/backend", reqData) + // Verify our precondition that the write succeeded. + for _, mountPath := range mountPaths { + mock.ExpectInternalValue(t, client, mountPath, "baz") + } + + // Perform plugin reload which should reset the value. + resp, err := client.Logical().Write(path, reqData) if err != nil { t.Fatalf("err: %v", err) } @@ -564,18 +590,9 @@ func testSystemBackend_PluginReload(t *testing.T, reqData map[string]interface{} t.Fatal(resp.Warnings) } - for i := 0; i < 2; i++ { - // Ensure internal backed value is reset - resp, err := client.Logical().Read(fmt.Sprintf("%s%d/internal", pathPrefix, i)) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp == nil { - t.Fatalf("bad: response should not be nil") - } - if resp.Data["value"].(string) == "baz" { - t.Fatal("did not expect backend internal value to be 'baz'") - } + // Ensure internal backed value is reset + for _, mountPath := range mountPaths { + mock.ExpectInternalValue(t, client, mountPath, mock.MockPluginDefaultInternalValue) } }) } @@ -643,7 +660,7 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo env := []string{pluginutil.PluginCACertPEMEnv + "=" + cluster.CACertPEMFile} switch backendType { - case logical.TypeLogical: + case logical.TypeLogical, logical.TypeUnknown: plugin := logicalVersionMap[pluginVersion] vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", plugin, env) for i := 0; i < numMounts; i++ { diff --git a/vault/logical_system.go b/vault/logical_system.go index 621595485004..c1f5a4625f5c 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -199,6 +199,7 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf b.Backend.Paths = append(b.Backend.Paths, b.pluginsCatalogListPaths()...) b.Backend.Paths = append(b.Backend.Paths, b.pluginsCatalogCRUDPath()) b.Backend.Paths = append(b.Backend.Paths, b.pluginsReloadPath()) + b.Backend.Paths = append(b.Backend.Paths, b.pluginsRootReloadPath()) b.Backend.Paths = append(b.Backend.Paths, b.pluginsRuntimesCatalogCRUDPath()) b.Backend.Paths = append(b.Backend.Paths, b.pluginsRuntimesCatalogListPaths()...) b.Backend.Paths = append(b.Backend.Paths, b.auditPaths()...) @@ -744,8 +745,13 @@ func (b *SystemBackend) handlePluginReloadUpdate(ctx context.Context, req *logic }, } + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, err + } + if pluginName != "" { - reloaded, err := b.Core.reloadMatchingPlugin(ctx, pluginName) + reloaded, err := b.Core.reloadMatchingPlugin(ctx, ns, consts.PluginTypeUnknown, pluginName) if err != nil { return nil, err } @@ -757,14 +763,84 @@ func (b *SystemBackend) handlePluginReloadUpdate(ctx context.Context, req *logic } } } else if len(pluginMounts) > 0 { - err := b.Core.reloadMatchingPluginMounts(ctx, pluginMounts) + err := b.Core.reloadMatchingPluginMounts(ctx, ns, pluginMounts) if err != nil { return nil, err } } if scope == globalScope { - err := handleGlobalPluginReload(ctx, b.Core, req.ID, pluginName, pluginMounts) + reloadRequest := pluginReloadRequest{ + Namespace: ns, + Timestamp: time.Now(), + ReloadID: req.ID, + PluginType: consts.PluginTypeUnknown, + } + + if pluginName != "" { + reloadRequest.Type = pluginReloadPluginsType + reloadRequest.Subjects = []string{pluginName} + } else { + reloadRequest.Type = pluginReloadMountsType + reloadRequest.Subjects = pluginMounts + } + err = handleGlobalPluginReload(ctx, b.Core, reloadRequest) + if err != nil { + return nil, err + } + return logical.RespondWithStatusCode(&resp, req, http.StatusAccepted) + } + return &resp, nil +} + +func (b *SystemBackend) handleRootPluginReloadUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + pluginName := d.Get("name").(string) + pluginTypeStr := d.Get("type").(string) + scope := d.Get("scope").(string) + + if pluginName == "" { + return logical.ErrorResponse("'plugin' must be provided"), nil + } + if pluginTypeStr == "" { + return logical.ErrorResponse("'type' must be provided"), nil + } + pluginType, err := consts.ParsePluginType(pluginTypeStr) + if err != nil { + return logical.ErrorResponse(`error parsing %q as a plugin type, must be one of "auth", "secret", "database", or "unknown"`, pluginTypeStr), nil + } + if scope != "" && scope != globalScope { + return logical.ErrorResponse("reload scope must be omitted or 'global'"), nil + } + + resp := logical.Response{ + Data: map[string]interface{}{ + "reload_id": req.ID, + }, + } + + reloaded, err := b.Core.reloadMatchingPlugin(ctx, nil, pluginType, pluginName) + if err != nil { + return nil, err + } + if reloaded == 0 { + if scope == globalScope { + resp.AddWarning("no plugins were reloaded locally (but they may be reloaded on other nodes)") + } else { + resp.AddWarning("no plugins were reloaded") + } + } + + if scope == globalScope { + reloadRequest := pluginReloadRequest{ + Type: pluginReloadPluginsType, + Subjects: []string{pluginName}, + PluginType: pluginType, + Namespace: nil, + Timestamp: time.Now(), + ReloadID: req.ID, + } + + err = handleGlobalPluginReload(ctx, b.Core, reloadRequest) if err != nil { return nil, err } @@ -6405,6 +6481,36 @@ This path responds to the following HTTP methods. `The mount paths of the plugin backends to reload.`, "", }, + "plugin-backend-reload-scope": { + `The scope for the reload operation. May be empty or "global".`, + `The scope for the reload operation. May be empty or "global". If empty, + the plugin(s) will be reloaded only on the local node. If "global", the + plugin(s) will be reloaded on all nodes in the cluster and in all replicated + clusters.`, + }, + "root-plugin-reload": { + "Reload all instances of a specific plugin.", + `Reload all plugins of a specific name and type across all namespaces. If + "scope" is provided and is "global", the plugin is reloaded across all + nodes and clusters. If a new plugin version has been pinned, this will + ensure all instances start using the new version.`, + }, + "root-plugin-reload-name": { + `The name of the plugin to reload, as registered in the plugin catalog.`, + "", + }, + "root-plugin-reload-type": { + `The type of the plugin to reload, as registered in the plugin catalog.`, + "", + }, + "root-plugin-reload-scope": { + `The scope for the reload operation. May be empty or "global".`, + `The scope for the reload operation. May be empty or "global". If empty, + the plugin will be reloaded only on the local node. If "global", the + plugin will be reloaded on all nodes in the cluster and in all replicated + clusters. A "global" reload will ensure that any pinned version specified + is in full effect.`, + }, "hash": { "Generate a hash sum for input data", "Generates a hash sum of the given algorithm against the given input data.", diff --git a/vault/logical_system_helpers.go b/vault/logical_system_helpers.go index 3499762b0162..f69b1e38da58 100644 --- a/vault/logical_system_helpers.go +++ b/vault/logical_system_helpers.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/go-memdb" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/logical" ) @@ -263,7 +264,7 @@ var ( return paths } - handleGlobalPluginReload = func(context.Context, *Core, string, string, []string) error { + handleGlobalPluginReload = func(context.Context, *Core, pluginReloadRequest) error { return nil } handleSetupPluginReload = func(*Core) error { @@ -277,6 +278,16 @@ var ( checkRaw = func(b *SystemBackend, path string) error { return nil } ) +// Contains the config for a global plugin reload +type pluginReloadRequest struct { + Type string `json:"type"` // Either 'plugins' or 'mounts' + PluginType consts.PluginType `json:"plugin_type"` + Subjects []string `json:"subjects"` // The plugin names or mount points for the reload + ReloadID string `json:"reload_id"` // a UUID for the request + Timestamp time.Time `json:"timestamp"` + Namespace *namespace.Namespace +} + // tuneMount is used to set config on a mount point func (b *SystemBackend) tuneMountTTLs(ctx context.Context, path string, me *MountEntry, newDefault, newMax time.Duration) error { zero := time.Duration(0) diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index c12b83e6ecaf..0b621c423d44 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -2109,6 +2109,66 @@ func (b *SystemBackend) pluginsReloadPath() *framework.Path { } } +func (b *SystemBackend) pluginsRootReloadPath() *framework.Path { + return &framework.Path{ + // Unknown plugin type is allowed to make it easier for the CLI changes to be more backwards compatible. + Pattern: "plugins/reload/(?Pauth|database|secret|unknown)/" + framework.GenericNameRegex("name") + "$", + + DisplayAttrs: &framework.DisplayAttributes{ + OperationVerb: "reload", + OperationSuffix: "plugins", + }, + + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["root-plugin-reload-name"][0]), + Required: true, + }, + "type": { + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["root-plugin-reload-type"][0]), + Required: true, + }, + "scope": { + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["root-plugin-reload-scope"][0]), + }, + }, + + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.handleRootPluginReloadUpdate, + Responses: map[int][]framework.Response{ + http.StatusOK: {{ + Description: "OK", + Fields: map[string]*framework.FieldSchema{ + "reload_id": { + Type: framework.TypeString, + Required: true, + }, + }, + }}, + http.StatusAccepted: {{ + Description: "OK", + Fields: map[string]*framework.FieldSchema{ + "reload_id": { + Type: framework.TypeString, + Required: true, + }, + }, + }}, + }, + Summary: "Reload all instances of a specific plugin.", + Description: `Reload all plugins of a specific name and type across all namespaces. If "scope" is provided and is "global", the plugin is reloaded across all nodes and clusters. If a new plugin version has been pinned, this will ensure all instances start using the new version.`, + }, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["root-plugin-reload"][0]), + HelpDescription: strings.TrimSpace(sysHelp["root-plugin-reload"][1]), + } +} + func (b *SystemBackend) pluginsRuntimesCatalogCRUDPath() *framework.Path { return &framework.Path{ Pattern: "plugins/runtimes/catalog/(?Pcontainer)/" + framework.GenericNameRegex("name"), diff --git a/vault/plugin_reload.go b/vault/plugin_reload.go index 938c47eb34bf..74f395c4030a 100644 --- a/vault/plugin_reload.go +++ b/vault/plugin_reload.go @@ -18,19 +18,19 @@ import ( "github.com/hashicorp/vault/sdk/plugin" ) +const ( + pluginReloadPluginsType = "plugins" + pluginReloadMountsType = "mounts" +) + // reloadMatchingPluginMounts reloads provided mounts, regardless of // plugin name, as long as the backend type is plugin. -func (c *Core) reloadMatchingPluginMounts(ctx context.Context, mounts []string) error { +func (c *Core) reloadMatchingPluginMounts(ctx context.Context, ns *namespace.Namespace, mounts []string) error { c.mountsLock.RLock() defer c.mountsLock.RUnlock() c.authLock.RLock() defer c.authLock.RUnlock() - ns, err := namespace.FromContext(ctx) - if err != nil { - return err - } - var errors error for _, mount := range mounts { var isAuth bool @@ -73,70 +73,87 @@ func (c *Core) reloadMatchingPluginMounts(ctx context.Context, mounts []string) // reloadMatchingPlugin reloads all mounted backends that are named pluginName // (name of the plugin as registered in the plugin catalog). It returns the // number of plugins that were reloaded and an error if any. -func (c *Core) reloadMatchingPlugin(ctx context.Context, pluginName string) (reloaded int, err error) { - c.mountsLock.RLock() - defer c.mountsLock.RUnlock() - c.authLock.RLock() - defer c.authLock.RUnlock() - - ns, err := namespace.FromContext(ctx) - if err != nil { - return reloaded, err +func (c *Core) reloadMatchingPlugin(ctx context.Context, ns *namespace.Namespace, pluginType consts.PluginType, pluginName string) (reloaded int, err error) { + var secrets, auth, database bool + switch pluginType { + case consts.PluginTypeSecrets: + secrets = true + case consts.PluginTypeCredential: + auth = true + case consts.PluginTypeDatabase: + database = true + case consts.PluginTypeUnknown: + secrets = true + auth = true + database = true + default: + return reloaded, fmt.Errorf("unsupported plugin type %q", pluginType.String()) } - for _, entry := range c.mounts.Entries { - // We dont reload mounts that are not in the same namespace - if ns.ID != entry.Namespace().ID { - continue - } + if secrets || database { + c.mountsLock.RLock() + defer c.mountsLock.RUnlock() - if entry.Type == pluginName || (entry.Type == "plugin" && entry.Config.PluginName == pluginName) { - err := c.reloadBackendCommon(ctx, entry, false) - if err != nil { - return reloaded, err - } - reloaded++ - c.logger.Info("successfully reloaded plugin", "plugin", pluginName, "namespace", entry.Namespace(), "path", entry.Path, "version", entry.Version) - } else if entry.Type == "database" { - // The combined database plugin is itself a secrets engine, but - // knowledge of whether a database plugin is in use within a particular - // mount is internal to the combined database plugin's storage, so - // we delegate the reload request with an internally routed request. - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: entry.Path + "reload/" + pluginName, - } - resp, err := c.router.Route(ctx, req) - if err != nil { - return reloaded, err - } - if resp == nil { - return reloaded, fmt.Errorf("failed to reload %q database plugin(s) mounted under %s", pluginName, entry.Path) - } - if resp.IsError() { - return reloaded, fmt.Errorf("failed to reload %q database plugin(s) mounted under %s: %s", pluginName, entry.Path, resp.Error()) + for _, entry := range c.mounts.Entries { + // We don't reload mounts that are not in the same namespace + if ns != nil && ns.ID != entry.Namespace().ID { + continue } - if count, ok := resp.Data["count"].(int); ok && count > 0 { - c.logger.Info("successfully reloaded database plugin(s)", "plugin", pluginName, "namespace", entry.Namespace(), "path", entry.Path, "connections", resp.Data["connections"]) - reloaded += count + if secrets && (entry.Type == pluginName || (entry.Type == "plugin" && entry.Config.PluginName == pluginName)) { + err := c.reloadBackendCommon(ctx, entry, false) + if err != nil { + return reloaded, err + } + reloaded++ + c.logger.Info("successfully reloaded plugin", "plugin", pluginName, "namespace", entry.Namespace(), "path", entry.Path, "version", entry.Version) + } else if database && entry.Type == "database" { + // The combined database plugin is itself a secrets engine, but + // knowledge of whether a database plugin is in use within a particular + // mount is internal to the combined database plugin's storage, so + // we delegate the reload request with an internally routed request. + reqCtx := namespace.ContextWithNamespace(ctx, entry.namespace) + req := &logical.Request{ + Operation: logical.UpdateOperation, + Path: entry.Path + "reload/" + pluginName, + } + resp, err := c.router.Route(reqCtx, req) + if err != nil { + return reloaded, err + } + if resp == nil { + return reloaded, fmt.Errorf("failed to reload %q database plugin(s) mounted under %s", pluginName, entry.Path) + } + if resp.IsError() { + return reloaded, fmt.Errorf("failed to reload %q database plugin(s) mounted under %s: %s", pluginName, entry.Path, resp.Error()) + } + + if count, ok := resp.Data["count"].(int); ok && count > 0 { + c.logger.Info("successfully reloaded database plugin(s)", "plugin", pluginName, "namespace", entry.Namespace(), "path", entry.Path, "connections", resp.Data["connections"]) + reloaded += count + } } } } - for _, entry := range c.auth.Entries { - // We dont reload mounts that are not in the same namespace - if ns.ID != entry.Namespace().ID { - continue - } + if auth { + c.authLock.RLock() + defer c.authLock.RUnlock() - if entry.Type == pluginName || (entry.Type == "plugin" && entry.Config.PluginName == pluginName) { - err := c.reloadBackendCommon(ctx, entry, true) - if err != nil { - return reloaded, err + for _, entry := range c.auth.Entries { + // We don't reload mounts that are not in the same namespace + if ns != nil && ns.ID != entry.Namespace().ID { + continue + } + + if entry.Type == pluginName || (entry.Type == "plugin" && entry.Config.PluginName == pluginName) { + err := c.reloadBackendCommon(ctx, entry, true) + if err != nil { + return reloaded, err + } + reloaded++ + c.logger.Info("successfully reloaded plugin", "plugin", entry.Accessor, "path", entry.Path, "version", entry.Version) } - reloaded++ - c.logger.Info("successfully reloaded plugin", "plugin", entry.Accessor, "path", entry.Path, "version", entry.Version) } } diff --git a/website/content/api-docs/system/plugins-reload-backend.mdx b/website/content/api-docs/system/plugins-reload-backend.mdx deleted file mode 100644 index 261371876e7b..000000000000 --- a/website/content/api-docs/system/plugins-reload-backend.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -layout: api -page_title: /sys/plugins/reload/backend - HTTP API -description: The `/sys/plugins/reload/backend` endpoint is used to reload plugin backends. ---- - -# `/sys/plugins/reload/backend` - -The `/sys/plugins/reload/backend` endpoint is used to reload mounted plugin -backends. Either the plugin name (`plugin`) or the desired plugin backend mounts -(`mounts`) must be provided, but not both. In the case that the plugin name is -provided, all mounted paths that use that plugin backend will be reloaded. - -## Reload plugins - -This endpoint reloads mounted plugin backends. - -| Method | Path - | -| :----- | :---------------------------- | -| `POST` | `/sys/plugins/reload/backend` | - -### Parameters - -- `plugin` `(string: "")` – The name of the plugin to reload, as - registered in the plugin catalog. - -- `mounts` `(array: [])` – Array or comma-separated string mount paths - of the plugin backends to reload. - -- `scope` `(string: "")` - The scope of the reload. If omitted, reloads the - plugin or mounts on this Vault instance. If 'global', will begin reloading the - plugin on all instances of a cluster. - -### Sample payload - -```json -{ - "plugin": "mock-plugin" -} -``` - -### Sample request - -```shell-session -$ curl \ - --header "X-Vault-Token: ..." \ - --request POST \ - --data @payload.json \ - http://127.0.0.1:8200/v1/sys/plugins/reload/backend -``` diff --git a/website/content/api-docs/system/plugins-reload.mdx b/website/content/api-docs/system/plugins-reload.mdx new file mode 100644 index 000000000000..d95d5f2dc84d --- /dev/null +++ b/website/content/api-docs/system/plugins-reload.mdx @@ -0,0 +1,121 @@ +--- +layout: api +page_title: /sys/plugins/reload - HTTP API +description: The `/sys/plugins/reload` endpoints are used to reload plugins. +--- + +# `/sys/plugins/reload` + +## Reload plugin + +The `/sys/plugins/reload/:type/:name` endpoint reloads a named plugin across all +namespaces. It is only available in the root namespace. All instances of the plugin +will be killed, and any newly pinned version of the plugin will be started in +their place. + +| Method | Path | +| :----- | :-------------------------------- | +| `POST` | `/sys/plugins/reload/:type/:name` | + +### Parameters + +- `type` `(string: )` – The type of the plugin, as registered in the + plugin catalog. One of "auth", "secret", or "database". + +- `name` `(string: )` – The name of the plugin to reload, as registered + in the plugin catalog. + +- `scope` `(string: "")` - The scope of the reload. If omitted, reloads the + plugin or mounts on this Vault instance. If 'global', will begin reloading the + plugin on all instances of a cluster. + +### Sample payload + +```json +{ + "scope": "global" +} +``` + +### Sample request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + http://127.0.0.1:8200/v1/sys/plugins/reload/auth/mock-plugin +``` + +### Sample response + +```json +{ + "data": { + "reload_id": "bdddb8df-ccb6-1b09-670d-efa9d3f2c11b" + }, + ... +} +``` + +-> Note: If no plugins are reloaded on the node that serviced the request, a +warning will also be returned in the response. + +## Reload plugins within a namespace + +The `/sys/plugins/reload/backend` endpoint is used to reload mounted plugin +backends. Either the plugin name (`plugin`) or the desired plugin backend mounts +(`mounts`) must be provided, but not both. In the case that the plugin name is +provided, all mounted paths that use that plugin backend will be reloaded. + +This API is available in all namespaces, and is limited to reloading plugins in +use within the request's namespace. + +| Method | Path - | +| :----- | :---------------------------- | +| `POST` | `/sys/plugins/reload/backend` | + +### Parameters + +- `plugin` `(string: "")` – The name of the plugin to reload, as + registered in the plugin catalog. + +- `mounts` `(array: [])` – Array or comma-separated string mount paths + of the plugin backends to reload. + +- `scope` `(string: "")` - The scope of the reload. If omitted, reloads the + plugin or mounts on this Vault instance. If 'global', will begin reloading the + plugin on all instances of a cluster. + +### Sample payload + +```json +{ + "plugin": "mock-plugin", + "scope": "global" +} +``` + +### Sample request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + http://127.0.0.1:8200/v1/sys/plugins/reload/backend +``` + +### Sample response + +```json +{ + "data": { + "reload_id": "bdddb8df-ccb6-1b09-670d-efa9d3f2c11b" + }, + ... +} +``` + +-> Note: If no plugins are reloaded on the node that serviced the request, a +warning will also be returned in the response. diff --git a/website/content/docs/upgrading/plugins.mdx b/website/content/docs/upgrading/plugins.mdx index 5f9bf9e43341..2ce54280db88 100644 --- a/website/content/docs/upgrading/plugins.mdx +++ b/website/content/docs/upgrading/plugins.mdx @@ -184,6 +184,6 @@ transit v1.12.0+builtin.vault of leases and tokens is handled by core systems within Vault. The plugin itself only handles renewal and revocation of them when it’s requested by those core systems. -[plugin_reload_api]: /vault/api-docs/system/plugins-reload-backend +[plugin_reload_api]: /vault/api-docs/system/plugins-reload [plugin_registration]: /vault/docs/plugins/plugin-architecture#plugin-registration [plugin_management]: /vault/docs/plugins/plugin-management#enabling-disabling-external-plugins diff --git a/website/data/api-docs-nav-data.json b/website/data/api-docs-nav-data.json index 55d82cd8f8ae..f05e31208792 100644 --- a/website/data/api-docs-nav-data.json +++ b/website/data/api-docs-nav-data.json @@ -602,8 +602,8 @@ "path": "system/namespaces" }, { - "title": "/sys/plugins/reload/backend", - "path": "system/plugins-reload-backend" + "title": "/sys/plugins/reload", + "path": "system/plugins-reload" }, { "title": "/sys/plugins/catalog", diff --git a/website/redirects.js b/website/redirects.js index 9ee247977fd2..2fa7fb80f537 100644 --- a/website/redirects.js +++ b/website/redirects.js @@ -104,5 +104,10 @@ module.exports = [ source: '/vault/docs/v1.13.x/agent-and-proxy/agent/apiproxy', destination: '/vault/docs/v1.13.x/agent/apiproxy', permanent: true, + }, + { + source: '/vault/api-docs/system/plugins-reload-backend', + destination: '/vault/api-docs/system/plugins-reload', + permanent: true, } ] From 6be5d09e861550b7f734a80f5efbb8ce3ce042f0 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Wed, 17 Jan 2024 15:20:25 +0000 Subject: [PATCH 2/2] Docs updates --- command/plugin_reload.go | 3 ++- .../api-docs/system/plugins-reload.mdx | 3 ++- .../content/docs/commands/plugin/reload.mdx | 23 ++++++++++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/command/plugin_reload.go b/command/plugin_reload.go index f7b1cc579f26..bdcd6f696efb 100644 --- a/command/plugin_reload.go +++ b/command/plugin_reload.go @@ -84,7 +84,8 @@ func (c *PluginReloadCommand) Flags() *FlagSets { Name: "type", Target: &c.pluginType, Completion: complete.PredictAnything, - Usage: "The type of plugin to reload, one of auth, secret, or database. Mutually exclusive with -mounts.", + Usage: "The type of plugin to reload, one of auth, secret, or database. Mutually " + + "exclusive with -mounts. If not provided, all plugins with a matching name will be reloaded.", }) return set diff --git a/website/content/api-docs/system/plugins-reload.mdx b/website/content/api-docs/system/plugins-reload.mdx index d95d5f2dc84d..eee8804de2fc 100644 --- a/website/content/api-docs/system/plugins-reload.mdx +++ b/website/content/api-docs/system/plugins-reload.mdx @@ -20,7 +20,8 @@ their place. ### Parameters - `type` `(string: )` – The type of the plugin, as registered in the - plugin catalog. One of "auth", "secret", or "database". + plugin catalog. One of "auth", "secret", "database", or "unknown". If "unknown", + all plugin types with the provided name will be reloaded. - `name` `(string: )` – The name of the plugin to reload, as registered in the plugin catalog. diff --git a/website/content/docs/commands/plugin/reload.mdx b/website/content/docs/commands/plugin/reload.mdx index 9a74b809fc27..0772e1ca99a0 100644 --- a/website/content/docs/commands/plugin/reload.mdx +++ b/website/content/docs/commands/plugin/reload.mdx @@ -9,14 +9,18 @@ description: |- The `plugin reload` command is used to reload mounted plugin backends. Either the plugin name (`plugin`) or the desired plugin backend mounts (`mounts`) -must be provided, but not both. In the case that the plugin name is provided, all mounted paths that use that plugin backend will be reloaded. +must be provided, but not both. In the case that the plugin name is provided, +all mounted paths that use that plugin backend will be reloaded. + +If run with a Vault namespace other than the root namespace, only plugins +running in the same namespace will be reloaded. ## Examples -Reload a plugin by name: +Reload a plugin by type and name: ```shell-session -$ vault plugin reload -plugin my-custom-plugin +$ vault plugin reload -type=auth -plugin my-custom-plugin Success! Reloaded plugin: my-custom-plugin ``` @@ -38,6 +42,15 @@ $ vault plugin reload \ Success! Reloaded mounts: [my-custom-plugin-1/ my-custom-plugin-2/] ``` +Reload a secrets plugin named "my-custom-plugin" across all nodes and replicated clusters: + +```shell-session +$ vault plugin reload \ + -type=secret \ + -plugin=my-custom-plugin \ + -scope=global +``` + ## Usage The following flags are available in addition to the [standard set of @@ -48,6 +61,10 @@ flags](/vault/docs/commands) included on all commands. - `-plugin` `(string: "")` - The name of the plugin to reload, as registered in the plugin catalog. +- `-type` `(string: "")` - The type of plugin to reload, one of auth, secret, or + database. Mutually exclusive with -mounts. If not provided, all plugins + with a matching name will be reloaded. + - `-mounts` `(array: [])` - Array or comma-separated string mount paths of the plugin backends to reload.