add plugin runtime API (#22469)

---------

Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
Thy Ton
2023-08-31 13:37:04 -07:00
committed by GitHub
parent 50bad8c035
commit 08574508c8
13 changed files with 1262 additions and 13 deletions

View File

@@ -0,0 +1,41 @@
// 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_runtime_types.go
// Any changes made should be made to both files at the same time.
import "fmt"
var PluginRuntimeTypes = []PluginRuntimeType{
PluginRuntimeTypeUnsupported,
PluginRuntimeTypeContainer,
}
type PluginRuntimeType uint32
// This is a list of PluginRuntimeTypes used by Vault.
const (
PluginRuntimeTypeUnsupported PluginRuntimeType = iota
PluginRuntimeTypeContainer
)
func (r PluginRuntimeType) String() string {
switch r {
case PluginRuntimeTypeContainer:
return "container"
default:
return "unsupported"
}
}
func ParsePluginRuntimeType(PluginRuntimeType string) (PluginRuntimeType, error) {
switch PluginRuntimeType {
case "container":
return PluginRuntimeTypeContainer, nil
default:
return PluginRuntimeTypeUnsupported, fmt.Errorf("%q is not a supported plugin runtime type", PluginRuntimeType)
}
}

View File

@@ -38,6 +38,8 @@ var sudoPaths = map[string]*regexp.Regexp{
"/sys/plugins/catalog/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[^/]+$`),
"/sys/plugins/catalog/{type}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+$`),
"/sys/plugins/catalog/{type}/{name}": regexp.MustCompile(`^/sys/plugins/catalog/[\w-]+/[^/]+$`),
"/sys/plugins/runtimes/catalog": regexp.MustCompile(`^/sys/plugins/runtimes/catalog/?$`),
"/sys/plugins/runtimes/catalog/{type}/{name}": regexp.MustCompile(`^/sys/plugins/runtimes/catalog/[\w-]+/[^/]+$`),
"/sys/raw/{path}": regexp.MustCompile(`^/sys/raw(?:/.+)?$`),
"/sys/remount": regexp.MustCompile(`^/sys/remount$`),
"/sys/revoke-force/{prefix}": regexp.MustCompile(`^/sys/revoke-force/.+$`),

View File

@@ -55,6 +55,11 @@ func TestIsSudoPath(t *testing.T) {
"/sys/plugins/catalog/some-type/some/name/with/slashes",
false,
},
// Testing: sys/plugins/runtimes/catalog/{type}/{name}
{
"/sys/plugins/runtimes/catalog/some-type/some-name",
true,
},
// Testing: auth/token/accessors (an example of a sudo path that only accepts list operations)
// It is matched as sudo without the trailing slash...
{

189
api/sys_plugins_runtimes.go Normal file
View File

@@ -0,0 +1,189 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package api
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/mitchellh/mapstructure"
)
// GetPluginRuntimeInput is used as input to the GetPluginRuntime function.
type GetPluginRuntimeInput struct {
Name string `json:"-"`
// Type of the plugin runtime. Required.
Type PluginRuntimeType `json:"type"`
}
// GetPluginRuntimeResponse is the response from the GetPluginRuntime call.
type GetPluginRuntimeResponse struct {
Type string `json:"type"`
Name string `json:"name"`
OCIRuntime string `json:"oci_runtime"`
CgroupParent string `json:"cgroup_parent"`
CPU int64 `json:"cpu_nanos"`
Memory int64 `json:"memory_bytes"`
}
// GetPluginRuntime retrieves information about the plugin.
func (c *Sys) GetPluginRuntime(ctx context.Context, i *GetPluginRuntimeInput) (*GetPluginRuntimeResponse, error) {
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
defer cancelFunc()
path := pluginRuntimeCatalogPathByType(i.Type, i.Name)
req := c.c.NewRequest(http.MethodGet, path)
resp, err := c.c.rawRequestWithContext(ctx, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Data *GetPluginRuntimeResponse
}
err = resp.DecodeJSON(&result)
if err != nil {
return nil, err
}
return result.Data, err
}
// RegisterPluginRuntimeInput is used as input to the RegisterPluginRuntime function.
type RegisterPluginRuntimeInput struct {
// Name is the name of the plugin. Required.
Name string `json:"-"`
// Type of the plugin. Required.
Type PluginRuntimeType `json:"type"`
OCIRuntime string `json:"oci_runtime,omitempty"`
CgroupParent string `json:"cgroup_parent,omitempty"`
CPU int64 `json:"cpu,omitempty"`
Memory int64 `json:"memory,omitempty"`
}
// RegisterPluginRuntime registers the plugin with the given information.
func (c *Sys) RegisterPluginRuntime(ctx context.Context, i *RegisterPluginRuntimeInput) error {
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
defer cancelFunc()
path := pluginRuntimeCatalogPathByType(i.Type, i.Name)
req := c.c.NewRequest(http.MethodPut, path)
if err := req.SetJSONBody(i); err != nil {
return err
}
resp, err := c.c.rawRequestWithContext(ctx, req)
if err == nil {
defer resp.Body.Close()
}
return err
}
// DeregisterPluginRuntimeInput is used as input to the DeregisterPluginRuntime function.
type DeregisterPluginRuntimeInput struct {
// Name is the name of the plugin runtime. Required.
Name string `json:"-"`
// Type of the plugin. Required.
Type PluginRuntimeType `json:"type"`
}
// DeregisterPluginRuntime removes the plugin with the given name from the plugin
// catalog.
func (c *Sys) DeregisterPluginRuntime(ctx context.Context, i *DeregisterPluginRuntimeInput) error {
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
defer cancelFunc()
path := pluginRuntimeCatalogPathByType(i.Type, i.Name)
req := c.c.NewRequest(http.MethodDelete, path)
resp, err := c.c.rawRequestWithContext(ctx, req)
if err == nil {
defer resp.Body.Close()
}
return err
}
type PluginRuntimeDetails struct {
Type string `json:"type" mapstructure:"type"`
Name string `json:"name" mapstructure:"name"`
OCIRuntime string `json:"oci_runtime" mapstructure:"oci_runtime"`
CgroupParent string `json:"cgroup_parent" mapstructure:"cgroup_parent"`
CPU int64 `json:"cpu_nanos" mapstructure:"cpu_nanos"`
Memory int64 `json:"memory_bytes" mapstructure:"memory_bytes"`
}
// ListPluginRuntimesInput is used as input to the ListPluginRuntimes function.
type ListPluginRuntimesInput struct {
// Type of the plugin. Required.
Type PluginRuntimeType `json:"type"`
}
// ListPluginRuntimesResponse is the response from the ListPluginRuntimes call.
type ListPluginRuntimesResponse struct {
// RuntimesByType is the list of plugin runtimes by type.
Runtimes []PluginRuntimeDetails `json:"runtimes"`
}
// ListPluginRuntimes lists all plugin runtimes in the catalog and returns their names as a
// list of strings.
func (c *Sys) ListPluginRuntimes(ctx context.Context, input *ListPluginRuntimesInput) (*ListPluginRuntimesResponse, error) {
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
defer cancelFunc()
if input != nil && input.Type == PluginRuntimeTypeUnsupported {
return nil, fmt.Errorf("%q is not a supported runtime type", input.Type.String())
}
resp, err := c.c.rawRequestWithContext(ctx, c.c.NewRequest(http.MethodGet, "/v1/sys/plugins/runtimes/catalog"))
if err != nil && resp == nil {
return nil, err
}
if resp == nil {
return nil, nil
}
defer resp.Body.Close()
secret, err := ParseSecret(resp.Body)
if err != nil {
return nil, err
}
if secret == nil || secret.Data == nil {
return nil, errors.New("data from server response is empty")
}
if _, ok := secret.Data["runtimes"]; !ok {
return nil, fmt.Errorf("data from server response does not contain runtimes")
}
var runtimes []PluginRuntimeDetails
if err = mapstructure.Decode(secret.Data["runtimes"], &runtimes); err != nil {
return nil, err
}
// return all runtimes in the catalog
if input == nil {
return &ListPluginRuntimesResponse{Runtimes: runtimes}, nil
}
result := &ListPluginRuntimesResponse{
Runtimes: []PluginRuntimeDetails{},
}
for _, runtime := range runtimes {
if runtime.Type == input.Type.String() {
result.Runtimes = append(result.Runtimes, runtime)
}
}
return result, nil
}
// pluginRuntimeCatalogPathByType is a helper to construct the proper API path by plugin type
func pluginRuntimeCatalogPathByType(runtimeType PluginRuntimeType, name string) string {
return fmt.Sprintf("/v1/sys/plugins/runtimes/catalog/%s/%s", runtimeType, name)
}

View File

@@ -0,0 +1,268 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package api
import (
"context"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
func TestRegisterPluginRuntime(t *testing.T) {
mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandlerRegister))
defer mockVaultServer.Close()
cfg := DefaultConfig()
cfg.Address = mockVaultServer.URL
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
err = client.Sys().RegisterPluginRuntime(context.Background(), &RegisterPluginRuntimeInput{
Name: "gvisor",
Type: PluginRuntimeTypeContainer,
OCIRuntime: "runsc",
CgroupParent: "/cpulimit/",
CPU: 1,
Memory: 10000,
})
if err != nil {
t.Fatal(err)
}
}
func TestGetPluginRuntime(t *testing.T) {
for name, tc := range map[string]struct {
body string
expected GetPluginRuntimeResponse
}{
"gvisor": {
body: getPluginRuntimeResponse,
expected: GetPluginRuntimeResponse{
Name: "gvisor",
Type: PluginRuntimeTypeContainer.String(),
OCIRuntime: "runsc",
CgroupParent: "/cpulimit/",
CPU: 1,
Memory: 10000,
},
},
} {
t.Run(name, func(t *testing.T) {
mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandlerInfo(tc.body)))
defer mockVaultServer.Close()
cfg := DefaultConfig()
cfg.Address = mockVaultServer.URL
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
input := GetPluginRuntimeInput{
Name: "gvisor",
Type: PluginRuntimeTypeContainer,
}
info, err := client.Sys().GetPluginRuntime(context.Background(), &input)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tc.expected, *info) {
t.Errorf("expected: %#v\ngot: %#v", tc.expected, info)
}
})
}
}
func TestListPluginRuntimeTyped(t *testing.T) {
for _, tc := range []struct {
runtimeType PluginRuntimeType
body string
expectedResponse *ListPluginRuntimesResponse
expectedErrNil bool
}{
{
runtimeType: PluginRuntimeTypeContainer,
body: listPluginRuntimeTypedResponse,
expectedResponse: &ListPluginRuntimesResponse{
Runtimes: []PluginRuntimeDetails{
{
Type: "container",
Name: "gvisor",
OCIRuntime: "runsc",
CgroupParent: "/cpulimit/",
CPU: 1,
Memory: 10000,
},
},
},
expectedErrNil: true,
},
{
runtimeType: PluginRuntimeTypeUnsupported,
body: listPluginRuntimeTypedResponse,
expectedResponse: nil,
expectedErrNil: false,
},
} {
t.Run(tc.runtimeType.String(), func(t *testing.T) {
mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandlerInfo(tc.body)))
defer mockVaultServer.Close()
cfg := DefaultConfig()
cfg.Address = mockVaultServer.URL
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
input := ListPluginRuntimesInput{
Type: tc.runtimeType,
}
list, err := client.Sys().ListPluginRuntimes(context.Background(), &input)
if tc.expectedErrNil && err != nil {
t.Fatal(err)
}
if (tc.expectedErrNil && !reflect.DeepEqual(tc.expectedResponse, list)) || (!tc.expectedErrNil && list != nil) {
t.Errorf("expected: %#v\ngot: %#v", tc.expectedResponse, list)
}
})
}
}
func TestListPluginRuntimeUntyped(t *testing.T) {
for _, tc := range []struct {
body string
expectedResponse *ListPluginRuntimesResponse
expectedErrNil bool
}{
{
body: listPluginRuntimeUntypedResponse,
expectedResponse: &ListPluginRuntimesResponse{
Runtimes: []PluginRuntimeDetails{
{
Type: "container",
Name: "gvisor",
OCIRuntime: "runsc",
CgroupParent: "/cpulimit/",
CPU: 1,
Memory: 10000,
},
{
Type: "container",
Name: "foo",
OCIRuntime: "otherociruntime",
CgroupParent: "/memorylimit/",
CPU: 2,
Memory: 20000,
},
{
Type: "container",
Name: "bar",
OCIRuntime: "otherociruntime",
CgroupParent: "/cpulimit/",
CPU: 3,
Memory: 30000,
},
},
},
expectedErrNil: true,
},
} {
t.Run("", func(t *testing.T) {
mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandlerInfo(tc.body)))
defer mockVaultServer.Close()
cfg := DefaultConfig()
cfg.Address = mockVaultServer.URL
client, err := NewClient(cfg)
if err != nil {
t.Fatal(err)
}
info, err := client.Sys().ListPluginRuntimes(context.Background(), nil)
if tc.expectedErrNil && err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tc.expectedResponse, info) {
t.Errorf("expected: %#v\ngot: %#v", tc.expectedResponse, info)
}
})
}
}
const getPluginRuntimeResponse = `{
"request_id": "e93d3f93-8e4f-8443-a803-f1c97c123456",
"data": {
"name": "gvisor",
"type": "container",
"oci_runtime": "runsc",
"cgroup_parent": "/cpulimit/",
"cpu_nanos": 1,
"memory_bytes": 10000
},
"warnings": null,
"auth": null
}`
const listPluginRuntimeTypedResponse = `{
"request_id": "e93d3f93-8e4f-8443-a803-f1c97c123456",
"data": {
"runtimes": [
{
"name": "gvisor",
"type": "container",
"oci_runtime": "runsc",
"cgroup_parent": "/cpulimit/",
"cpu_nanos": 1,
"memory_bytes": 10000
}
]
},
"warnings": null,
"auth": null
}
`
const listPluginRuntimeUntypedResponse = `{
"request_id": "e93d3f93-8e4f-8443-a803-f1c97c123456",
"data": {
"runtimes": [
{
"name": "gvisor",
"type": "container",
"oci_runtime": "runsc",
"cgroup_parent": "/cpulimit/",
"cpu_nanos": 1,
"memory_bytes": 10000
},
{
"name": "foo",
"type": "container",
"oci_runtime": "otherociruntime",
"cgroup_parent": "/memorylimit/",
"cpu_nanos": 2,
"memory_bytes": 20000
},
{
"name": "bar",
"type": "container",
"oci_runtime": "otherociruntime",
"cgroup_parent": "/cpulimit/",
"cpu_nanos": 3,
"memory_bytes": 30000
}
]
},
"warnings": null,
"auth": null
}`

View File

@@ -0,0 +1,41 @@
// 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_runtime_types.go
// Any changes made should be made to both files at the same time.
import "fmt"
var PluginRuntimeTypes = []PluginRuntimeType{
PluginRuntimeTypeUnsupported,
PluginRuntimeTypeContainer,
}
type PluginRuntimeType uint32
// This is a list of PluginRuntimeTypes used by Vault.
const (
PluginRuntimeTypeUnsupported PluginRuntimeType = iota
PluginRuntimeTypeContainer
)
func (r PluginRuntimeType) String() string {
switch r {
case PluginRuntimeTypeContainer:
return "container"
default:
return "unsupported"
}
}
func ParsePluginRuntimeType(PluginRuntimeType string) (PluginRuntimeType, error) {
switch PluginRuntimeType {
case "container":
return PluginRuntimeTypeContainer, nil
default:
return PluginRuntimeTypeUnsupported, fmt.Errorf("%q is not a supported plugin runtime type", PluginRuntimeType)
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pluginruntimeutil
import "github.com/hashicorp/vault/sdk/helper/consts"
// PluginRuntimeConfig defines the metadata needed to run a plugin runtime
type PluginRuntimeConfig struct {
Name string `json:"name" structs:"name"`
Type consts.PluginRuntimeType `json:"type" structs:"type"`
OCIRuntime string `json:"oci_runtime" structs:"oci_runtime"`
CgroupParent string `json:"cgroup_parent" structs:"cgroup_parent"`
CPU int64 `json:"cpu" structs:"cpu"`
Memory int64 `json:"memory" structs:"memory"`
}

View File

@@ -548,6 +548,9 @@ type Core struct {
// pluginCatalog is used to manage plugin configurations
pluginCatalog *PluginCatalog
// pluginRuntimeCatalog is used to manage plugin runtime configurations
pluginRuntimeCatalog *PluginRuntimeCatalog
// The userFailedLoginInfo map has user failed login information.
// It has user information (alias-name and mount accessor) as a key
// and login counter, last failed login time as value
@@ -2293,6 +2296,9 @@ func (s standardUnsealStrategy) unseal(ctx context.Context, logger log.Logger, c
return err
}
}
if err := c.setupPluginRuntimeCatalog(ctx); err != nil {
return err
}
if err := c.setupPluginCatalog(ctx); err != nil {
return err
}

View File

@@ -43,6 +43,7 @@ import (
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/pluginruntimeutil"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/helper/roottoken"
"github.com/hashicorp/vault/sdk/helper/wrapping"
@@ -113,6 +114,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
"config/auditing/*",
"config/ui/headers/*",
"plugins/catalog/*",
"plugins/runtimes/catalog/*",
"revoke-prefix/*",
"revoke-force/*",
"leases/revoke-prefix/*",
@@ -186,6 +188,8 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
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.pluginsRuntimesCatalogCRUDPath())
b.Backend.Paths = append(b.Backend.Paths, b.pluginsRuntimesCatalogListPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.auditPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.mountPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.authPaths()...)
@@ -733,6 +737,147 @@ func (b *SystemBackend) handlePluginReloadUpdate(ctx context.Context, req *logic
return &r, nil
}
func (b *SystemBackend) handlePluginRuntimeCatalogUpdate(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
runtimeName := d.Get("name").(string)
if runtimeName == "" {
return logical.ErrorResponse("missing plugin runtime name"), nil
}
runtimeTypeStr := d.Get("type").(string)
if runtimeTypeStr == "" {
return logical.ErrorResponse("missing plugin runtime type"), nil
}
runtimeType, err := consts.ParsePluginRuntimeType(runtimeTypeStr)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
switch runtimeType {
case consts.PluginRuntimeTypeContainer:
ociRuntime := d.Get("oci_runtime").(string)
cgroupParent := d.Get("cgroup_parent").(string)
cpu := d.Get("cpu_nanos").(int64)
if cpu < 0 {
return logical.ErrorResponse("runtime cpu in nanos cannot be negative"), nil
}
memory := d.Get("memory_bytes").(int64)
if memory < 0 {
return logical.ErrorResponse("runtime memory in bytes cannot be negative"), nil
}
if err = b.Core.pluginRuntimeCatalog.Set(ctx,
&pluginruntimeutil.PluginRuntimeConfig{
Name: runtimeName,
Type: runtimeType,
OCIRuntime: ociRuntime,
CgroupParent: cgroupParent,
CPU: cpu,
Memory: memory,
}); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
default:
logical.ErrorResponse(fmt.Sprintf("%s is not a supported plugin runtime type", runtimeTypeStr))
}
return nil, nil
}
func (b *SystemBackend) handlePluginRuntimeCatalogDelete(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
runtimeName := d.Get("name").(string)
if runtimeName == "" {
return logical.ErrorResponse("missing plugin runtime name"), nil
}
runtimeTypeStr := d.Get("type").(string)
if runtimeTypeStr == "" {
return logical.ErrorResponse("missing plugin runtime type"), nil
}
runtimeType, err := consts.ParsePluginRuntimeType(runtimeTypeStr)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
err = b.Core.pluginRuntimeCatalog.Delete(ctx, runtimeName, runtimeType)
if err != nil {
return nil, err
}
return nil, nil
}
func (b *SystemBackend) handlePluginRuntimeCatalogRead(ctx context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
runtimeName := d.Get("name").(string)
if runtimeName == "" {
return logical.ErrorResponse("missing plugin runtime name"), nil
}
runtimeTypeStr := d.Get("type").(string)
if runtimeTypeStr == "" {
return logical.ErrorResponse("missing plugin runtime type"), nil
}
runtimeType, err := consts.ParsePluginRuntimeType(runtimeTypeStr)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
conf, err := b.Core.pluginRuntimeCatalog.Get(ctx, runtimeName, runtimeType)
if err != nil {
return nil, err
}
if conf == nil {
return nil, nil
}
return &logical.Response{Data: map[string]interface{}{
"name": conf.Name,
"type": conf.Type.String(),
"oci_runtime": conf.OCIRuntime,
"cgroup_parent": conf.CgroupParent,
"cpu_nanos": conf.CPU,
"memory_bytes": conf.Memory,
}}, nil
}
func (b *SystemBackend) handlePluginRuntimeCatalogList(ctx context.Context, _ *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
var data []map[string]any
for _, runtimeType := range consts.PluginRuntimeTypes {
if runtimeType == consts.PluginRuntimeTypeUnsupported {
continue
}
configs, err := b.Core.pluginRuntimeCatalog.List(ctx, runtimeType)
if err != nil {
return nil, err
}
if len(configs) > 0 {
sort.Slice(configs, func(i, j int) bool {
return strings.Compare(configs[i].Name, configs[j].Name) == -1
})
for _, conf := range configs {
data = append(data, map[string]any{
"name": conf.Name,
"type": conf.Type.String(),
"oci_runtime": conf.OCIRuntime,
"cgroup_parent": conf.CgroupParent,
"cpu_nanos": conf.CPU,
"memory_bytes": conf.Memory,
})
}
}
}
resp := &logical.Response{
Data: map[string]interface{}{},
}
if len(data) > 0 {
resp.Data["runtimes"] = data
}
return resp, nil
}
// handleAuditedHeaderUpdate creates or overwrites a header entry
func (b *SystemBackend) handleAuditedHeaderUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
header := d.Get("header").(string)
@@ -5955,6 +6100,51 @@ Each entry is of the form "key=value".`,
"The semantic version of the plugin to use.",
"",
},
"plugin-runtime-catalog": {
"Configures plugin runtimes",
`
This path responds to the following HTTP methods.
LIST /
Returns a list of names of configured plugin runtimes.
GET /<type>/<name>
Retrieve the metadata for the named plugin runtime.
PUT /<type>/<name>
Add or update plugin runtime.
DELETE /<type>/<name>
Delete the plugin runtime with the given name.
`,
},
"plugin-runtime-catalog-list-all": {
"List all plugin runtimes in the catalog as a map of type to names.",
"",
},
"plugin-runtime-catalog_name": {
"The name of the plugin runtime",
"",
},
"plugin-runtime-catalog_type": {
"The type of the plugin runtime",
"",
},
"plugin-runtime-catalog_oci-runtime": {
"The OCI-compatible runtime (default \"runsc\")",
"",
},
"plugin-runtime-catalog_cgroup-parent": {
"Optional parent cgroup for the container",
"",
},
"plugin-runtime-catalog_cpu-nanos": {
"The limit of runtime CPU in nanos",
"",
},
"plugin-runtime-catalog_memory-bytes": {
"The limit of runtime memory in bytes",
"",
},
"leases": {
`View or list lease metadata.`,
`

View File

@@ -2093,6 +2093,155 @@ func (b *SystemBackend) pluginsReloadPath() *framework.Path {
}
}
func (b *SystemBackend) pluginsRuntimesCatalogCRUDPath() *framework.Path {
return &framework.Path{
Pattern: "plugins/runtimes/catalog/(?P<type>container)/" + framework.GenericNameRegex("name"),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "plugins-runtimes-catalog",
},
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_name"][0]),
},
"type": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_type"][0]),
},
"oci_runtime": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_oci-runtime"][0]),
},
"cgroup_parent": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_cgroup-parent"][0]),
},
"cpu_nanos": {
Type: framework.TypeInt64,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_cpu-nanos"][0]),
},
"memory_bytes": {
Type: framework.TypeInt64,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_memory-bytes"][0]),
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.handlePluginRuntimeCatalogUpdate,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "register",
OperationSuffix: "plugin-runtime|plugin-runtime-with-type|plugin-runtime-with-type-and-name", // TODO
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
}},
},
Summary: "Register a new plugin runtime, or updates an existing one with the supplied name.",
},
logical.DeleteOperation: &framework.PathOperation{
Callback: b.handlePluginRuntimeCatalogDelete,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "remove",
OperationSuffix: "plugin-runtime|plugin-runtime-with-type|plugin-runtime-with-type-and-name", // TODO
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
}},
},
Summary: "Remove the plugin runtime with the given name.",
},
logical.ReadOperation: &framework.PathOperation{
Callback: b.handlePluginRuntimeCatalogRead,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
OperationSuffix: "plugin-runtime-configuration|plugin-runtime-configuration-with-type|plugin-runtime-configuration-with-type-and-name",
},
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_name"][0]),
Required: true,
},
"type": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_type"][0]),
Required: true,
},
"oci_runtime": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_oci-runtime"][0]),
Required: true,
},
"cgroup_parent": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_cgroup-parent"][0]),
Required: true,
},
"cpu_nanos": {
Type: framework.TypeInt64,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_cpu-nanos"][0]),
Required: true,
},
"memory_bytes": {
Type: framework.TypeInt64,
Description: strings.TrimSpace(sysHelp["plugin-runtime-catalog_memory-bytes"][0]),
Required: true,
},
},
}},
},
Summary: "Return the configuration data for the plugin runtime with the given name.",
},
},
HelpSynopsis: strings.TrimSpace(sysHelp["plugin-runtime-catalog"][0]),
HelpDescription: strings.TrimSpace(sysHelp["plugin-runtime-catalog"][1]),
}
}
func (b *SystemBackend) pluginsRuntimesCatalogListPaths() []*framework.Path {
return []*framework.Path{
{
Pattern: "plugins/runtimes/catalog/?$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "plugins-runtimes-catalog",
OperationVerb: "list",
OperationSuffix: "plugins-runtimes",
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: b.handlePluginRuntimeCatalogList,
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"runtimes": {
Type: framework.TypeSlice,
Description: "List of all plugin runtimes in the catalog",
Required: true,
},
},
}},
},
},
},
HelpSynopsis: strings.TrimSpace(sysHelp["plugin-runtime-catalog-list-all"][0]),
HelpDescription: strings.TrimSpace(sysHelp["plugin-runtime-catalog-list-all"][1]),
},
}
}
func (b *SystemBackend) toolsPaths() []*framework.Path {
return []*framework.Path{
{

View File

@@ -33,6 +33,7 @@ import (
"github.com/hashicorp/vault/sdk/helper/compressutil"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/pluginruntimeutil"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/helper/testhelpers/schema"
"github.com/hashicorp/vault/sdk/logical"
@@ -5900,3 +5901,122 @@ func TestSystemBackend_ReadExperiments(t *testing.T) {
})
}
}
func TestSystemBackend_pluginRuntimeCRUD(t *testing.T) {
b := testSystemBackend(t)
conf := pluginruntimeutil.PluginRuntimeConfig{
Name: "foo",
Type: consts.PluginRuntimeTypeContainer,
OCIRuntime: "some-oci-runtime",
CgroupParent: "/cpulimit/",
CPU: 1,
Memory: 10000,
}
// Register the plugin runtime
req := logical.TestRequest(t, logical.UpdateOperation, fmt.Sprintf("plugins/runtimes/catalog/%s/%s", conf.Type.String(), conf.Name))
req.Data = map[string]interface{}{
"oci_runtime": conf.OCIRuntime,
"cgroup_parent": conf.OCIRuntime,
"cpu_nanos": conf.CPU,
"memory_bytes": conf.Memory,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v %#v", err, resp)
}
if resp != nil && (resp.IsError() || len(resp.Data) > 0) {
t.Fatalf("bad: %#v", resp)
}
// validate the response structure for plugin container runtime named foo
schema.ValidateResponse(
t,
schema.GetResponseSchema(t, b.(*SystemBackend).Route(req.Path), req.Operation),
resp,
true,
)
// Read the plugin runtime
req = logical.TestRequest(t, logical.ReadOperation, "plugins/runtimes/catalog/container/foo")
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
// validate the response structure for plugin container runtime named foo
schema.ValidateResponse(
t,
schema.GetResponseSchema(t, b.(*SystemBackend).Route(req.Path), req.Operation),
resp,
true,
)
readExp := map[string]any{
"type": conf.Type.String(),
"name": conf.Name,
"oci_runtime": conf.OCIRuntime,
"cgroup_parent": conf.OCIRuntime,
"cpu_nanos": conf.CPU,
"memory_bytes": conf.Memory,
}
if !reflect.DeepEqual(resp.Data, readExp) {
t.Fatalf("got: %#v expect: %#v", resp.Data, readExp)
}
// List the plugin runtimes (untyped or all)
req = logical.TestRequest(t, logical.ListOperation, "plugins/runtimes/catalog")
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
listExp := map[string]interface{}{
"runtimes": []map[string]any{readExp},
}
if !reflect.DeepEqual(resp.Data, listExp) {
t.Fatalf("got: %#v expect: %#v", resp.Data, listExp)
}
// Delete the plugin runtime
req = logical.TestRequest(t, logical.DeleteOperation, "plugins/runtimes/catalog/container/foo")
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %#v", resp)
}
// validate the response structure for plugin container runtime named foo
schema.ValidateResponse(
t,
schema.GetResponseSchema(t, b.(*SystemBackend).Route(req.Path), req.Operation),
resp,
true,
)
// Read the plugin runtime (deleted)
req = logical.TestRequest(t, logical.ReadOperation, "plugins/runtimes/catalog/container/foo")
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err == nil {
t.Fatal("expected a read error after the runtime was deleted")
}
if resp != nil {
t.Fatalf("bad: %#v", resp)
}
// List the plugin runtimes (untyped or all)
req = logical.TestRequest(t, logical.ListOperation, "plugins/runtimes/catalog")
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
listExp = map[string]interface{}{}
if !reflect.DeepEqual(resp.Data, listExp) {
t.Fatalf("got: %#v expect: %#v", resp.Data, listExp)
}
}

View File

@@ -0,0 +1,140 @@
package vault
import (
"context"
"encoding/json"
"errors"
"fmt"
"path"
"sync"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/pluginruntimeutil"
"github.com/hashicorp/vault/sdk/logical"
)
var (
pluginRuntimeCatalogPath = "core/plugin-runtime-catalog/"
ErrPluginRuntimeNotFound = errors.New("plugin runtime not found")
ErrPluginRuntimeBadType = errors.New("unable to determine plugin runtime type")
ErrPluginRuntimeBadContainerConfig = errors.New("bad container config")
)
// PluginRuntimeCatalog keeps a record of plugin runtimes. Plugin runtimes need
// to be registered to the catalog before they can be used in backends when registering plugins with runtimes
type PluginRuntimeCatalog struct {
catalogView *BarrierView
logger log.Logger
lock sync.RWMutex
}
func (c *Core) setupPluginRuntimeCatalog(ctx context.Context) error {
c.pluginRuntimeCatalog = &PluginRuntimeCatalog{
catalogView: NewBarrierView(c.barrier, pluginRuntimeCatalogPath),
logger: c.logger,
}
if c.logger.IsInfo() {
c.logger.Info("successfully setup plugin runtime catalog")
}
return nil
}
// Get retrieves a plugin runtime with the specified name from the catalog
// It returns a PluginRuntimeConfig or an error if no plugin runtime was found.
func (c *PluginRuntimeCatalog) Get(ctx context.Context, name string, prt consts.PluginRuntimeType) (*pluginruntimeutil.PluginRuntimeConfig, error) {
storageKey := path.Join(prt.String(), name)
c.lock.RLock()
defer c.lock.RUnlock()
entry, err := c.catalogView.Get(ctx, storageKey)
if err != nil {
return nil, fmt.Errorf("failed to retrieve plugin runtime %q %q: %w", prt.String(), name, err)
}
if entry == nil {
return nil, fmt.Errorf("failed to retrieve plugin %q %q: %w", prt.String(), name, err)
}
runner := new(pluginruntimeutil.PluginRuntimeConfig)
if err := jsonutil.DecodeJSON(entry.Value, runner); err != nil {
return nil, fmt.Errorf("failed to decode plugin runtime entry: %w", err)
}
if runner.Type != prt {
return nil, nil
}
return runner, nil
}
// Set registers a new plugin with the catalog, or updates an existing plugin runtime
func (c *PluginRuntimeCatalog) Set(ctx context.Context, conf *pluginruntimeutil.PluginRuntimeConfig) error {
c.lock.Lock()
defer c.lock.Unlock()
if conf == nil {
return fmt.Errorf("plugin runtime config reference is nil")
}
buf, err := json.Marshal(conf)
if err != nil {
return fmt.Errorf("failed to encode plugin entry: %w", err)
}
storageKey := path.Join(conf.Type.String(), conf.Name)
logicalEntry := logical.StorageEntry{
Key: storageKey,
Value: buf,
}
if err := c.catalogView.Put(ctx, &logicalEntry); err != nil {
return fmt.Errorf("failed to persist plugin runtime entry: %w", err)
}
return err
}
// Delete is used to remove an external plugin from the catalog. Builtin plugins
// can not be deleted.
func (c *PluginRuntimeCatalog) Delete(ctx context.Context, name string, prt consts.PluginRuntimeType) error {
c.lock.Lock()
defer c.lock.Unlock()
storageKey := path.Join(prt.String(), name)
out, err := c.catalogView.Get(ctx, storageKey)
if err != nil || out == nil {
return ErrPluginRuntimeNotFound
}
return c.catalogView.Delete(ctx, storageKey)
}
func (c *PluginRuntimeCatalog) List(ctx context.Context, prt consts.PluginRuntimeType) ([]*pluginruntimeutil.PluginRuntimeConfig, error) {
c.lock.RLock()
defer c.lock.RUnlock()
var retList []*pluginruntimeutil.PluginRuntimeConfig
keys, err := logical.CollectKeys(ctx, c.catalogView)
if err != nil {
return nil, err
}
for _, key := range keys {
entry, err := c.catalogView.Get(ctx, key)
if err != nil || entry == nil {
continue
}
conf := new(pluginruntimeutil.PluginRuntimeConfig)
if err := jsonutil.DecodeJSON(entry.Value, conf); err != nil {
return nil, fmt.Errorf("failed to decode plugin runtime entry: %w", err)
}
if conf.Type != prt {
continue
}
retList = append(retList, conf)
}
return retList, nil
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package vault
import (
"context"
"reflect"
"testing"
"github.com/hashicorp/vault/sdk/helper/pluginruntimeutil"
)
func TestPluginRuntimeCatalog_CRUD(t *testing.T) {
core, _, _ := TestCoreUnsealed(t)
ctx := context.Background()
expected := &pluginruntimeutil.PluginRuntimeConfig{
Name: "gvisor",
OCIRuntime: "runsc",
CgroupParent: "/cpulimit/",
CPU: 1,
Memory: 10000,
}
// Set new plugin runtime
err := core.pluginRuntimeCatalog.Set(ctx, expected)
if err != nil {
t.Fatalf("err: %v", err)
}
// Get plugin runtime
runner, err := core.pluginRuntimeCatalog.Get(ctx, expected.Name, expected.Type)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(expected, runner) {
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", runner, expected)
}
// Set existing plugin runtime
expected.CgroupParent = "memorylimit-cgroup"
expected.CPU = 2
expected.Memory = 5000
err = core.pluginRuntimeCatalog.Set(ctx, expected)
if err != nil {
t.Fatalf("err: %v", err)
}
// Get plugin runtime again
runner, err = core.pluginRuntimeCatalog.Get(ctx, expected.Name, expected.Type)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(expected, runner) {
t.Fatalf("expected did not match actual, got %#v\n expected %#v\n", runner, expected)
}
configs, err := core.pluginRuntimeCatalog.List(ctx, expected.Type)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(configs) != 1 {
t.Fatalf("expected plugin runtime catalog to have 1 container runtime but got %d", len(configs))
}
// Delete plugin runtime
err = core.pluginRuntimeCatalog.Delete(ctx, expected.Name, expected.Type)
if err != nil {
t.Fatalf("err: %v", err)
}
// Assert the plugin runtime catalog is empty
configs, err = core.pluginRuntimeCatalog.List(ctx, expected.Type)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(configs) != 0 {
t.Fatalf("expected plugin runtime catalog to have 0 container runtimes but got %d", len(configs))
}
}