add CLI commands for plugin runtime VAULT-18181 (#22819)

---------

Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
Thy Ton
2023-09-08 10:11:48 -07:00
committed by GitHub
parent 06d0c396b9
commit 12b9e5dd36
14 changed files with 1230 additions and 45 deletions

View File

@@ -64,8 +64,8 @@ type RegisterPluginRuntimeInput struct {
OCIRuntime string `json:"oci_runtime,omitempty"`
CgroupParent string `json:"cgroup_parent,omitempty"`
CPU int64 `json:"cpu,omitempty"`
Memory int64 `json:"memory,omitempty"`
CPU int64 `json:"cpu_nanos,omitempty"`
Memory int64 `json:"memory_bytes,omitempty"`
}
// RegisterPluginRuntime registers the plugin with the given information.

View File

@@ -604,6 +604,31 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co
BaseCommand: getBaseCommand(),
}, nil
},
"plugin runtime": func() (cli.Command, error) {
return &PluginRuntimeCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"plugin runtime register": func() (cli.Command, error) {
return &PluginRuntimeRegisterCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"plugin runtime deregister": func() (cli.Command, error) {
return &PluginRuntimeDeregisterCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"plugin runtime info": func() (cli.Command, error) {
return &PluginRuntimeInfoCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"plugin runtime list": func() (cli.Command, error) {
return &PluginRuntimeListCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"proxy": func() (cli.Command, error) {
return &ProxyCommand{
BaseCommand: &BaseCommand{

View File

@@ -6,14 +6,11 @@ package command
import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"sort"
"strings"
"testing"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/testhelpers/corehelpers"
"github.com/hashicorp/vault/sdk/helper/consts"
@@ -338,40 +335,3 @@ func TestFlagParsing(t *testing.T) {
})
}
}
func mockClient(t *testing.T) (*api.Client, *recordingRoundTripper) {
t.Helper()
config := api.DefaultConfig()
httpClient := cleanhttp.DefaultClient()
roundTripper := &recordingRoundTripper{}
httpClient.Transport = roundTripper
config.HttpClient = httpClient
client, err := api.NewClient(config)
if err != nil {
t.Fatal(err)
}
return client, roundTripper
}
var _ http.RoundTripper = (*recordingRoundTripper)(nil)
type recordingRoundTripper struct {
path string
body []byte
}
func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
r.path = req.URL.Path
defer req.Body.Close()
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
r.body = body
return &http.Response{
StatusCode: 200,
}, nil
}

54
command/plugin_runtime.go Normal file
View File

@@ -0,0 +1,54 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*PluginRuntimeCommand)(nil)
type PluginRuntimeCommand struct {
*BaseCommand
}
func (c *PluginRuntimeCommand) Synopsis() string {
return "Interact with Vault plugin runtimes catalog."
}
func (c *PluginRuntimeCommand) Help() string {
helpText := `
Usage: vault plugin runtime <subcommand> [options] [args]
This command groups subcommands for interacting with Vault's plugin runtimes and the
plugin runtime catalog. The plugin runtime catalog is divided into types. Currently,
Vault only supports "container" plugin runtimes. A plugin runtime allows users to
fine-tune the parameters with which a plugin is executed. For example, you can select
a different OCI-compatible runtime, or set resource limits. A plugin runtime can
optionally be referenced during plugin registration. A type must be specified on each call.
Here are a few examples of the plugin runtime commands.
List all available plugin runtimes in the catalog of a particular type:
$ vault plugin runtime list -type=container
Register a new plugin runtime to the catalog as a particular type:
$ vault plugin runtime register -type=container -oci_runtime=my-oci-runtime my-custom-plugin-runtime
Get information about a plugin runtime in the catalog listed under a particular type:
$ vault plugin runtime info -type=container my-custom-plugin-runtime
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *PluginRuntimeCommand) Run(args []string) int {
return cli.RunResultHelp
}

View File

@@ -0,0 +1,124 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var (
_ cli.Command = (*PluginRuntimeDeregisterCommand)(nil)
_ cli.CommandAutocomplete = (*PluginRuntimeDeregisterCommand)(nil)
)
type PluginRuntimeDeregisterCommand struct {
*BaseCommand
flagType string
}
func (c *PluginRuntimeDeregisterCommand) Synopsis() string {
return "Deregister an existing plugin runtime in the catalog"
}
func (c *PluginRuntimeDeregisterCommand) Help() string {
helpText := `
Usage: vault plugin runtime deregister [options] NAME
Deregister an existing plugin runtime in the catalog with the given name. If
any registered plugin references the plugin runtime, an error is returned. If
the plugin runtime does not exist, an error is returned. The -type flag
currently only accepts "container".
Deregister a plugin runtime:
$ vault plugin runtime deregister -type=container my-plugin-runtime
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PluginRuntimeDeregisterCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "type",
Target: &c.flagType,
Completion: complete.PredictAnything,
Usage: "Plugin runtime type. Vault currently only supports \"container\" runtime type.",
})
return set
}
func (c *PluginRuntimeDeregisterCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *PluginRuntimeDeregisterCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PluginRuntimeDeregisterCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
runtimeTyeRaw := strings.TrimSpace(c.flagType)
if len(runtimeTyeRaw) == 0 {
c.UI.Error("-type is required for plugin runtime deregistration")
return 1
}
runtimeType, err := api.ParsePluginRuntimeType(runtimeTyeRaw)
if err != nil {
c.UI.Error(err.Error())
return 2
}
var runtimeNameRaw string
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
// This case should come after invalid cases have been checked
case len(args) == 1:
runtimeNameRaw = args[0]
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
runtimeName := strings.TrimSpace(runtimeNameRaw)
if err = client.Sys().DeregisterPluginRuntime(context.Background(), &api.DeregisterPluginRuntimeInput{
Name: runtimeName,
Type: runtimeType,
}); err != nil {
c.UI.Error(fmt.Sprintf("Error deregistering plugin runtime named %s: %s", runtimeName, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Deregistered plugin runtime: %s", runtimeName))
return 0
}

View File

@@ -0,0 +1,116 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"regexp"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testPluginRuntimeDeregisterCommand(tb testing.TB) (*cli.MockUi, *PluginRuntimeDeregisterCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginRuntimeDeregisterCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginRuntimeDeregisterCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
[]string{"-type=container"},
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"-type=container", "foo", "baz"},
"Too many arguments",
1,
},
{
"invalid_runtime_type",
[]string{"-type=foo", "bar"},
"\"foo\" is not a supported plugin runtime type",
2,
},
{
"info_container_on_empty_plugin_runtime_catalog",
[]string{"-type=container", "my-plugin-runtime"},
"Error deregistering plugin runtime named my-plugin-runtime",
2,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginRuntimeDeregisterCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
matcher := regexp.MustCompile(tc.out)
if !matcher.MatchString(combined) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPluginRuntimeDeregisterCommand(t)
cmd.client = client
code := cmd.Run([]string{"-type=container", "my-plugin-runtime"})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error deregistering plugin runtime named my-plugin-runtime"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPluginRuntimeDeregisterCommand(t)
assertNoTabs(t, cmd)
})
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var (
_ cli.Command = (*PluginRuntimeInfoCommand)(nil)
_ cli.CommandAutocomplete = (*PluginRuntimeInfoCommand)(nil)
)
type PluginRuntimeInfoCommand struct {
*BaseCommand
flagType string
}
func (c *PluginRuntimeInfoCommand) Synopsis() string {
return "Read information about a plugin runtime in the catalog"
}
func (c *PluginRuntimeInfoCommand) Help() string {
helpText := `
Usage: vault plugin runtime info [options] NAME
Displays information about a plugin runtime in the catalog with the given name. If
the plugin runtime does not exist, an error is returned. The -type flag
currently only accepts "container".
Get info about a plugin runtime:
$ vault plugin runtime info -type=container my-plugin-runtime
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PluginRuntimeInfoCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "type",
Target: &c.flagType,
Completion: complete.PredictAnything,
Usage: "Plugin runtime type. Vault currently only supports \"container\" runtime type.",
})
return set
}
func (c *PluginRuntimeInfoCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *PluginRuntimeInfoCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PluginRuntimeInfoCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
runtimeTyeRaw := strings.TrimSpace(c.flagType)
if len(runtimeTyeRaw) == 0 {
c.UI.Error("-type is required for plugin runtime info retrieval")
return 1
}
runtimeType, err := api.ParsePluginRuntimeType(runtimeTyeRaw)
if err != nil {
c.UI.Error(err.Error())
return 2
}
var runtimeNameRaw string
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
// This case should come after invalid cases have been checked
case len(args) == 1:
runtimeNameRaw = args[0]
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
runtimeName := strings.TrimSpace(runtimeNameRaw)
resp, err := client.Sys().GetPluginRuntime(context.Background(), &api.GetPluginRuntimeInput{
Name: runtimeName,
Type: runtimeType,
})
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading plugin runtime named %s: %s", runtimeName, err))
return 2
}
if resp == nil {
c.UI.Error(fmt.Sprintf("No value found for plugin runtime %q", runtimeName))
return 2
}
data := map[string]interface{}{
"name": resp.Name,
"type": resp.Type,
"oci_runtime": resp.OCIRuntime,
"cgroup_parent": resp.CgroupParent,
"cpu_nanos": resp.CPU,
"memory_bytes": resp.Memory,
}
if c.flagField != "" {
return PrintRawField(c.UI, data, c.flagField)
}
return OutputData(c.UI, data)
}

View File

@@ -0,0 +1,116 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"regexp"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testPluginRuntimeInfoCommand(tb testing.TB) (*cli.MockUi, *PluginRuntimeInfoCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginRuntimeInfoCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginRuntimeInfoCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
[]string{"-type=container"},
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"-type=container", "bar", "baz"},
"Too many arguments",
1,
},
{
"invalid_runtime_type",
[]string{"-type=foo", "bar"},
"\"foo\" is not a supported plugin runtime type",
2,
},
{
"info_container_on_empty_plugin_runtime_catalog",
[]string{"-type=container", "my-plugin-runtime"},
"Error reading plugin runtime named my-plugin-runtime",
2,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginRuntimeInfoCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
matcher := regexp.MustCompile(tc.out)
if !matcher.MatchString(combined) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPluginRuntimeInfoCommand(t)
cmd.client = client
code := cmd.Run([]string{"-type=container", "my-plugin-runtime"})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error reading plugin runtime named my-plugin-runtime"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPluginRuntimeInfoCommand(t)
assertNoTabs(t, cmd)
})
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var (
_ cli.Command = (*PluginRuntimeListCommand)(nil)
_ cli.CommandAutocomplete = (*PluginRuntimeListCommand)(nil)
)
type PluginRuntimeListCommand struct {
*BaseCommand
flagType string
}
func (c *PluginRuntimeListCommand) Synopsis() string {
return "Lists available plugin runtimes"
}
func (c *PluginRuntimeListCommand) Help() string {
helpText := `
Usage: vault plugin runtime list [options]
Lists available plugin runtimes registered in the catalog. This does not list whether
plugin runtimes are in use, but rather just their availability.
List all available plugin runtimes in the catalog:
$ vault plugin runtime list
List all available container plugin runtimes in the catalog:
$ vault plugin runtime list -type=container
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PluginRuntimeListCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "type",
Target: &c.flagType,
Completion: complete.PredictAnything,
Usage: "Plugin runtime type. Vault currently only supports \"container\" runtime type.",
})
return set
}
func (c *PluginRuntimeListCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *PluginRuntimeListCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PluginRuntimeListCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
if len(f.Args()) > 0 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
return 1
}
var input *api.ListPluginRuntimesInput
runtimeTyeRaw := strings.TrimSpace(c.flagType)
if len(runtimeTyeRaw) > 0 {
runtimeType, err := api.ParsePluginRuntimeType(runtimeTyeRaw)
if err != nil {
c.UI.Error(err.Error())
return 2
}
input = &api.ListPluginRuntimesInput{Type: runtimeType}
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
resp, err := client.Sys().ListPluginRuntimes(context.Background(), input)
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing available plugin runtimes: %s", err))
return 2
}
if resp == nil {
c.UI.Error("No tableResponse from server when listing plugin runtimes")
return 2
}
switch Format(c.UI) {
case "table":
c.UI.Output(tableOutput(c.tableResponse(resp), nil))
return 0
default:
return OutputData(c.UI, resp.Runtimes)
}
}
func (c *PluginRuntimeListCommand) tableResponse(response *api.ListPluginRuntimesResponse) []string {
out := []string{"Name | Type | OCI Runtime | Parent Cgroup | CPU Nanos | Memory Bytes"}
for _, runtime := range response.Runtimes {
out = append(out, fmt.Sprintf("%s | %s | %s | %s | %d | %d",
runtime.Name, runtime.Type, runtime.OCIRuntime, runtime.CgroupParent, runtime.CPU, runtime.Memory))
}
return out
}

View File

@@ -0,0 +1,116 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"regexp"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testPluginRuntimeListCommand(tb testing.TB) (*cli.MockUi, *PluginRuntimeListCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginRuntimeListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginRuntimeListCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo"},
"Too many arguments",
1,
},
{
"invalid_runtime_type",
[]string{"-type=foo"},
"\"foo\" is not a supported plugin runtime type",
2,
},
{
"list container on empty plugin runtime catalog",
[]string{"-type=container"},
"Error listing available plugin runtimes:",
2,
},
{
"list on empty plugin runtime catalog",
nil,
"Error listing available plugin runtimes:",
2,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginRuntimeListCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
matcher := regexp.MustCompile(tc.out)
if !matcher.MatchString(combined) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPluginRuntimeListCommand(t)
cmd.client = client
code := cmd.Run([]string{"-type=container"})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error listing available plugin runtimes: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPluginRuntimeListCommand(t)
assertNoTabs(t, cmd)
})
}

View File

@@ -0,0 +1,161 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var (
_ cli.Command = (*PluginRuntimeRegisterCommand)(nil)
_ cli.CommandAutocomplete = (*PluginRuntimeRegisterCommand)(nil)
)
type PluginRuntimeRegisterCommand struct {
*BaseCommand
flagType string
flagOCIRuntime string
flagCgroupParent string
flagCPUNanos int64
flagMemoryBytes int64
}
func (c *PluginRuntimeRegisterCommand) Synopsis() string {
return "Registers a new plugin runtime in the catalog"
}
func (c *PluginRuntimeRegisterCommand) Help() string {
helpText := `
Usage: vault plugin runtime register [options] NAME
Registers a new plugin runtime in the catalog. Currently, Vault only supports registering runtimes of type "container".
The OCI runtime must be available on Vault's host. If no OCI runtime is specified, Vault will use "runsc", gVisor's OCI runtime.
Register the plugin runtime named my-custom-plugin-runtime:
$ vault plugin runtime register -type=container -oci_runtime=my-oci-runtime my-custom-plugin-runtime
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PluginRuntimeRegisterCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "type",
Target: &c.flagType,
Completion: complete.PredictAnything,
Usage: "Plugin runtime type. Vault currently only supports \"container\" runtime type.",
})
f.StringVar(&StringVar{
Name: "oci_runtime",
Target: &c.flagOCIRuntime,
Completion: complete.PredictAnything,
Usage: "OCI runtime. Default is \"runsc\", gVisor's OCI runtime.",
})
f.StringVar(&StringVar{
Name: "cgroup_parent",
Target: &c.flagCgroupParent,
Completion: complete.PredictAnything,
Usage: "Parent cgroup to set for each container. This can be used to control the total resource usage for a group of plugins.",
})
f.Int64Var(&Int64Var{
Name: "cpu_nanos",
Target: &c.flagCPUNanos,
Completion: complete.PredictAnything,
Usage: "CPU limit to set per container in nanos. Defaults to no limit.",
})
f.Int64Var(&Int64Var{
Name: "memory_bytes",
Target: &c.flagMemoryBytes,
Completion: complete.PredictAnything,
Usage: "Memory limit to set per container in bytes. Defaults to no limit.",
})
return set
}
func (c *PluginRuntimeRegisterCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *PluginRuntimeRegisterCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PluginRuntimeRegisterCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
runtimeTyeRaw := strings.TrimSpace(c.flagType)
if len(runtimeTyeRaw) == 0 {
c.UI.Error("-type is required for plugin runtime registration")
return 1
}
runtimeType, err := api.ParsePluginRuntimeType(runtimeTyeRaw)
if err != nil {
c.UI.Error(err.Error())
return 2
}
var runtimeNameRaw string
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
// This case should come after invalid cases have been checked
case len(args) == 1:
runtimeNameRaw = args[0]
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
runtimeName := strings.TrimSpace(runtimeNameRaw)
ociRuntime := strings.TrimSpace(c.flagOCIRuntime)
cgroupParent := strings.TrimSpace(c.flagCgroupParent)
if err := client.Sys().RegisterPluginRuntime(context.Background(), &api.RegisterPluginRuntimeInput{
Name: runtimeName,
Type: runtimeType,
OCIRuntime: ociRuntime,
CgroupParent: cgroupParent,
CPU: c.flagCPUNanos,
Memory: c.flagMemoryBytes,
}); err != nil {
c.UI.Error(fmt.Sprintf("Error registering plugin runtime %s: %s", runtimeName, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Registered plugin runtime: %s", runtimeName))
return 0
}

View File

@@ -0,0 +1,202 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/mitchellh/cli"
)
func testPluginRuntimeRegisterCommand(tb testing.TB) (*cli.MockUi, *PluginRuntimeRegisterCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginRuntimeRegisterCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginRuntimeRegisterCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
flags []string
args []string
out string
code int
}{
{
"no type specified",
[]string{},
[]string{"foo"},
"-type is required for plugin runtime registration",
1,
},
{
"invalid type",
[]string{"-type", "foo"},
[]string{"not"},
"\"foo\" is not a supported plugin runtime type",
2,
},
{
"not_enough_args",
[]string{"-type", consts.PluginRuntimeTypeContainer.String()},
[]string{},
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"-type", consts.PluginRuntimeTypeContainer.String()},
[]string{"foo", "bar"},
"Too many arguments",
1,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginRuntimeRegisterCommand(t)
cmd.client = client
args := append(tc.flags, tc.args...)
code := cmd.Run(args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPluginRuntimeRegisterCommand(t)
cmd.client = client
code := cmd.Run([]string{"-type", consts.PluginRuntimeTypeContainer.String(), "my-plugin-runtime"})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error registering plugin runtime my-plugin-runtime"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPluginRuntimeRegisterCommand(t)
assertNoTabs(t, cmd)
})
}
// TestPluginRuntimeFlagParsing ensures that flags passed to vault plugin runtime register correctly
// translate into the expected JSON body and request path.
func TestPluginRuntimeFlagParsing(t *testing.T) {
for name, tc := range map[string]struct {
runtimeType api.PluginRuntimeType
name string
ociRuntime string
cgroupParent string
cpu int64
memory int64
args []string
expectedPayload string
}{
"minimal": {
runtimeType: api.PluginRuntimeTypeContainer,
name: "foo",
expectedPayload: `{"type":1,"name":"foo"}`,
},
"full": {
runtimeType: api.PluginRuntimeTypeContainer,
name: "foo",
cgroupParent: "/cpulimit/",
ociRuntime: "runtime",
cpu: 5678,
memory: 1234,
expectedPayload: `{"type":1,"cgroup_parent":"/cpulimit/","memory_bytes":1234,"cpu_nanos":5678,"oci_runtime":"runtime"}`,
},
} {
tc := tc
t.Run(name, func(t *testing.T) {
ui, cmd := testPluginRuntimeRegisterCommand(t)
var requestLogger *recordingRoundTripper
cmd.client, requestLogger = mockClient(t)
var args []string
if tc.cgroupParent != "" {
args = append(args, "-cgroup_parent="+tc.cgroupParent)
}
if tc.ociRuntime != "" {
args = append(args, "-oci_runtime="+tc.ociRuntime)
}
if tc.memory != 0 {
args = append(args, fmt.Sprintf("-memory_bytes=%d", tc.memory))
}
if tc.cpu != 0 {
args = append(args, fmt.Sprintf("-cpu_nanos=%d", tc.cpu))
}
if tc.runtimeType != api.PluginRuntimeTypeUnsupported {
args = append(args, "-type="+tc.runtimeType.String())
}
args = append(args, tc.name)
t.Log(args)
code := cmd.Run(args)
if exp := 0; code != exp {
t.Fatalf("expected %d to be %d\nstdout: %s\nstderr: %s", code, exp, ui.OutputWriter.String(), ui.ErrorWriter.String())
}
actual := &api.RegisterPluginRuntimeInput{}
expected := &api.RegisterPluginRuntimeInput{}
err := json.Unmarshal(requestLogger.body, actual)
if err != nil {
t.Fatal(err)
}
err = json.Unmarshal([]byte(tc.expectedPayload), expected)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expected, actual) {
t.Errorf("expected: %s\ngot: %s", tc.expectedPayload, requestLogger.body)
}
expectedPath := fmt.Sprintf("/v1/sys/plugins/runtimes/catalog/%s/%s", tc.runtimeType.String(), tc.name)
if requestLogger.path != expectedPath {
t.Errorf("Expected path %s, got %s", expectedPath, requestLogger.path)
}
})
}
}

View File

@@ -6,10 +6,13 @@ package command
import (
"fmt"
"io"
"net/http"
"os"
"testing"
"time"
"github.com/fatih/color"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/config"
"github.com/hashicorp/vault/command/token"
@@ -161,3 +164,40 @@ func getWriterFromUI(ui cli.Ui) io.Writer {
return os.Stdout
}
}
func mockClient(t *testing.T) (*api.Client, *recordingRoundTripper) {
t.Helper()
config := api.DefaultConfig()
httpClient := cleanhttp.DefaultClient()
roundTripper := &recordingRoundTripper{}
httpClient.Transport = roundTripper
config.HttpClient = httpClient
client, err := api.NewClient(config)
if err != nil {
t.Fatal(err)
}
return client, roundTripper
}
var _ http.RoundTripper = (*recordingRoundTripper)(nil)
type recordingRoundTripper struct {
path string
body []byte
}
func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
r.path = req.URL.Path
defer req.Body.Close()
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
r.body = body
return &http.Response{
StatusCode: 200,
}, nil
}

View File

@@ -6191,15 +6191,15 @@ This path responds to the following HTTP methods.
"",
},
"plugin-runtime-catalog_cgroup-parent": {
"Optional parent cgroup for the container",
"Parent cgroup to set for each container. This can be used to control the total resource usage for a group of plugins.",
"",
},
"plugin-runtime-catalog_cpu-nanos": {
"The limit of runtime CPU in nanos",
"CPU limit to set per container in nanos. Defaults to no limit.",
"",
},
"plugin-runtime-catalog_memory-bytes": {
"The limit of runtime memory in bytes",
"Memory limit to set per container in bytes. Defaults to no limit.",
"",
},
"leases": {