mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
New root namespace plugin reload API sys/plugins/reload/:type/:name (#24878)
This commit is contained in:
@@ -7,7 +7,10 @@ package api
|
|||||||
// https://github.com/hashicorp/vault/blob/main/sdk/helper/consts/plugin_types.go
|
// 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.
|
// Any changes made should be made to both files at the same time.
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
var PluginTypes = []PluginType{
|
var PluginTypes = []PluginType{
|
||||||
PluginTypeUnknown,
|
PluginTypeUnknown,
|
||||||
@@ -64,3 +67,34 @@ func ParsePluginType(pluginType string) (PluginType, error) {
|
|||||||
return PluginTypeUnknown, fmt.Errorf("%q is not a supported plugin type", pluginType)
|
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())
|
||||||
|
}
|
||||||
|
|||||||
101
api/plugin_types_test.go
Normal file
101
api/plugin_types_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,6 +274,22 @@ func (c *Sys) DeregisterPluginWithContext(ctx context.Context, i *DeregisterPlug
|
|||||||
return err
|
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.
|
// ReloadPluginInput is used as input to the ReloadPlugin function.
|
||||||
type ReloadPluginInput struct {
|
type ReloadPluginInput struct {
|
||||||
// Plugin is the name of the plugin to reload, as registered in the plugin catalog
|
// 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
|
// 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) {
|
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)
|
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
|
||||||
defer cancelFunc()
|
defer cancelFunc()
|
||||||
|
|
||||||
path := "/v1/sys/plugins/reload/backend"
|
|
||||||
req := c.c.NewRequest(http.MethodPut, path)
|
req := c.c.NewRequest(http.MethodPut, path)
|
||||||
|
|
||||||
if err := req.SetJSONBody(i); err != nil {
|
if err := req.SetJSONBody(body); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +331,7 @@ func (c *Sys) ReloadPluginWithContext(ctx context.Context, i *ReloadPluginInput)
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if i.Scope == "global" {
|
if global {
|
||||||
// Get the reload id
|
// Get the reload id
|
||||||
secret, parseErr := ParseSecret(resp.Body)
|
secret, parseErr := ParseSecret(resp.Body)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
|
|||||||
6
changelog/24878.txt
Normal file
6
changelog/24878.txt
Normal file
@@ -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.
|
||||||
|
```
|
||||||
@@ -249,7 +249,7 @@ func TestFlagParsing(t *testing.T) {
|
|||||||
pluginType: api.PluginTypeUnknown,
|
pluginType: api.PluginTypeUnknown,
|
||||||
name: "foo",
|
name: "foo",
|
||||||
sha256: "abc123",
|
sha256: "abc123",
|
||||||
expectedPayload: `{"type":0,"command":"foo","sha256":"abc123"}`,
|
expectedPayload: `{"type":"unknown","command":"foo","sha256":"abc123"}`,
|
||||||
},
|
},
|
||||||
"full": {
|
"full": {
|
||||||
pluginType: api.PluginTypeCredential,
|
pluginType: api.PluginTypeCredential,
|
||||||
@@ -261,14 +261,14 @@ func TestFlagParsing(t *testing.T) {
|
|||||||
sha256: "abc123",
|
sha256: "abc123",
|
||||||
args: []string{"--a=b", "--b=c", "positional"},
|
args: []string{"--a=b", "--b=c", "positional"},
|
||||||
env: []string{"x=1", "y=2"},
|
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": {
|
"command remains empty if oci_image specified": {
|
||||||
pluginType: api.PluginTypeCredential,
|
pluginType: api.PluginTypeCredential,
|
||||||
name: "name",
|
name: "name",
|
||||||
ociImage: "image",
|
ociImage: "image",
|
||||||
sha256: "abc123",
|
sha256: "abc123",
|
||||||
expectedPayload: `{"type":1,"sha256":"abc123","oci_image":"image"}`,
|
expectedPayload: `{"type":"auth","sha256":"abc123","oci_image":"image"}`,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
tc := tc
|
tc := tc
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -19,9 +20,10 @@ var (
|
|||||||
|
|
||||||
type PluginReloadCommand struct {
|
type PluginReloadCommand struct {
|
||||||
*BaseCommand
|
*BaseCommand
|
||||||
plugin string
|
plugin string
|
||||||
mounts []string
|
mounts []string
|
||||||
scope string
|
scope string
|
||||||
|
pluginType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PluginReloadCommand) Synopsis() 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,
|
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.
|
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()
|
` + c.Flags().Help()
|
||||||
|
|
||||||
@@ -68,7 +77,15 @@ func (c *PluginReloadCommand) Flags() *FlagSets {
|
|||||||
Name: "scope",
|
Name: "scope",
|
||||||
Target: &c.scope,
|
Target: &c.scope,
|
||||||
Completion: complete.PredictAnything,
|
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. If not provided, all plugins with a matching name will be reloaded.",
|
||||||
})
|
})
|
||||||
|
|
||||||
return set
|
return set
|
||||||
@@ -103,6 +120,10 @@ func (c *PluginReloadCommand) Run(args []string) int {
|
|||||||
return 1
|
return 1
|
||||||
case c.scope != "" && c.scope != "global":
|
case c.scope != "" && c.scope != "global":
|
||||||
c.UI.Error(fmt.Sprintf("Invalid reload scope: %s", c.scope))
|
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()
|
client, err := c.Client()
|
||||||
@@ -111,25 +132,46 @@ func (c *PluginReloadCommand) Run(args []string) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
rid, err := client.Sys().ReloadPlugin(&api.ReloadPluginInput{
|
var reloadID string
|
||||||
Plugin: c.plugin,
|
if client.Namespace() == "" {
|
||||||
Mounts: c.mounts,
|
pluginType := api.PluginTypeUnknown
|
||||||
Scope: c.scope,
|
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 {
|
if err != nil {
|
||||||
c.UI.Error(fmt.Sprintf("Error reloading plugin/mounts: %s", err))
|
c.UI.Error(fmt.Sprintf("Error reloading plugin/mounts: %s", err))
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.mounts) > 0 {
|
if len(c.mounts) > 0 {
|
||||||
if rid != "" {
|
if reloadID != "" {
|
||||||
c.UI.Output(fmt.Sprintf("Success! Reloading mounts: %s, reload_id: %s", c.mounts, rid))
|
c.UI.Output(fmt.Sprintf("Success! Reloading mounts: %s, reload_id: %s", c.mounts, reloadID))
|
||||||
} else {
|
} else {
|
||||||
c.UI.Output(fmt.Sprintf("Success! Reloaded mounts: %s", c.mounts))
|
c.UI.Output(fmt.Sprintf("Success! Reloaded mounts: %s", c.mounts))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if rid != "" {
|
if reloadID != "" {
|
||||||
c.UI.Output(fmt.Sprintf("Success! Reloading plugin: %s, reload_id: %s", c.plugin, rid))
|
c.UI.Output(fmt.Sprintf("Success! Reloading plugin: %s, reload_id: %s", c.plugin, reloadID))
|
||||||
} else {
|
} else {
|
||||||
c.UI.Output(fmt.Sprintf("Success! Reloaded plugin: %s", c.plugin))
|
c.UI.Output(fmt.Sprintf("Success! Reloaded plugin: %s", c.plugin))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,18 @@ func TestPluginReloadCommand_Run(t *testing.T) {
|
|||||||
"Must specify exactly one of -plugin or -mounts",
|
"Must specify exactly one of -plugin or -mounts",
|
||||||
1,
|
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 {
|
for _, tc := range cases {
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ package consts
|
|||||||
// https://github.com/hashicorp/vault/blob/main/api/plugin_types.go
|
// https://github.com/hashicorp/vault/blob/main/api/plugin_types.go
|
||||||
// Any changes made should be made to both files at the same time.
|
// Any changes made should be made to both files at the same time.
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
var PluginTypes = []PluginType{
|
var PluginTypes = []PluginType{
|
||||||
PluginTypeUnknown,
|
PluginTypeUnknown,
|
||||||
@@ -64,3 +67,34 @@ func ParsePluginType(pluginType string) (PluginType, error) {
|
|||||||
return PluginTypeUnknown, fmt.Errorf("%q is not a supported plugin type", pluginType)
|
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())
|
||||||
|
}
|
||||||
|
|||||||
101
sdk/helper/consts/plugin_types_test.go
Normal file
101
sdk/helper/consts/plugin_types_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,19 @@ package mock
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
"github.com/hashicorp/vault/sdk/framework"
|
"github.com/hashicorp/vault/sdk/framework"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"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
|
// New returns a new backend as an interface. This func
|
||||||
// is only necessary for builtin backend plugins.
|
// is only necessary for builtin backend plugins.
|
||||||
@@ -64,7 +70,7 @@ func Backend() *backend {
|
|||||||
Invalidate: b.invalidate,
|
Invalidate: b.invalidate,
|
||||||
BackendType: logical.TypeLogical,
|
BackendType: logical.TypeLogical,
|
||||||
}
|
}
|
||||||
b.internal = "bar"
|
b.internal = MockPluginDefaultInternalValue
|
||||||
b.RunningVersion = "v0.0.0+mock"
|
b.RunningVersion = "v0.0.0+mock"
|
||||||
if version := os.Getenv(MockPluginVersionEnv); version != "" {
|
if version := os.Getenv(MockPluginVersionEnv); version != "" {
|
||||||
b.RunningVersion = version
|
b.RunningVersion = version
|
||||||
@@ -75,7 +81,7 @@ func Backend() *backend {
|
|||||||
type backend struct {
|
type backend struct {
|
||||||
*framework.Backend
|
*framework.Backend
|
||||||
|
|
||||||
// internal is used to test invalidate
|
// internal is used to test invalidate and reloads.
|
||||||
internal string
|
internal string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,3 +91,39 @@ func (b *backend) invalidate(ctx context.Context, key string) {
|
|||||||
b.internal = ""
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -466,14 +466,21 @@ func TestSystemBackend_Plugin_SealUnseal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSystemBackend_Plugin_reload(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 {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
backendType logical.BackendType
|
backendType logical.BackendType
|
||||||
|
path string
|
||||||
data map[string]interface{}
|
data map[string]interface{}
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "test plugin reload for type credential",
|
name: "test plugin reload for type credential",
|
||||||
backendType: logical.TypeCredential,
|
backendType: logical.TypeCredential,
|
||||||
|
path: reloadBackendPath,
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"plugin": "mock-plugin",
|
"plugin": "mock-plugin",
|
||||||
},
|
},
|
||||||
@@ -481,6 +488,7 @@ func TestSystemBackend_Plugin_reload(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "test mount reload for type credential",
|
name: "test mount reload for type credential",
|
||||||
backendType: logical.TypeCredential,
|
backendType: logical.TypeCredential,
|
||||||
|
path: reloadBackendPath,
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"mounts": "sys/auth/mock-0/,auth/mock-1/",
|
"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",
|
name: "test plugin reload for type secret",
|
||||||
backendType: logical.TypeLogical,
|
backendType: logical.TypeLogical,
|
||||||
|
path: reloadBackendPath,
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"plugin": "mock-plugin",
|
"plugin": "mock-plugin",
|
||||||
},
|
},
|
||||||
@@ -495,21 +504,38 @@ func TestSystemBackend_Plugin_reload(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "test mount reload for type secret",
|
name: "test mount reload for type secret",
|
||||||
backendType: logical.TypeLogical,
|
backendType: logical.TypeLogical,
|
||||||
|
path: reloadBackendPath,
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"mounts": "mock-0/,mock-1",
|
"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 {
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
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
|
// 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 {
|
testCases := []struct {
|
||||||
pluginVersion string
|
pluginVersion string
|
||||||
}{
|
}{
|
||||||
@@ -532,25 +558,25 @@ func testSystemBackend_PluginReload(t *testing.T, reqData map[string]interface{}
|
|||||||
core := cluster.Cores[0]
|
core := cluster.Cores[0]
|
||||||
client := core.Client
|
client := core.Client
|
||||||
|
|
||||||
pathPrefix := "mock-"
|
var mountPaths []string
|
||||||
if backendType == logical.TypeCredential {
|
if backendType == logical.TypeCredential {
|
||||||
pathPrefix = "auth/" + pathPrefix
|
mountPaths = []string{"auth/mock-0", "auth/mock-1"}
|
||||||
}
|
} else {
|
||||||
for i := 0; i < 2; i++ {
|
mountPaths = []string{"mock-0", "mock-1"}
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform plugin reload
|
for _, mountPath := range mountPaths {
|
||||||
resp, err := client.Logical().Write("sys/plugins/reload/backend", reqData)
|
// Update internal value in the backend
|
||||||
|
mock.WriteInternalValue(t, client, mountPath, "baz")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
@@ -564,18 +590,9 @@ func testSystemBackend_PluginReload(t *testing.T, reqData map[string]interface{}
|
|||||||
t.Fatal(resp.Warnings)
|
t.Fatal(resp.Warnings)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
// Ensure internal backed value is reset
|
||||||
// Ensure internal backed value is reset
|
for _, mountPath := range mountPaths {
|
||||||
resp, err := client.Logical().Read(fmt.Sprintf("%s%d/internal", pathPrefix, i))
|
mock.ExpectInternalValue(t, client, mountPath, mock.MockPluginDefaultInternalValue)
|
||||||
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'")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -643,7 +660,7 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo
|
|||||||
env := []string{pluginutil.PluginCACertPEMEnv + "=" + cluster.CACertPEMFile}
|
env := []string{pluginutil.PluginCACertPEMEnv + "=" + cluster.CACertPEMFile}
|
||||||
|
|
||||||
switch backendType {
|
switch backendType {
|
||||||
case logical.TypeLogical:
|
case logical.TypeLogical, logical.TypeUnknown:
|
||||||
plugin := logicalVersionMap[pluginVersion]
|
plugin := logicalVersionMap[pluginVersion]
|
||||||
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", plugin, env)
|
vault.TestAddTestPlugin(t, core.Core, "mock-plugin", consts.PluginTypeSecrets, "", plugin, env)
|
||||||
for i := 0; i < numMounts; i++ {
|
for i := 0; i < numMounts; i++ {
|
||||||
|
|||||||
@@ -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.pluginsCatalogListPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.pluginsCatalogCRUDPath())
|
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.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.pluginsRuntimesCatalogCRUDPath())
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.pluginsRuntimesCatalogListPaths()...)
|
b.Backend.Paths = append(b.Backend.Paths, b.pluginsRuntimesCatalogListPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.auditPaths()...)
|
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 != "" {
|
if pluginName != "" {
|
||||||
reloaded, err := b.Core.reloadMatchingPlugin(ctx, pluginName)
|
reloaded, err := b.Core.reloadMatchingPlugin(ctx, ns, consts.PluginTypeUnknown, pluginName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -757,14 +763,84 @@ func (b *SystemBackend) handlePluginReloadUpdate(ctx context.Context, req *logic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if len(pluginMounts) > 0 {
|
} else if len(pluginMounts) > 0 {
|
||||||
err := b.Core.reloadMatchingPluginMounts(ctx, pluginMounts)
|
err := b.Core.reloadMatchingPluginMounts(ctx, ns, pluginMounts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if scope == globalScope {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -6401,6 +6477,36 @@ This path responds to the following HTTP methods.
|
|||||||
`The mount paths of the plugin backends to reload.`,
|
`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": {
|
"hash": {
|
||||||
"Generate a hash sum for input data",
|
"Generate a hash sum for input data",
|
||||||
"Generates a hash sum of the given algorithm against the given input data.",
|
"Generates a hash sum of the given algorithm against the given input data.",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
"github.com/hashicorp/vault/helper/namespace"
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
"github.com/hashicorp/vault/sdk/framework"
|
"github.com/hashicorp/vault/sdk/framework"
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -263,7 +264,7 @@ var (
|
|||||||
|
|
||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
handleGlobalPluginReload = func(context.Context, *Core, string, string, []string) error {
|
handleGlobalPluginReload = func(context.Context, *Core, pluginReloadRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
handleSetupPluginReload = func(*Core) error {
|
handleSetupPluginReload = func(*Core) error {
|
||||||
@@ -277,6 +278,16 @@ var (
|
|||||||
checkRaw = func(b *SystemBackend, path string) error { return nil }
|
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
|
// 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 {
|
func (b *SystemBackend) tuneMountTTLs(ctx context.Context, path string, me *MountEntry, newDefault, newMax time.Duration) error {
|
||||||
zero := time.Duration(0)
|
zero := time.Duration(0)
|
||||||
|
|||||||
@@ -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/(?P<type>auth|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 {
|
func (b *SystemBackend) pluginsRuntimesCatalogCRUDPath() *framework.Path {
|
||||||
return &framework.Path{
|
return &framework.Path{
|
||||||
Pattern: "plugins/runtimes/catalog/(?P<type>container)/" + framework.GenericNameRegex("name"),
|
Pattern: "plugins/runtimes/catalog/(?P<type>container)/" + framework.GenericNameRegex("name"),
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ import (
|
|||||||
"github.com/hashicorp/vault/sdk/plugin"
|
"github.com/hashicorp/vault/sdk/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pluginReloadPluginsType = "plugins"
|
||||||
|
pluginReloadMountsType = "mounts"
|
||||||
|
)
|
||||||
|
|
||||||
// reloadMatchingPluginMounts reloads provided mounts, regardless of
|
// reloadMatchingPluginMounts reloads provided mounts, regardless of
|
||||||
// plugin name, as long as the backend type is plugin.
|
// 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()
|
c.mountsLock.RLock()
|
||||||
defer c.mountsLock.RUnlock()
|
defer c.mountsLock.RUnlock()
|
||||||
c.authLock.RLock()
|
c.authLock.RLock()
|
||||||
defer c.authLock.RUnlock()
|
defer c.authLock.RUnlock()
|
||||||
|
|
||||||
ns, err := namespace.FromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var errors error
|
var errors error
|
||||||
for _, mount := range mounts {
|
for _, mount := range mounts {
|
||||||
var isAuth bool
|
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
|
// reloadMatchingPlugin reloads all mounted backends that are named pluginName
|
||||||
// (name of the plugin as registered in the plugin catalog). It returns the
|
// (name of the plugin as registered in the plugin catalog). It returns the
|
||||||
// number of plugins that were reloaded and an error if any.
|
// number of plugins that were reloaded and an error if any.
|
||||||
func (c *Core) reloadMatchingPlugin(ctx context.Context, pluginName string) (reloaded int, err error) {
|
func (c *Core) reloadMatchingPlugin(ctx context.Context, ns *namespace.Namespace, pluginType consts.PluginType, pluginName string) (reloaded int, err error) {
|
||||||
c.mountsLock.RLock()
|
var secrets, auth, database bool
|
||||||
defer c.mountsLock.RUnlock()
|
switch pluginType {
|
||||||
c.authLock.RLock()
|
case consts.PluginTypeSecrets:
|
||||||
defer c.authLock.RUnlock()
|
secrets = true
|
||||||
|
case consts.PluginTypeCredential:
|
||||||
ns, err := namespace.FromContext(ctx)
|
auth = true
|
||||||
if err != nil {
|
case consts.PluginTypeDatabase:
|
||||||
return reloaded, err
|
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 {
|
if secrets || database {
|
||||||
// We dont reload mounts that are not in the same namespace
|
c.mountsLock.RLock()
|
||||||
if ns.ID != entry.Namespace().ID {
|
defer c.mountsLock.RUnlock()
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.Type == pluginName || (entry.Type == "plugin" && entry.Config.PluginName == pluginName) {
|
for _, entry := range c.mounts.Entries {
|
||||||
err := c.reloadBackendCommon(ctx, entry, false)
|
// We don't reload mounts that are not in the same namespace
|
||||||
if err != nil {
|
if ns != nil && ns.ID != entry.Namespace().ID {
|
||||||
return reloaded, err
|
continue
|
||||||
}
|
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if count, ok := resp.Data["count"].(int); ok && count > 0 {
|
if secrets && (entry.Type == pluginName || (entry.Type == "plugin" && entry.Config.PluginName == pluginName)) {
|
||||||
c.logger.Info("successfully reloaded database plugin(s)", "plugin", pluginName, "namespace", entry.Namespace(), "path", entry.Path, "connections", resp.Data["connections"])
|
err := c.reloadBackendCommon(ctx, entry, false)
|
||||||
reloaded += count
|
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 {
|
if auth {
|
||||||
// We dont reload mounts that are not in the same namespace
|
c.authLock.RLock()
|
||||||
if ns.ID != entry.Namespace().ID {
|
defer c.authLock.RUnlock()
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.Type == pluginName || (entry.Type == "plugin" && entry.Config.PluginName == pluginName) {
|
for _, entry := range c.auth.Entries {
|
||||||
err := c.reloadBackendCommon(ctx, entry, true)
|
// We don't reload mounts that are not in the same namespace
|
||||||
if err != nil {
|
if ns != nil && ns.ID != entry.Namespace().ID {
|
||||||
return reloaded, err
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
```
|
|
||||||
122
website/content/api-docs/system/plugins-reload.mdx
Normal file
122
website/content/api-docs/system/plugins-reload.mdx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
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: <required>)` – The type of the plugin, as registered in the
|
||||||
|
plugin catalog. One of "auth", "secret", "database", or "unknown". If "unknown",
|
||||||
|
all plugin types with the provided name will be reloaded.
|
||||||
|
|
||||||
|
- `name` `(string: <required>)` – 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.
|
||||||
@@ -9,14 +9,18 @@ description: |-
|
|||||||
|
|
||||||
The `plugin reload` command is used to reload mounted plugin backends. Either
|
The `plugin reload` command is used to reload mounted plugin backends. Either
|
||||||
the plugin name (`plugin`) or the desired plugin backend mounts (`mounts`)
|
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
|
## Examples
|
||||||
|
|
||||||
Reload a plugin by name:
|
Reload a plugin by type and name:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ vault plugin reload -plugin my-custom-plugin
|
$ vault plugin reload -type=auth -plugin my-custom-plugin
|
||||||
Success! Reloaded 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/]
|
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
|
## Usage
|
||||||
|
|
||||||
The following flags are available in addition to the [standard set of
|
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
|
- `-plugin` `(string: "")` - The name of the plugin to reload, as registered in
|
||||||
the plugin catalog.
|
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
|
- `-mounts` `(array: [])` - Array or comma-separated string mount paths of the
|
||||||
plugin backends to reload.
|
plugin backends to reload.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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.
|
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_registration]: /vault/docs/plugins/plugin-architecture#plugin-registration
|
||||||
[plugin_management]: /vault/docs/plugins/plugin-management#enabling-disabling-external-plugins
|
[plugin_management]: /vault/docs/plugins/plugin-management#enabling-disabling-external-plugins
|
||||||
|
|||||||
@@ -602,8 +602,8 @@
|
|||||||
"path": "system/namespaces"
|
"path": "system/namespaces"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "<code>/sys/plugins/reload/backend</code>",
|
"title": "<code>/sys/plugins/reload</code>",
|
||||||
"path": "system/plugins-reload-backend"
|
"path": "system/plugins-reload"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "<code>/sys/plugins/catalog</code>",
|
"title": "<code>/sys/plugins/catalog</code>",
|
||||||
|
|||||||
@@ -104,5 +104,10 @@ module.exports = [
|
|||||||
source: '/vault/docs/v1.13.x/agent-and-proxy/agent/apiproxy',
|
source: '/vault/docs/v1.13.x/agent-and-proxy/agent/apiproxy',
|
||||||
destination: '/vault/docs/v1.13.x/agent/apiproxy',
|
destination: '/vault/docs/v1.13.x/agent/apiproxy',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/vault/api-docs/system/plugins-reload-backend',
|
||||||
|
destination: '/vault/api-docs/system/plugins-reload',
|
||||||
|
permanent: true,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user