Skip to content

Commit 80f85a0

Browse files
authored
New root namespace plugin reload API sys/plugins/reload/:type/:name (#24878)
1 parent cadef7b commit 80f85a0

File tree

21 files changed

+873
-175
lines changed

21 files changed

+873
-175
lines changed

api/plugin_types.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ package api
77
// https://github.com/hashicorp/vault/blob/main/sdk/helper/consts/plugin_types.go
88
// Any changes made should be made to both files at the same time.
99

10-
import "fmt"
10+
import (
11+
"encoding/json"
12+
"fmt"
13+
)
1114

1215
var PluginTypes = []PluginType{
1316
PluginTypeUnknown,
@@ -64,3 +67,34 @@ func ParsePluginType(pluginType string) (PluginType, error) {
6467
return PluginTypeUnknown, fmt.Errorf("%q is not a supported plugin type", pluginType)
6568
}
6669
}
70+
71+
// UnmarshalJSON implements json.Unmarshaler. It supports unmarshaling either a
72+
// string or a uint32. All new serialization will be as a string, but we
73+
// previously serialized as a uint32 so we need to support that for backwards
74+
// compatibility.
75+
func (p *PluginType) UnmarshalJSON(data []byte) error {
76+
var asString string
77+
err := json.Unmarshal(data, &asString)
78+
if err == nil {
79+
*p, err = ParsePluginType(asString)
80+
return err
81+
}
82+
83+
var asUint32 uint32
84+
err = json.Unmarshal(data, &asUint32)
85+
if err != nil {
86+
return err
87+
}
88+
*p = PluginType(asUint32)
89+
switch *p {
90+
case PluginTypeUnknown, PluginTypeCredential, PluginTypeDatabase, PluginTypeSecrets:
91+
return nil
92+
default:
93+
return fmt.Errorf("%d is not a supported plugin type", asUint32)
94+
}
95+
}
96+
97+
// MarshalJSON implements json.Marshaler.
98+
func (p PluginType) MarshalJSON() ([]byte, error) {
99+
return json.Marshal(p.String())
100+
}

api/plugin_types_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package api
5+
6+
// NOTE: this file was copied from
7+
// https://github.com/hashicorp/vault/blob/main/sdk/helper/consts/plugin_types_test.go
8+
// Any changes made should be made to both files at the same time.
9+
10+
import (
11+
"encoding/json"
12+
"testing"
13+
)
14+
15+
type testType struct {
16+
PluginType PluginType `json:"plugin_type"`
17+
}
18+
19+
func TestPluginTypeJSONRoundTrip(t *testing.T) {
20+
for _, pluginType := range PluginTypes {
21+
original := testType{
22+
PluginType: pluginType,
23+
}
24+
asBytes, err := json.Marshal(original)
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
29+
var roundTripped testType
30+
err = json.Unmarshal(asBytes, &roundTripped)
31+
if err != nil {
32+
t.Fatal(err)
33+
}
34+
35+
if original != roundTripped {
36+
t.Fatalf("expected %v, got %v", original, roundTripped)
37+
}
38+
}
39+
}
40+
41+
func TestPluginTypeJSONUnmarshal(t *testing.T) {
42+
// Failure/unsupported cases.
43+
for name, tc := range map[string]string{
44+
"unsupported": `{"plugin_type":"unsupported"}`,
45+
"random string": `{"plugin_type":"foo"}`,
46+
"boolean": `{"plugin_type":true}`,
47+
"empty": `{"plugin_type":""}`,
48+
"negative": `{"plugin_type":-1}`,
49+
"out of range": `{"plugin_type":10}`,
50+
} {
51+
t.Run(name, func(t *testing.T) {
52+
var result testType
53+
err := json.Unmarshal([]byte(tc), &result)
54+
if err == nil {
55+
t.Fatal("expected error")
56+
}
57+
})
58+
}
59+
60+
// Valid cases.
61+
for name, tc := range map[string]struct {
62+
json string
63+
expected PluginType
64+
}{
65+
"unknown": {`{"plugin_type":"unknown"}`, PluginTypeUnknown},
66+
"auth": {`{"plugin_type":"auth"}`, PluginTypeCredential},
67+
"secret": {`{"plugin_type":"secret"}`, PluginTypeSecrets},
68+
"database": {`{"plugin_type":"database"}`, PluginTypeDatabase},
69+
"absent": {`{}`, PluginTypeUnknown},
70+
"integer unknown": {`{"plugin_type":0}`, PluginTypeUnknown},
71+
"integer auth": {`{"plugin_type":1}`, PluginTypeCredential},
72+
"integer db": {`{"plugin_type":2}`, PluginTypeDatabase},
73+
"integer secret": {`{"plugin_type":3}`, PluginTypeSecrets},
74+
} {
75+
t.Run(name, func(t *testing.T) {
76+
var result testType
77+
err := json.Unmarshal([]byte(tc.json), &result)
78+
if err != nil {
79+
t.Fatal(err)
80+
}
81+
if tc.expected != result.PluginType {
82+
t.Fatalf("expected %v, got %v", tc.expected, result.PluginType)
83+
}
84+
})
85+
}
86+
}
87+
88+
func TestUnknownTypeExcludedWithOmitEmpty(t *testing.T) {
89+
type testTypeOmitEmpty struct {
90+
Type PluginType `json:"type,omitempty"`
91+
}
92+
bytes, err := json.Marshal(testTypeOmitEmpty{})
93+
if err != nil {
94+
t.Fatal(err)
95+
}
96+
m := map[string]any{}
97+
json.Unmarshal(bytes, &m)
98+
if _, exists := m["type"]; exists {
99+
t.Fatal("type should not be present")
100+
}
101+
}

api/sys_plugins.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,22 @@ func (c *Sys) DeregisterPluginWithContext(ctx context.Context, i *DeregisterPlug
274274
return err
275275
}
276276

277+
// RootReloadPluginInput is used as input to the RootReloadPlugin function.
278+
type RootReloadPluginInput struct {
279+
Plugin string `json:"-"` // Plugin name, as registered in the plugin catalog.
280+
Type PluginType `json:"-"` // Plugin type: auth, secret, or database.
281+
Scope string `json:"scope,omitempty"` // Empty to reload on current node, "global" for all nodes.
282+
}
283+
284+
// RootReloadPlugin reloads plugins, possibly returning reloadID for a global
285+
// scoped reload. This is only available in the root namespace, and reloads
286+
// plugins across all namespaces, whereas ReloadPlugin is available in all
287+
// namespaces but only reloads plugins in use in the request's namespace.
288+
func (c *Sys) RootReloadPlugin(ctx context.Context, i *RootReloadPluginInput) (string, error) {
289+
path := fmt.Sprintf("/v1/sys/plugins/reload/%s/%s", i.Type.String(), i.Plugin)
290+
return c.reloadPluginInternal(ctx, path, i, i.Scope == "global")
291+
}
292+
277293
// ReloadPluginInput is used as input to the ReloadPlugin function.
278294
type ReloadPluginInput struct {
279295
// 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) {
292308
}
293309

294310
// ReloadPluginWithContext reloads mounted plugin backends, possibly returning
295-
// reloadId for a cluster scoped reload
311+
// reloadID for a cluster scoped reload. It is limited to reloading plugins that
312+
// are in use in the request's namespace. See RootReloadPlugin for an API that
313+
// can reload plugins across all namespaces.
296314
func (c *Sys) ReloadPluginWithContext(ctx context.Context, i *ReloadPluginInput) (string, error) {
315+
return c.reloadPluginInternal(ctx, "/v1/sys/plugins/reload/backend", i, i.Scope == "global")
316+
}
317+
318+
func (c *Sys) reloadPluginInternal(ctx context.Context, path string, body any, global bool) (string, error) {
297319
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
298320
defer cancelFunc()
299321

300-
path := "/v1/sys/plugins/reload/backend"
301322
req := c.c.NewRequest(http.MethodPut, path)
302323

303-
if err := req.SetJSONBody(i); err != nil {
324+
if err := req.SetJSONBody(body); err != nil {
304325
return "", err
305326
}
306327

@@ -310,7 +331,7 @@ func (c *Sys) ReloadPluginWithContext(ctx context.Context, i *ReloadPluginInput)
310331
}
311332
defer resp.Body.Close()
312333

313-
if i.Scope == "global" {
334+
if global {
314335
// Get the reload id
315336
secret, parseErr := ParseSecret(resp.Body)
316337
if parseErr != nil {

changelog/24878.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
```release-note:improvement
2+
plugins: New API `sys/plugins/reload/:type/:name` available in the root namespace for reloading a specific plugin across all namespaces.
3+
```
4+
```release-note:change
5+
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.
6+
```

command/plugin_register_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ func TestFlagParsing(t *testing.T) {
249249
pluginType: api.PluginTypeUnknown,
250250
name: "foo",
251251
sha256: "abc123",
252-
expectedPayload: `{"type":0,"command":"foo","sha256":"abc123"}`,
252+
expectedPayload: `{"type":"unknown","command":"foo","sha256":"abc123"}`,
253253
},
254254
"full": {
255255
pluginType: api.PluginTypeCredential,
@@ -261,14 +261,14 @@ func TestFlagParsing(t *testing.T) {
261261
sha256: "abc123",
262262
args: []string{"--a=b", "--b=c", "positional"},
263263
env: []string{"x=1", "y=2"},
264-
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"]}`,
264+
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"]}`,
265265
},
266266
"command remains empty if oci_image specified": {
267267
pluginType: api.PluginTypeCredential,
268268
name: "name",
269269
ociImage: "image",
270270
sha256: "abc123",
271-
expectedPayload: `{"type":1,"sha256":"abc123","oci_image":"image"}`,
271+
expectedPayload: `{"type":"auth","sha256":"abc123","oci_image":"image"}`,
272272
},
273273
} {
274274
tc := tc

command/plugin_reload.go

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package command
55

66
import (
7+
"context"
78
"fmt"
89
"strings"
910

@@ -19,9 +20,10 @@ var (
1920

2021
type PluginReloadCommand struct {
2122
*BaseCommand
22-
plugin string
23-
mounts []string
24-
scope string
23+
plugin string
24+
mounts []string
25+
scope string
26+
pluginType string
2527
}
2628

2729
func (c *PluginReloadCommand) Synopsis() string {
@@ -36,9 +38,16 @@ Usage: vault plugin reload [options]
3638
mount(s) must be provided, but not both. In case the plugin name is provided,
3739
all of its corresponding mounted paths that use the plugin backend will be reloaded.
3840
39-
Reload the plugin named "my-custom-plugin":
41+
If run with a Vault namespace other than the root namespace, only plugins
42+
running in the same namespace will be reloaded.
4043
41-
$ vault plugin reload -plugin=my-custom-plugin
44+
Reload the secret plugin named "my-custom-plugin" on the current node:
45+
46+
$ vault plugin reload -type=secret -plugin=my-custom-plugin
47+
48+
Reload the secret plugin named "my-custom-plugin" across all nodes and replicated clusters:
49+
50+
$ vault plugin reload -type=secret -plugin=my-custom-plugin -scope=global
4251
4352
` + c.Flags().Help()
4453

@@ -68,7 +77,15 @@ func (c *PluginReloadCommand) Flags() *FlagSets {
6877
Name: "scope",
6978
Target: &c.scope,
7079
Completion: complete.PredictAnything,
71-
Usage: "The scope of the reload, omitted for local, 'global', for replicated reloads",
80+
Usage: "The scope of the reload, omitted for local, 'global', for replicated reloads.",
81+
})
82+
83+
f.StringVar(&StringVar{
84+
Name: "type",
85+
Target: &c.pluginType,
86+
Completion: complete.PredictAnything,
87+
Usage: "The type of plugin to reload, one of auth, secret, or database. Mutually " +
88+
"exclusive with -mounts. If not provided, all plugins with a matching name will be reloaded.",
7289
})
7390

7491
return set
@@ -103,6 +120,10 @@ func (c *PluginReloadCommand) Run(args []string) int {
103120
return 1
104121
case c.scope != "" && c.scope != "global":
105122
c.UI.Error(fmt.Sprintf("Invalid reload scope: %s", c.scope))
123+
return 1
124+
case len(c.mounts) > 0 && c.pluginType != "":
125+
c.UI.Error("Cannot specify -type with -mounts")
126+
return 1
106127
}
107128

108129
client, err := c.Client()
@@ -111,25 +132,46 @@ func (c *PluginReloadCommand) Run(args []string) int {
111132
return 2
112133
}
113134

114-
rid, err := client.Sys().ReloadPlugin(&api.ReloadPluginInput{
115-
Plugin: c.plugin,
116-
Mounts: c.mounts,
117-
Scope: c.scope,
118-
})
135+
var reloadID string
136+
if client.Namespace() == "" {
137+
pluginType := api.PluginTypeUnknown
138+
pluginTypeStr := strings.TrimSpace(c.pluginType)
139+
if pluginTypeStr != "" {
140+
var err error
141+
pluginType, err = api.ParsePluginType(pluginTypeStr)
142+
if err != nil {
143+
c.UI.Error(fmt.Sprintf("Error parsing -type as a plugin type, must be unset or one of auth, secret, or database: %s", err))
144+
return 1
145+
}
146+
}
147+
148+
reloadID, err = client.Sys().RootReloadPlugin(context.Background(), &api.RootReloadPluginInput{
149+
Plugin: c.plugin,
150+
Type: pluginType,
151+
Scope: c.scope,
152+
})
153+
} else {
154+
reloadID, err = client.Sys().ReloadPlugin(&api.ReloadPluginInput{
155+
Plugin: c.plugin,
156+
Mounts: c.mounts,
157+
Scope: c.scope,
158+
})
159+
}
160+
119161
if err != nil {
120162
c.UI.Error(fmt.Sprintf("Error reloading plugin/mounts: %s", err))
121163
return 2
122164
}
123165

124166
if len(c.mounts) > 0 {
125-
if rid != "" {
126-
c.UI.Output(fmt.Sprintf("Success! Reloading mounts: %s, reload_id: %s", c.mounts, rid))
167+
if reloadID != "" {
168+
c.UI.Output(fmt.Sprintf("Success! Reloading mounts: %s, reload_id: %s", c.mounts, reloadID))
127169
} else {
128170
c.UI.Output(fmt.Sprintf("Success! Reloaded mounts: %s", c.mounts))
129171
}
130172
} else {
131-
if rid != "" {
132-
c.UI.Output(fmt.Sprintf("Success! Reloading plugin: %s, reload_id: %s", c.plugin, rid))
173+
if reloadID != "" {
174+
c.UI.Output(fmt.Sprintf("Success! Reloading plugin: %s, reload_id: %s", c.plugin, reloadID))
133175
} else {
134176
c.UI.Output(fmt.Sprintf("Success! Reloaded plugin: %s", c.plugin))
135177
}

command/plugin_reload_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ func TestPluginReloadCommand_Run(t *testing.T) {
5555
"Must specify exactly one of -plugin or -mounts",
5656
1,
5757
},
58+
{
59+
"type_and_mounts_mutually_exclusive",
60+
[]string{"-mounts", "bar", "-type", "secret"},
61+
"Cannot specify -type with -mounts",
62+
1,
63+
},
64+
{
65+
"invalid_type",
66+
[]string{"-plugin", "bar", "-type", "unsupported"},
67+
"Error parsing -type as a plugin type",
68+
1,
69+
},
5870
}
5971

6072
for _, tc := range cases {

0 commit comments

Comments
 (0)