Skip to content

Vault 36295 Improve plugin mgmt ux in api and cli #30811

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
16780f4
cli: only set default command parameter to plugin name if sha256 is p…
helenfufu May 29, 2025
0bf96d2
api: write warnings to RegisterPluginResponse, propagate up to cli
helenfufu May 29, 2025
3cbd9d1
api: filter out 'Endpoint replaced the value of these parameters' war…
helenfufu May 30, 2025
3ab4609
docs
helenfufu May 30, 2025
7a08810
add TODO on filtering that links to api type parameter deprecation ti…
helenfufu Jun 2, 2025
76b9dec
fix tests
helenfufu Jun 4, 2025
a9ec8c2
Merge branch 'main' into vault-36295-improve-plugin-mgmt-ux-in-api-an…
helenfufu Jun 4, 2025
814a0d1
allocate filteredWarning slice only if there are warnings
helenfufu Jun 4, 2025
04c9bb8
improve deferred resp close and early error return conditionals in Re…
helenfufu Jun 4, 2025
512b038
refer to sha256 as cli option -sha256 in command cli usage
helenfufu Jun 4, 2025
318bc9a
break up ui error lines for sha256 and version flag check
helenfufu Jun 4, 2025
20b2618
consolidate if statements for sha256 and command, oci_image check in cli
helenfufu Jun 4, 2025
d021fd1
consolidate if statements for sha256 and command, oci_image check in api
helenfufu Jun 4, 2025
dd91232
Merge branch 'main' into vault-36295-improve-plugin-mgmt-ux-in-api-an…
helenfufu Jun 19, 2025
ee7d570
new RegisterPluginV2 and RegisterPluginWithContextV2 api client funct…
helenfufu Jun 19, 2025
83fb091
add changelog
helenfufu Jun 19, 2025
1cce87a
more descriptive changelog
helenfufu Jun 19, 2025
76214d0
rename RegisterPluginV2 to RegisterPluginDetailed and RegisterPluginW…
helenfufu Jun 24, 2025
ae42f9f
return nil, nil if no warnings to preserve status code
helenfufu Jun 24, 2025
5ab3c6a
fix eof from decoding (check if no content before decoding)
helenfufu Jun 24, 2025
40deae7
doc for RegisterPluginResponse
helenfufu Jun 24, 2025
91c5446
Merge branch 'main' into vault-36295-improve-plugin-mgmt-ux-in-api-an…
helenfufu Jun 24, 2025
2982aa5
only validate plugin.Command in plugin catalog set for downloaded and…
helenfufu Jun 24, 2025
ab20a4c
Update website/content/api-docs/system/plugins-catalog.mdx
helenfufu Jun 25, 2025
4b8446d
Update website/content/api-docs/system/plugins-catalog.mdx
helenfufu Jun 25, 2025
29b5d6e
Update website/content/api-docs/system/plugins-catalog.mdx
helenfufu Jun 25, 2025
4d66328
Update website/content/docs/commands/plugin/register.mdx
helenfufu Jun 25, 2025
e0526ed
Update website/content/docs/commands/plugin/register.mdx
helenfufu Jun 25, 2025
5796723
Update website/content/docs/commands/plugin/register.mdx
helenfufu Jun 25, 2025
d4b2de6
Update website/content/docs/commands/plugin/register.mdx
helenfufu Jun 26, 2025
ca63320
move up enterprise note on plugin register command doc
helenfufu Jun 26, 2025
f0d6d25
[DOCS] Editorial suggestions for PR #30811 (#31111)
schavis Jun 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions api/sys_plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"time"

"github.com/mitchellh/mapstructure"
Expand Down Expand Up @@ -217,28 +218,54 @@ type RegisterPluginInput struct {
Env []string `json:"env,omitempty"`
}

type RegisterPluginResponse struct {
Warnings []string `json:"warnings"`
}

// RegisterPlugin wraps RegisterPluginWithContext using context.Background.
func (c *Sys) RegisterPlugin(i *RegisterPluginInput) error {
func (c *Sys) RegisterPlugin(i *RegisterPluginInput) (*RegisterPluginResponse, error) {
return c.RegisterPluginWithContext(context.Background(), i)
}

// RegisterPluginWithContext registers the plugin with the given information.
func (c *Sys) RegisterPluginWithContext(ctx context.Context, i *RegisterPluginInput) error {
func (c *Sys) RegisterPluginWithContext(ctx context.Context, i *RegisterPluginInput) (*RegisterPluginResponse, error) {
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
defer cancelFunc()

path := catalogPathByType(i.Type, i.Name)
req := c.c.NewRequest(http.MethodPut, path)

if err := req.SetJSONBody(i); err != nil {
return err
return nil, err
}

resp, err := c.c.rawRequestWithContext(ctx, req)
if err == nil {
defer resp.Body.Close()
} else {
return nil, err
}
return err

var registerResp RegisterPluginResponse
err = resp.DecodeJSON(&registerResp)
if err != nil {
return nil, err
}

// Filter out the `Endpoint replaced the value of these parameters with the values captured from the endpoint's path: [type]`
// warning because it is expected behavior from this function, as we set the type parameter in both the path and request body,
// and the warning informs us the path parameter takes precedence. However, this warning is not relevant for an end user so we
// omit it before returning to any client.
// TODO: This can likely be removed once https://hashicorp.atlassian.net/browse/VAULT-36722 is addressed.
filteredWarnings := make([]string, 0, len(registerResp.Warnings))
for _, warning := range registerResp.Warnings {
if !strings.Contains(warning, "Endpoint replaced the value of these parameters with the values captured from the endpoint's path") {
filteredWarnings = append(filteredWarnings, warning)
}
}
registerResp.Warnings = filteredWarnings

return &registerResp, err
}

// DeregisterPluginInput is used as input to the DeregisterPlugin function.
Expand Down
5 changes: 4 additions & 1 deletion api/sys_plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ func TestRegisterPlugin(t *testing.T) {
t.Fatal(err)
}

err = client.Sys().RegisterPluginWithContext(context.Background(), &RegisterPluginInput{
resp, err := client.Sys().RegisterPluginWithContext(context.Background(), &RegisterPluginInput{
Version: "v1.0.0",
})
if err != nil {
t.Fatal(err)
}
if len(resp.Warnings) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: any chance we could check the Warnings here using reflect.DeepEqual()?

Copy link
Contributor Author

@helenfufu helenfufu Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a bit of trouble with this, namely needing to differentiate when resp.Warnings is []string(nil) or []string{}, which the length check handles. Are we ok ignoring this nit in favor of prioritizing the download work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case where we don't expect a warning, we should not assert the value. Maybe I am misunderstanding. Is the suggestion to test the non-happy path?

t.Errorf("expected no warnings, got: %v", resp.Warnings)
}
}

func TestListPlugins(t *testing.T) {
Expand Down
14 changes: 9 additions & 5 deletions command/plugin_deregister_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,18 @@ func TestPluginDeregisterCommand_Run(t *testing.T) {
ui, cmd := testPluginDeregisterCommand(t)
cmd.client = client

if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
registerResp, err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: pluginName,
Type: api.PluginTypeCredential,
Command: pluginName,
SHA256: sha256Sum,
}); err != nil {
})
if err != nil {
t.Fatal(err)
}
if len(registerResp.Warnings) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: similar comment as above about testing the equality rather than the length of Warnings. Testing both is fine however.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above #30811 (comment)

t.Errorf("expected no warnings, got %q", registerResp.Warnings)
}

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

resp, err := client.Sys().ListPlugins(&api.ListPluginsInput{
listResp, err := client.Sys().ListPlugins(&api.ListPluginsInput{
Type: api.PluginTypeCredential,
})
if err != nil {
t.Fatal(err)
}

found := false
for _, plugins := range resp.PluginsByType {
for _, plugins := range listResp.PluginsByType {
for _, p := range plugins {
if p == pluginName {
found = true
}
}
}
if found {
t.Errorf("expected %q to not be in %q", pluginName, resp.PluginsByType)
t.Errorf("expected %q to not be in %q", pluginName, listResp.PluginsByType)
}
})

Expand Down
34 changes: 25 additions & 9 deletions command/plugin_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,27 @@ func (c *PluginRegisterCommand) Flags() *FlagSets {
Name: "command",
Target: &c.flagCommand,
Completion: complete.PredictAnything,
Usage: "Command to spawn the plugin. This defaults to the name of the " +
"plugin if both oci_image and command are unspecified.",
Usage: "Command to spawn the plugin. If sha256 is provided to register with a plugin binary, " +
"this defaults to the name of the plugin if both oci_image and command are unspecified. " +
"Otherwise, if sha256 is not provided, a plugin artifact is expected for registration, and " +
"this will be ignored because the run command is known.",
})

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

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

f.StringVar(&StringVar{
Expand Down Expand Up @@ -151,7 +156,7 @@ func (c *PluginRegisterCommand) Run(args []string) int {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1 or 2, got %d)", len(args)))
return 1
case c.flagSHA256 == "" && c.flagVersion == "":
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.")
c.UI.Error("One of -sha256 or -version is required. If registering with a binary, please provide at least -sha256 (-version optional). If registering with an artifact, please provide -version only.")
return 1

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

command := c.flagCommand
if command == "" && c.flagOCIImage == "" {
command = pluginName
if c.flagSHA256 != "" {
if command == "" && c.flagOCIImage == "" {
command = pluginName
}
}

if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
resp, err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: pluginName,
Type: pluginType,
Args: c.flagArgs,
Expand All @@ -191,11 +198,20 @@ func (c *PluginRegisterCommand) Run(args []string) int {
OCIImage: c.flagOCIImage,
Runtime: c.flagRuntime,
Env: c.flagEnv,
}); err != nil {
})
if err != nil {
c.UI.Error(fmt.Sprintf("Error registering plugin %s: %s", pluginName, err))
return 2
}

if resp != nil && len(resp.Warnings) > 0 {
c.UI.Warn(wrapAtLength(fmt.Sprintf(
"Warnings while registering plugin %s: %s",
pluginName,
strings.Join(resp.Warnings, "\n\n"),
)) + "\n")
}

c.UI.Output(fmt.Sprintf("Success! Registered plugin: %s", pluginName))
return 0
}
8 changes: 6 additions & 2 deletions command/plugin_reload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,18 @@ func TestPluginReloadCommand_Run(t *testing.T) {
ui, cmd := testPluginReloadCommand(t)
cmd.client = client

if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
resp, err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: pluginName,
Type: api.PluginTypeCredential,
Command: pluginName,
SHA256: sha256Sum,
}); err != nil {
})
if err != nil {
t.Fatal(err)
}
if len(resp.Warnings) > 0 {
t.Errorf("expected no warnings, got: %v", resp.Warnings)
}

code := cmd.Run([]string{
"-plugin", pluginName,
Expand Down
16 changes: 12 additions & 4 deletions command/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@ func testPluginCreateAndRegister(tb testing.TB, client *api.Client, dir, name st

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

if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
resp, err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: name,
Type: pluginType,
Command: name,
SHA256: sha256Sum,
Version: version,
}); err != nil {
})
if err != nil {
tb.Fatal(err)
}
if len(resp.Warnings) > 0 {
tb.Errorf("expected no warnings, got: %v", resp.Warnings)
}

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

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

if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
resp, err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: name,
Type: pluginType,
Command: name,
SHA256: sha256Sum,
Version: "v1.0.0",
}); err != nil {
})
if err != nil {
tb.Fatal(err)
}
if len(resp.Warnings) > 0 {
tb.Errorf("expected no warnings, got: %v", resp.Warnings)
}

return pth, sha256Sum, "v1.0.0"
}
2 changes: 2 additions & 0 deletions command/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package command

import (
"bytes"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -193,6 +194,7 @@ func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, er
r.body = body
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader([]byte(`{"warnings": []}`))),
}, nil
}

Expand Down
8 changes: 6 additions & 2 deletions vault/external_tests/plugin/external_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,19 @@ func TestExternalPlugin_RollbackAndReload(t *testing.T) {

func testRegisterVersion(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin, version string) {
t.Helper()
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
resp, err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: plugin.Name,
Type: api.PluginType(plugin.Typ),
Command: plugin.Name,
SHA256: plugin.Sha256,
Version: version,
}); err != nil {
})
if err != nil {
t.Fatal(err)
}
if len(resp.Warnings) > 0 {
t.Errorf("expected no warnings, got: %v", resp.Warnings)
}
}

func testEnableVersion(t *testing.T, client *api.Client, plugin pluginhelpers.TestPlugin, version string) {
Expand Down
14 changes: 11 additions & 3 deletions vault/logical_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,8 +553,16 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, _ *logica

command := d.Get("command").(string)
ociImage := d.Get("oci_image").(string)
if command == "" && ociImage == "" {
return logical.ErrorResponse("must provide at least one of command or oci_image"), nil
var resp logical.Response
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benashz @thyton When testing this PR locally since the plugin download work was merged, there's some behavioral changes compared to my testing prior to the plugin download work. Namely this command validation was added in the plugin catalog set function and now fails when only version is supplied for an extracted artifact directory (via CLI or API).

CLI
$ vault plugin register -version=0.24.1 auth vault-plugin-auth-jwt
Error registering plugin vault-plugin-auth-jwt: Error making API request.

URL: PUT http://127.0.0.1:8200/v1/sys/plugins/catalog/auth/vault-plugin-auth-jwt
Code: 500. Errors:

* 1 error occurred:
	* must specify command to register plugin
API
$ curl --header 'X-Vault-Token: root' --request POST --data @data-register-jwt.json http://127.0.0.1:8200/v1/sys/plugins/catalog/auth/vault-plugin-auth-jwt
{"errors":["1 error occurred:\n\t* must specify command to register plugin\n\n"]}

I'm not quite sure exactly what changed the behavior and still working to pinpoint it (i.e. we now send empty plugin.Command to set from CLI and API, but still working to check: did we always do that?), but also finding it a bit difficult to reconcile between the now two diverging handlePluginCatalogUpdate and entHandlePluginCatalogUpdate functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2982aa5 to limit the plugin.Command validation to download and binary cases only. It's a hack because we have divergent ways of populating the plugin runner (either with plugin.Command in the download and binary cases or with plugin.Name in the extracted artifact case), but it works.

The alternative is to stop requiring plugin.Command in the download case, and instead rely on plugin.Name when computing plugin.Command in entPrepareDownloadedPlugin. I recall while working on plugin downloads, we discussed a lot + ultimately decided to set plugin.Command here from the download response in entHandlePluginCatalogUpdate, and rely on that, so opted for the former approach.

We should likely refactor this when we get to TODO 1 on entHandlePluginCatalogUpdate to "refactor to share code with the non-Enterprise version more cleanly" and TODO 2 to "move XYZ into handlePluginCatalogUpdate".

Thanks @thyton for pairing!


if sha256 == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally these parameter constraints would be handled by the Vault server API. The vault client could just pass in everything that it got from the command line to Vault and the Vault server would do the parameter checks, returning any errors to the vault client. This is not a blocker, but something to consider for the future, especially since the Vault client may be connecting to some version of Vault that does support plugin artifacts, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack for the future, thanks for the input.

There might be a misunderstanding on my side: I thought handlePluginCatalogUpdatein logical_system.go is the API handler? Where would you suggest making the change in the future?

if command != "" {
resp.AddWarning(fmt.Sprintf("When sha256 is unspecified, a plugin artifact is expected for registration and the command parameter %q will be ignored.", command))
}
} else {
if command == "" && ociImage == "" {
return logical.ErrorResponse("must provide at least one of command or oci_image"), nil
}
}

if ociImage == "" {
Expand Down Expand Up @@ -619,7 +627,7 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, _ *logica
return nil, err
}

return nil, nil
return &resp, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this change the response code of this API when there are no warnings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm good call. In respondLogical we call respondOk which gives StatusNoContent (204) for the previous behavior of returning nil response and StatusOK (200) for the new behavior of returning &resp.

I supposed to preserve the existing code, we could do something like the following?:

if len(resp.Warnings) == 0 {
    return nil, nil
}
return &resp, nil

Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is a common pattern in vault. +1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! ae42f9f

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit above caused some tests to fail (https://github.com/hashicorp/vault/actions/runs/15856157544/job/44701993144). Traced it back to this resp.DecodeJSON failing with EOF when the status code was a 204. I've added a resp != nil && resp.StatusCode != http.StatusNoContent check before decoding in a follow-up commit 5ab3c6a, but I don't see this pattern anywhere else. Is there a better way to handle this?

}

func (b *SystemBackend) handlePluginCatalogRead(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
Expand Down
18 changes: 11 additions & 7 deletions website/content/api-docs/system/plugins-catalog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,21 @@ supplied name.
See [/sys/plugins/runtimes/catalog](/vault/api-docs/system/plugins-runtimes-catalog) for additional information.

- `version` `(string: "")` - Specifies the semantic version of the plugin. Used as the tag
when specifying `oci_image`, but with any leading 'v' trimmed.
when specifying `oci_image`, but with any leading 'v' trimmed. This is required to register
with a plugin artifact but optional when registering with a plugin binary.

- `sha256` `(string: <required>)` – The SHA256 sum of a Community plugin
binary or the OCI image. Before a plugin is run, its SHA will be checked against this value.
If the actual SHA of the plugin binary and the SHA provided in `sha256` do not match, Vault will not run the plugin. The `sha256` parameter is only required for Community plugins. Enterprise plugins do not require SHA confirmation.
- `sha256` `(string: "")` – The SHA256 sum of a plugin binary or the OCI image.
Before a plugin is run, its SHA will be checked against this value. If the actual SHA of the
plugin binary and the SHA provided in `sha256` do not match, Vault will not run the plugin.
This is required to register with a plugin binary but should not be specified when registering
with a plugin artifact.

- `command` `(string: <required>)` - Specifies the command used to execute the
- `command` `(string: "")` - Specifies the command used to execute the
plugin. This is relative to the plugin directory. e.g. `"myplugin"`, or if `oci_image`
is also specified, it is relative to the image's working directory.
The `command` parameter is only required for Community plugins as
the run command is known for Enterprise plugins.
This is required to register with a plugin binary but should not be specified when
registering with a plugin artifact; if it is specified for an artifact, it will be
ignored as the run command is known.

- `args` `(array: [])` – Specifies the arguments used to execute the plugin. If
the arguments are provided here, the `command` parameter should only contain
Expand Down
Loading
Loading