mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 09:42:25 +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
|
||||
// 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())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
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,
|
||||
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
|
||||
|
||||
@@ -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,15 @@ 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. If not provided, all plugins with a matching name will be reloaded.",
|
||||
})
|
||||
|
||||
return set
|
||||
@@ -103,6 +120,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 +132,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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
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 (
|
||||
"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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
// 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)
|
||||
}
|
||||
mountPaths = []string{"auth/mock-0", "auth/mock-1"}
|
||||
} else {
|
||||
mountPaths = []string{"mock-0", "mock-1"}
|
||||
}
|
||||
|
||||
// Perform plugin reload
|
||||
resp, err := client.Logical().Write("sys/plugins/reload/backend", reqData)
|
||||
for _, mountPath := range mountPaths {
|
||||
// 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 {
|
||||
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++ {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -6401,6 +6477,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.",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
return &framework.Path{
|
||||
Pattern: "plugins/runtimes/catalog/(?P<type>container)/" + framework.GenericNameRegex("name"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -602,8 +602,8 @@
|
||||
"path": "system/namespaces"
|
||||
},
|
||||
{
|
||||
"title": "<code>/sys/plugins/reload/backend</code>",
|
||||
"path": "system/plugins-reload-backend"
|
||||
"title": "<code>/sys/plugins/reload</code>",
|
||||
"path": "system/plugins-reload"
|
||||
},
|
||||
{
|
||||
"title": "<code>/sys/plugins/catalog</code>",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user