Skip to content

Commit abac619

Browse files
helenfufuschavis
andauthored
Vault 36295 Improve plugin mgmt ux in api and cli (#30811)
* cli: only set default command parameter to plugin name if sha256 is provided * api: write warnings to RegisterPluginResponse, propagate up to cli * api: filter out 'Endpoint replaced the value of these parameters' warning before returning in RegisterPluginWithContext * docs * add TODO on filtering that links to api type parameter deprecation ticket * fix tests * allocate filteredWarning slice only if there are warnings * improve deferred resp close and early error return conditionals in RegisterPluginWithContext * refer to sha256 as cli option -sha256 in command cli usage * break up ui error lines for sha256 and version flag check * consolidate if statements for sha256 and command, oci_image check in cli * consolidate if statements for sha256 and command, oci_image check in api * new RegisterPluginV2 and RegisterPluginWithContextV2 api client functions for backward compatibility * add changelog * more descriptive changelog * rename RegisterPluginV2 to RegisterPluginDetailed and RegisterPluginWithContextV2 to RegisterPluginWithContextDetailed * return nil, nil if no warnings to preserve status code * fix eof from decoding (check if no content before decoding) * doc for RegisterPluginResponse * only validate plugin.Command in plugin catalog set for downloaded and binary plugins, which rely on plugin.Command input; extracted artifact plugins don't rely on plugin.Command input * Update website/content/api-docs/system/plugins-catalog.mdx Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> * Update website/content/api-docs/system/plugins-catalog.mdx Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> * Update website/content/api-docs/system/plugins-catalog.mdx Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> * Update website/content/docs/commands/plugin/register.mdx Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> * Update website/content/docs/commands/plugin/register.mdx Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> * Update website/content/docs/commands/plugin/register.mdx Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> * Update website/content/docs/commands/plugin/register.mdx Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> * move up enterprise note on plugin register command doc * [DOCS] Editorial suggestions for PR #30811 (#31111) * suggestions * move common reqs to a partial * fix typo * tweak reqs * Update website/content/partials/plugins/prepare-plugin.mdx Co-authored-by: helenfufu <25168806+helenfufu@users.noreply.github.com> * Update website/content/partials/plugins/prepare-plugin.mdx Co-authored-by: helenfufu <25168806+helenfufu@users.noreply.github.com> * Update website/content/partials/plugins/prepare-plugin.mdx Co-authored-by: helenfufu <25168806+helenfufu@users.noreply.github.com> * tweak feedback * remove deprecation * Update website/content/partials/plugins/common-requirements.mdx Co-authored-by: helenfufu <25168806+helenfufu@users.noreply.github.com> * save * Update website/content/docs/plugins/rollback.mdx Co-authored-by: helenfufu <25168806+helenfufu@users.noreply.github.com> * Update website/content/docs/plugins/upgrade.mdx Co-authored-by: helenfufu <25168806+helenfufu@users.noreply.github.com> * fix formatting --------- Co-authored-by: helenfufu <25168806+helenfufu@users.noreply.github.com> --------- Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com>
1 parent d86521c commit abac619

19 files changed

Lines changed: 372 additions & 104 deletions

File tree

api/sys_plugins.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"fmt"
1010
"net/http"
11+
"strings"
1112
"time"
1213

1314
"github.com/mitchellh/mapstructure"
@@ -220,12 +221,19 @@ type RegisterPluginInput struct {
220221
Download bool `json:"download,omitempty"`
221222
}
222223

224+
// RegisterPluginResponse is the response from the RegisterPluginDetailed call.
225+
type RegisterPluginResponse struct {
226+
Warnings []string `json:"warnings"`
227+
}
228+
223229
// RegisterPlugin wraps RegisterPluginWithContext using context.Background.
230+
// Deprecated: Use RegisterPluginDetailed instead.
224231
func (c *Sys) RegisterPlugin(i *RegisterPluginInput) error {
225232
return c.RegisterPluginWithContext(context.Background(), i)
226233
}
227234

228235
// RegisterPluginWithContext registers the plugin with the given information.
236+
// Deprecated: Use RegisterPluginWithContextDetailed instead.
229237
func (c *Sys) RegisterPluginWithContext(ctx context.Context, i *RegisterPluginInput) error {
230238
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
231239
defer cancelFunc()
@@ -244,6 +252,58 @@ func (c *Sys) RegisterPluginWithContext(ctx context.Context, i *RegisterPluginIn
244252
return err
245253
}
246254

255+
// RegisterPluginDetailed wraps RegisterPluginWtihContextDetailed using context.Background.
256+
func (c *Sys) RegisterPluginDetailed(i *RegisterPluginInput) (*RegisterPluginResponse, error) {
257+
return c.RegisterPluginWithContextDetailed(context.Background(), i)
258+
}
259+
260+
// RegisterPluginWithContextDetailed registers the plugin with the given information.
261+
func (c *Sys) RegisterPluginWithContextDetailed(ctx context.Context, i *RegisterPluginInput) (*RegisterPluginResponse, error) {
262+
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
263+
defer cancelFunc()
264+
265+
path := catalogPathByType(i.Type, i.Name)
266+
req := c.c.NewRequest(http.MethodPut, path)
267+
268+
if err := req.SetJSONBody(i); err != nil {
269+
return nil, err
270+
}
271+
272+
resp, err := c.c.rawRequestWithContext(ctx, req)
273+
if resp != nil {
274+
defer resp.Body.Close()
275+
}
276+
if err != nil {
277+
return nil, err
278+
}
279+
280+
var registerResp RegisterPluginResponse
281+
if resp != nil && resp.StatusCode != http.StatusNoContent {
282+
if err := resp.DecodeJSON(&registerResp); err != nil {
283+
return nil, err
284+
}
285+
}
286+
287+
// Filter out the `Endpoint replaced the value of these parameters with the values captured from the endpoint's path: [type]`
288+
// warning because it is expected behavior from this function, as we set the type parameter in both the path and request body,
289+
// and the warning informs us the path parameter takes precedence. However, this warning is not relevant for an end user so we
290+
// omit it before returning to any client.
291+
// TODO: This can likely be removed once https://hashicorp.atlassian.net/browse/VAULT-36722 is addressed.
292+
var filteredWarnings []string
293+
if len(registerResp.Warnings) > 0 {
294+
filteredWarnings = make([]string, 0, len(registerResp.Warnings))
295+
}
296+
297+
for _, warning := range registerResp.Warnings {
298+
if !strings.Contains(warning, "Endpoint replaced the value of these parameters with the values captured from the endpoint's path") {
299+
filteredWarnings = append(filteredWarnings, warning)
300+
}
301+
}
302+
registerResp.Warnings = filteredWarnings
303+
304+
return &registerResp, err
305+
}
306+
247307
// DeregisterPluginInput is used as input to the DeregisterPlugin function.
248308
type DeregisterPluginInput struct {
249309
// Name is the name of the plugin. Required.

api/sys_plugins_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ func TestRegisterPlugin(t *testing.T) {
2424
t.Fatal(err)
2525
}
2626

27-
err = client.Sys().RegisterPluginWithContext(context.Background(), &RegisterPluginInput{
27+
resp, err := client.Sys().RegisterPluginWithContextDetailed(context.Background(), &RegisterPluginInput{
2828
Version: "v1.0.0",
2929
})
3030
if err != nil {
3131
t.Fatal(err)
3232
}
33+
if len(resp.Warnings) > 0 {
34+
t.Errorf("expected no warnings, got: %v", resp.Warnings)
35+
}
3336
}
3437

3538
func TestListPlugins(t *testing.T) {

changelog/30811.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
plugins: Clarify usage of sha256, command, and version for plugin registration of binary or artifact with API and CLI. Introduce new RegisterPluginDetailed and RegisterPluginWtihContextDetailed functions to API client to propagate response along with error, and mark RegisterPlugin and RegisterPluginWithContext as deprecated.
3+
```

command/plugin_deregister_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,18 @@ func TestPluginDeregisterCommand_Run(t *testing.T) {
9191
ui, cmd := testPluginDeregisterCommand(t)
9292
cmd.client = client
9393

94-
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
94+
registerResp, err := client.Sys().RegisterPluginDetailed(&api.RegisterPluginInput{
9595
Name: pluginName,
9696
Type: api.PluginTypeCredential,
9797
Command: pluginName,
9898
SHA256: sha256Sum,
99-
}); err != nil {
99+
})
100+
if err != nil {
100101
t.Fatal(err)
101102
}
103+
if len(registerResp.Warnings) > 0 {
104+
t.Errorf("expected no warnings, got %q", registerResp.Warnings)
105+
}
102106

103107
code := cmd.Run([]string{
104108
consts.PluginTypeCredential.String(),
@@ -114,23 +118,23 @@ func TestPluginDeregisterCommand_Run(t *testing.T) {
114118
t.Errorf("expected %q to contain %q", combined, expected)
115119
}
116120

117-
resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{
121+
listResp, err := client.Sys().ListPlugins(&api.ListPluginsInput{
118122
Type: api.PluginTypeCredential,
119123
})
120124
if err != nil {
121125
t.Fatal(err)
122126
}
123127

124128
found := false
125-
for _, plugins := range resp.PluginsByType {
129+
for _, plugins := range listResp.PluginsByType {
126130
for _, p := range plugins {
127131
if p == pluginName {
128132
found = true
129133
}
130134
}
131135
}
132136
if found {
133-
t.Errorf("expected %q to not be in %q", pluginName, resp.PluginsByType)
137+
t.Errorf("expected %q to not be in %q", pluginName, listResp.PluginsByType)
134138
}
135139
})
136140

command/plugin_register.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,27 @@ func (c *PluginRegisterCommand) Flags() *FlagSets {
8181
Name: "command",
8282
Target: &c.flagCommand,
8383
Completion: complete.PredictAnything,
84-
Usage: "Command to spawn the plugin. This defaults to the name of the " +
85-
"plugin if both oci_image and command are unspecified.",
84+
Usage: "Command to spawn the plugin. If -sha256 is provided to register with a plugin binary, " +
85+
"this defaults to the name of the plugin if both oci_image and command are unspecified. " +
86+
"Otherwise, if -sha256 is not provided, a plugin artifact is expected for registration, and " +
87+
"this will be ignored because the run command is known.",
8688
})
8789

8890
f.StringVar(&StringVar{
8991
Name: "sha256",
9092
Target: &c.flagSHA256,
9193
Completion: complete.PredictAnything,
92-
Usage: "SHA256 of the plugin binary or the oci_image provided. This is required for all plugins.",
94+
Usage: "SHA256 of the plugin binary or the OCI image provided. " +
95+
"This is required to register with a plugin binary but should not be " +
96+
"specified when registering with a plugin artifact.",
9397
})
9498

9599
f.StringVar(&StringVar{
96100
Name: "version",
97101
Target: &c.flagVersion,
98102
Completion: complete.PredictAnything,
99-
Usage: "Semantic version of the plugin. Used as the tag when specifying oci_image, but with any leading 'v' trimmed. Optional.",
103+
Usage: "Semantic version of the plugin. Used as the tag when specifying oci_image, but with any leading 'v' trimmed. " +
104+
"This is required to register with a plugin artifact but optional when registering with a plugin binary.",
100105
})
101106

102107
f.StringVar(&StringVar{
@@ -151,7 +156,9 @@ func (c *PluginRegisterCommand) Run(args []string) int {
151156
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1 or 2, got %d)", len(args)))
152157
return 1
153158
case c.flagSHA256 == "" && c.flagVersion == "":
154-
c.UI.Error("One of -sha256 or -version is required. If registering with binary, please provide at least -sha256 (-version optional). If registering with extracted artifact directory, please provide -version only.")
159+
c.UI.Error("One of -sha256 or -version is required. " +
160+
"If registering with a binary, please provide at least -sha256 (-version optional)." +
161+
"If registering with an artifact, please provide -version only.")
155162
return 1
156163

157164
// These cases should come after invalid cases have been checked
@@ -177,11 +184,11 @@ func (c *PluginRegisterCommand) Run(args []string) int {
177184
pluginName := strings.TrimSpace(pluginNameRaw)
178185

179186
command := c.flagCommand
180-
if command == "" && c.flagOCIImage == "" {
187+
if c.flagSHA256 != "" && (command == "" && c.flagOCIImage == "") {
181188
command = pluginName
182189
}
183190

184-
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
191+
resp, err := client.Sys().RegisterPluginDetailed(&api.RegisterPluginInput{
185192
Name: pluginName,
186193
Type: pluginType,
187194
Args: c.flagArgs,
@@ -191,11 +198,20 @@ func (c *PluginRegisterCommand) Run(args []string) int {
191198
OCIImage: c.flagOCIImage,
192199
Runtime: c.flagRuntime,
193200
Env: c.flagEnv,
194-
}); err != nil {
201+
})
202+
if err != nil {
195203
c.UI.Error(fmt.Sprintf("Error registering plugin %s: %s", pluginName, err))
196204
return 2
197205
}
198206

207+
if resp != nil && len(resp.Warnings) > 0 {
208+
c.UI.Warn(wrapAtLength(fmt.Sprintf(
209+
"Warnings while registering plugin %s: %s",
210+
pluginName,
211+
strings.Join(resp.Warnings, "\n\n"),
212+
)) + "\n")
213+
}
214+
199215
c.UI.Output(fmt.Sprintf("Success! Registered plugin: %s", pluginName))
200216
return 0
201217
}

command/plugin_reload_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,18 @@ func TestPluginReloadCommand_Run(t *testing.T) {
108108
ui, cmd := testPluginReloadCommand(t)
109109
cmd.client = client
110110

111-
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
111+
resp, err := client.Sys().RegisterPluginDetailed(&api.RegisterPluginInput{
112112
Name: pluginName,
113113
Type: api.PluginTypeCredential,
114114
Command: pluginName,
115115
SHA256: sha256Sum,
116-
}); err != nil {
116+
})
117+
if err != nil {
117118
t.Fatal(err)
118119
}
120+
if len(resp.Warnings) > 0 {
121+
t.Errorf("expected no warnings, got: %v", resp.Warnings)
122+
}
119123

120124
code := cmd.Run([]string{
121125
"-plugin", pluginName,

command/plugin_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,19 @@ func testPluginCreateAndRegister(tb testing.TB, client *api.Client, dir, name st
4545

4646
pth, sha256Sum := testPluginCreate(tb, dir, name)
4747

48-
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
48+
resp, err := client.Sys().RegisterPluginDetailed(&api.RegisterPluginInput{
4949
Name: name,
5050
Type: pluginType,
5151
Command: name,
5252
SHA256: sha256Sum,
5353
Version: version,
54-
}); err != nil {
54+
})
55+
if err != nil {
5556
tb.Fatal(err)
5657
}
58+
if len(resp.Warnings) > 0 {
59+
tb.Errorf("expected no warnings, got: %v", resp.Warnings)
60+
}
5761

5862
return pth, sha256Sum
5963
}
@@ -64,15 +68,19 @@ func testPluginCreateAndRegisterVersioned(tb testing.TB, client *api.Client, dir
6468

6569
pth, sha256Sum := testPluginCreate(tb, dir, name)
6670

67-
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
71+
resp, err := client.Sys().RegisterPluginDetailed(&api.RegisterPluginInput{
6872
Name: name,
6973
Type: pluginType,
7074
Command: name,
7175
SHA256: sha256Sum,
7276
Version: "v1.0.0",
73-
}); err != nil {
77+
})
78+
if err != nil {
7479
tb.Fatal(err)
7580
}
81+
if len(resp.Warnings) > 0 {
82+
tb.Errorf("expected no warnings, got: %v", resp.Warnings)
83+
}
7684

7785
return pth, sha256Sum, "v1.0.0"
7886
}

command/util.go

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

66
import (
7+
"bytes"
78
"fmt"
89
"io"
910
"net/http"
@@ -193,6 +194,7 @@ func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, er
193194
r.body = body
194195
return &http.Response{
195196
StatusCode: 200,
197+
Body: io.NopCloser(bytes.NewReader([]byte(`{"warnings": []}`))),
196198
}, nil
197199
}
198200

vault/external_tests/plugin/external_plugin_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,19 @@ func TestExternalPlugin_RollbackAndReload(t *testing.T) {
106106

107107
func testRegisterVersion(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin, version string) {
108108
t.Helper()
109-
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
109+
resp, err := client.Sys().RegisterPluginDetailed(&api.RegisterPluginInput{
110110
Name: plugin.Name,
111111
Type: api.PluginType(plugin.Typ),
112112
Command: plugin.Name,
113113
SHA256: plugin.Sha256,
114114
Version: version,
115-
}); err != nil {
115+
})
116+
if err != nil {
116117
t.Fatal(err)
117118
}
119+
if len(resp.Warnings) > 0 {
120+
t.Errorf("expected no warnings, got: %v", resp.Warnings)
121+
}
118122
}
119123

120124
func testEnableVersion(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin, version string) {

vault/logical_system.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,11 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, _ *logica
553553

554554
command := d.Get("command").(string)
555555
ociImage := d.Get("oci_image").(string)
556-
if command == "" && ociImage == "" {
556+
var resp logical.Response
557+
558+
if sha256 == "" && command != "" {
559+
resp.AddWarning(fmt.Sprintf("When sha256 is unspecified, a plugin artifact is expected for registration and the command parameter %q will be ignored.", command))
560+
} else if sha256 != "" && (command == "" && ociImage == "") {
557561
return logical.ErrorResponse("must provide at least one of command or oci_image"), nil
558562
}
559563

@@ -619,7 +623,11 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, _ *logica
619623
return nil, err
620624
}
621625

622-
return nil, nil
626+
if len(resp.Warnings) == 0 {
627+
return nil, nil
628+
}
629+
630+
return &resp, nil
623631
}
624632

625633
func (b *SystemBackend) handlePluginCatalogRead(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {

0 commit comments

Comments
 (0)