transit cache is an Interface implemented by wrapped versions of sync… (#6225)

* transit cache is an Interface implemented by wrapped versions of syncmap and golang-lru

* transit cache is an Interface implemented by wrapped versions of syncmap and golang-lru

* changed some import paths to point to sdk

* Apply suggestions from code review

Co-Authored-By: Lexman42 <Lexman42@users.noreply.github.com>

* updates docs with information on transit/cache-config endpoint

* updates vendored files

* fixes policy tests to actually use a cache where expected and renames the struct and storage path used for cache configurations to be more generic

* updates document links

* fixed a typo in a documentation link

* changes cache_size to just size for the cache-config endpoint
This commit is contained in:
Lexman 2019-06-04 15:40:56 -07:00 committed by Brian Kassouf
parent 4d0d70551d
commit 4ed616dacb
15 changed files with 547 additions and 36 deletions

View File

@ -4,20 +4,25 @@ import (
"context"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/keysutil"
"github.com/hashicorp/vault/sdk/logical"
)
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
b := Backend(conf)
b, err := Backend(ctx, conf)
if err != nil {
return nil, err
}
if err := b.Setup(ctx, conf); err != nil {
return nil, err
}
return b, nil
}
func Backend(conf *logical.BackendConfig) *backend {
func Backend(ctx context.Context, conf *logical.BackendConfig) (*backend, error) {
var b backend
b.Backend = &framework.Backend{
PathsSpecial: &logical.Paths{
@ -47,6 +52,7 @@ func Backend(conf *logical.BackendConfig) *backend {
b.pathBackup(),
b.pathRestore(),
b.pathTrim(),
b.pathCacheConfig(),
},
Secrets: []*framework.Secret{},
@ -54,9 +60,24 @@ func Backend(conf *logical.BackendConfig) *backend {
BackendType: logical.TypeLogical,
}
b.lm = keysutil.NewLockManager(conf.System.CachingDisabled())
// determine cacheSize to use. Defaults to 0 which means unlimited
cacheSize := 0
useCache := !conf.System.CachingDisabled()
if useCache {
var err error
cacheSize, err = GetCacheSizeFromStorage(ctx, conf.StorageView)
if err != nil {
return nil, errwrap.Wrapf("Error retrieving cache size from storage: {{err}}", err)
}
}
return &b
var err error
b.lm, err = keysutil.NewLockManager(useCache, cacheSize)
if err != nil {
return nil, err
}
return &b, nil
}
type backend struct {
@ -64,6 +85,22 @@ type backend struct {
lm *keysutil.LockManager
}
func GetCacheSizeFromStorage(ctx context.Context, s logical.Storage) (int, error) {
size := 0
entry, err := s.Get(ctx, "config/cache")
if err != nil {
return 0, err
}
if entry != nil {
var storedCache configCache
if err := entry.DecodeJSON(&storedCache); err != nil {
return 0, err
}
size = storedCache.Size
}
return size, nil
}
func (b *backend) invalidate(_ context.Context, key string) {
if b.Logger().IsDebug() {
b.Logger().Debug("invalidating key", "key", key)

View File

@ -30,7 +30,7 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b := Backend(config)
b, _ := Backend(context.Background(), config)
if b == nil {
t.Fatalf("failed to create backend")
}
@ -50,7 +50,7 @@ func createBackendWithSysView(t *testing.T) (*backend, logical.Storage) {
System: sysView,
}
b := Backend(conf)
b, _ := Backend(context.Background(), conf)
if b == nil {
t.Fatal("failed to create backend")
}
@ -63,6 +63,49 @@ func createBackendWithSysView(t *testing.T) (*backend, logical.Storage) {
return b, storage
}
func createBackendWithSysViewWithStorage(t *testing.T, s logical.Storage) *backend {
sysView := logical.TestSystemView()
conf := &logical.BackendConfig{
StorageView: s,
System: sysView,
}
b, _ := Backend(context.Background(), conf)
if b == nil {
t.Fatal("failed to create backend")
}
err := b.Backend.Setup(context.Background(), conf)
if err != nil {
t.Fatal(err)
}
return b
}
func createBackendWithForceNoCacheWithSysViewWithStorage(t *testing.T, s logical.Storage) *backend {
sysView := logical.TestSystemView()
sysView.CachingDisabledVal = true
conf := &logical.BackendConfig{
StorageView: s,
System: sysView,
}
b, _ := Backend(context.Background(), conf)
if b == nil {
t.Fatal("failed to create backend")
}
err := b.Backend.Setup(context.Background(), conf)
if err != nil {
t.Fatal(err)
}
return b
}
func TestTransit_RSA(t *testing.T) {
testTransit_RSA(t, "rsa-2048")
testTransit_RSA(t, "rsa-4096")
@ -1294,16 +1337,17 @@ func testConvergentEncryptionCommon(t *testing.T, ver int, keyType keysutil.KeyT
func TestPolicyFuzzing(t *testing.T) {
var be *backend
sysView := logical.TestSystemView()
sysView.CachingDisabledVal = true
conf := &logical.BackendConfig{
System: sysView,
}
be = Backend(conf)
be, _ = Backend(context.Background(), conf)
be.Setup(context.Background(), conf)
testPolicyFuzzingCommon(t, be)
sysView.CachingDisabledVal = true
be = Backend(conf)
be, _ = Backend(context.Background(), conf)
be.Setup(context.Background(), conf)
testPolicyFuzzingCommon(t, be)
}

View File

@ -0,0 +1,106 @@
package transit
import (
"context"
"errors"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
func (b *backend) pathCacheConfig() *framework.Path {
return &framework.Path{
Pattern: "cache-config",
Fields: map[string]*framework.FieldSchema{
"size": &framework.FieldSchema{
Type: framework.TypeInt,
Required: false,
Default: 0,
Description: `Size of cache, use 0 for an unlimited cache size, defaults to 0`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathCacheConfigRead,
Summary: "Returns the size of the active cache",
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathCacheConfigWrite,
Summary: "Configures a new cache of the specified size",
},
logical.CreateOperation: &framework.PathOperation{
Callback: b.pathCacheConfigWrite,
Summary: "Configures a new cache of the specified size",
},
},
HelpSynopsis: pathCacheConfigHelpSyn,
HelpDescription: pathCacheConfigHelpDesc,
}
}
func (b *backend) pathCacheConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// get target size
cacheSize := d.Get("size").(int)
if cacheSize < 0 {
return logical.ErrorResponse("size must be greater or equal to 0"), logical.ErrInvalidRequest
}
// store cache size
entry, err := logical.StorageEntryJSON("config/cache", &configCache{
Size: cacheSize,
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
resp := &logical.Response{
Warnings: []string{"cache configurations will be applied when this backend is restarted"},
}
return resp, nil
}
type configCache struct {
Size int `json:"size"`
}
func (b *backend) pathCacheConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// error if no cache is configured
if !b.lm.GetUseCache() {
return nil, errors.New(
"caching is disabled for this transit mount",
)
}
// Compare current and stored cache sizes. If they are different warn the user.
currentCacheSize := b.lm.GetCacheSize()
storedCacheSize, err := GetCacheSizeFromStorage(ctx, req.Storage)
if err != nil {
return nil, err
}
resp := &logical.Response{
Data: map[string]interface{}{
"size": storedCacheSize,
},
}
if currentCacheSize != storedCacheSize {
resp.Warnings = []string{"This cache size will not be applied until the transit mount is reloaded"}
}
return resp, nil
}
const pathCacheConfigHelpSyn = `Configure caching strategy`
const pathCacheConfigHelpDesc = `
This path is used to configure and query the cache size of the active cache, a size of 0 means unlimited.
`

View File

@ -0,0 +1,80 @@
package transit
import (
"context"
"testing"
"github.com/hashicorp/vault/sdk/logical"
)
const targetCacheSize = 12345
func TestTransit_CacheConfig(t *testing.T) {
b1, storage := createBackendWithSysView(t)
doReq := func(b *backend, req *logical.Request) *logical.Response {
resp, err := b.HandleRequest(context.Background(), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("got err:\n%#v\nreq:\n%#v\n", err, *req)
}
return resp
}
doErrReq := func(b *backend, req *logical.Request) {
resp, err := b.HandleRequest(context.Background(), req)
if err == nil {
if resp == nil || !resp.IsError() {
t.Fatalf("expected error; req:\n%#v\n", *req)
}
}
}
validateResponse := func(resp *logical.Response, expectedCacheSize int, expectedWarning bool) {
actualCacheSize, ok := resp.Data["size"].(int)
if !ok {
t.Fatalf("No size returned")
}
if expectedCacheSize != actualCacheSize {
t.Fatalf("testAccReadCacheConfig expected: %d got: %d", expectedCacheSize, actualCacheSize)
}
// check for the presence/absence of warnings - warnings are expected if a cache size has been
// configured but not yet applied by reloading the plugin
warningCheckPass := expectedWarning == (len(resp.Warnings) > 0)
if !warningCheckPass {
t.Fatalf(
"testAccSteporeadCacheConfig warnings error.\n"+
"expect warnings: %t but number of warnings was: %d",
expectedWarning, len(resp.Warnings),
)
}
}
writeReq := &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "cache-config",
Data: map[string]interface{}{
"size": targetCacheSize,
},
}
readReq := &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: "cache-config",
}
// test steps
// b1 should spin up with an unlimited cache
validateResponse(doReq(b1, readReq), 0, false)
doReq(b1, writeReq)
validateResponse(doReq(b1, readReq), targetCacheSize, true)
// b2 should spin up with a configured cache
b2 := createBackendWithSysViewWithStorage(t, storage)
validateResponse(doReq(b2, readReq), targetCacheSize, false)
// b3 enables transit without a cache, trying to read it should error
b3 := createBackendWithForceNoCacheWithSysViewWithStorage(t, storage)
doErrReq(b3, readReq)
}

View File

@ -14,8 +14,9 @@ func TestTransit_Random(t *testing.T) {
var b *backend
sysView := logical.TestSystemView()
storage := &logical.InmemStorage{}
sysView.CachingDisabledVal = true
b = Backend(&logical.BackendConfig{
b, _ = Backend(context.Background(), &logical.BackendConfig{
StorageView: storage,
System: sysView,
})

View File

@ -0,0 +1,8 @@
package keysutil
type Cache interface {
Delete(key interface{})
Load(key interface{}) (value interface{}, ok bool)
Store(key, value interface{})
Size() int
}

View File

@ -55,21 +55,44 @@ type PolicyRequest struct {
type LockManager struct {
useCache bool
// If caching is enabled, the map of name to in-memory policy cache
cache sync.Map
cache Cache
keyLocks []*locksutil.LockEntry
}
func NewLockManager(cacheDisabled bool) *LockManager {
func NewLockManager(useCache bool, cacheSize int) (*LockManager, error) {
// determine the type of cache to create
var cache Cache
switch {
case !useCache:
case cacheSize < 0:
return nil, errors.New("cache size must be greater or equal to zero")
case cacheSize == 0:
cache = NewTransitSyncMap()
case cacheSize > 0:
newLRUCache, err := NewTransitLRU(cacheSize)
if err != nil {
return nil, errwrap.Wrapf("failed to create cache: {{err}}", err)
}
cache = newLRUCache
}
lm := &LockManager{
useCache: !cacheDisabled,
useCache: useCache,
cache: cache,
keyLocks: locksutil.CreateLocks(),
}
return lm
return lm, nil
}
func (lm *LockManager) CacheActive() bool {
func (lm *LockManager) GetCacheSize() int {
if !lm.useCache {
return 0
}
return lm.cache.Size()
}
func (lm *LockManager) GetUseCache() bool {
return lm.useCache
}
@ -178,7 +201,6 @@ func (lm *LockManager) RestorePolicy(ctx context.Context, storage logical.Storag
if lm.useCache {
lm.cache.Store(name, keyData.Policy)
}
return nil
}
@ -186,7 +208,7 @@ func (lm *LockManager) BackupPolicy(ctx context.Context, storage logical.Storage
var p *Policy
var err error
// Backup writes information about when the bacup took place, so we get an
// Backup writes information about when the backup took place, so we get an
// exclusive lock here
lock := locksutil.LockForKey(lm.keyLocks, name)
lock.Lock()

View File

@ -52,8 +52,10 @@ func TestPolicy_KeyEntryMapUpgrade(t *testing.T) {
}
func Test_KeyUpgrade(t *testing.T) {
testKeyUpgradeCommon(t, NewLockManager(false))
testKeyUpgradeCommon(t, NewLockManager(true))
lockManagerWithCache, _ := NewLockManager(true, 0)
lockManagerWithoutCache, _ := NewLockManager(false, 0)
testKeyUpgradeCommon(t, lockManagerWithCache)
testKeyUpgradeCommon(t, lockManagerWithoutCache)
}
func testKeyUpgradeCommon(t *testing.T, lm *LockManager) {
@ -97,8 +99,10 @@ func testKeyUpgradeCommon(t *testing.T, lm *LockManager) {
}
func Test_ArchivingUpgrade(t *testing.T) {
testArchivingUpgradeCommon(t, NewLockManager(false))
testArchivingUpgradeCommon(t, NewLockManager(true))
lockManagerWithCache, _ := NewLockManager(true, 0)
lockManagerWithoutCache, _ := NewLockManager(false, 0)
testArchivingUpgradeCommon(t, lockManagerWithCache)
testArchivingUpgradeCommon(t, lockManagerWithoutCache)
}
func testArchivingUpgradeCommon(t *testing.T, lm *LockManager) {
@ -255,8 +259,10 @@ func testArchivingUpgradeCommon(t *testing.T, lm *LockManager) {
}
func Test_Archiving(t *testing.T) {
testArchivingCommon(t, NewLockManager(false))
testArchivingCommon(t, NewLockManager(true))
lockManagerWithCache, _ := NewLockManager(true, 0)
lockManagerWithoutCache, _ := NewLockManager(false, 0)
testArchivingUpgradeCommon(t, lockManagerWithCache)
testArchivingUpgradeCommon(t, lockManagerWithoutCache)
}
func testArchivingCommon(t *testing.T, lm *LockManager) {
@ -420,7 +426,7 @@ func checkKeys(t *testing.T,
func Test_StorageErrorSafety(t *testing.T) {
ctx := context.Background()
lm := NewLockManager(false)
lm, _ := NewLockManager(true, 0)
storage := &logical.InmemStorage{}
p, _, err := lm.GetPolicy(ctx, PolicyRequest{
@ -468,7 +474,7 @@ func Test_StorageErrorSafety(t *testing.T) {
func Test_BadUpgrade(t *testing.T) {
ctx := context.Background()
lm := NewLockManager(false)
lm, _ := NewLockManager(true, 0)
storage := &logical.InmemStorage{}
p, _, err := lm.GetPolicy(ctx, PolicyRequest{
Upsert: true,
@ -533,7 +539,7 @@ func Test_BadUpgrade(t *testing.T) {
func Test_BadArchive(t *testing.T) {
ctx := context.Background()
lm := NewLockManager(false)
lm, _ := NewLockManager(true, 0)
storage := &logical.InmemStorage{}
p, _, err := lm.GetPolicy(ctx, PolicyRequest{
Upsert: true,

View File

@ -0,0 +1,29 @@
package keysutil
import lru "github.com/hashicorp/golang-lru"
type TransitLRU struct {
size int
lru *lru.TwoQueueCache
}
func NewTransitLRU(size int) (*TransitLRU, error) {
lru, err := lru.New2Q(size)
return &TransitLRU{lru: lru, size: size}, err
}
func (c *TransitLRU) Delete(key interface{}) {
c.lru.Remove(key)
}
func (c *TransitLRU) Load(key interface{}) (value interface{}, ok bool) {
return c.lru.Get(key)
}
func (c *TransitLRU) Store(key, value interface{}) {
c.lru.Add(key, value)
}
func (c *TransitLRU) Size() int {
return c.size
}

View File

@ -0,0 +1,29 @@
package keysutil
import (
"sync"
)
type TransitSyncMap struct {
syncmap sync.Map
}
func NewTransitSyncMap() *TransitSyncMap {
return &TransitSyncMap{syncmap: sync.Map{}}
}
func (c *TransitSyncMap) Delete(key interface{}) {
c.syncmap.Delete(key)
}
func (c *TransitSyncMap) Load(key interface{}) (value interface{}, ok bool) {
return c.syncmap.Load(key)
}
func (c *TransitSyncMap) Store(key, value interface{}) {
c.syncmap.Store(key, value)
}
func (c *TransitSyncMap) Size() int {
return 0
}

View File

@ -0,0 +1,8 @@
package keysutil
type Cache interface {
Delete(key interface{})
Load(key interface{}) (value interface{}, ok bool)
Store(key, value interface{})
Size() int
}

View File

@ -55,21 +55,44 @@ type PolicyRequest struct {
type LockManager struct {
useCache bool
// If caching is enabled, the map of name to in-memory policy cache
cache sync.Map
cache Cache
keyLocks []*locksutil.LockEntry
}
func NewLockManager(cacheDisabled bool) *LockManager {
func NewLockManager(useCache bool, cacheSize int) (*LockManager, error) {
// determine the type of cache to create
var cache Cache
switch {
case !useCache:
case cacheSize < 0:
return nil, errors.New("cache size must be greater or equal to zero")
case cacheSize == 0:
cache = NewTransitSyncMap()
case cacheSize > 0:
newLRUCache, err := NewTransitLRU(cacheSize)
if err != nil {
return nil, errwrap.Wrapf("failed to create cache: {{err}}", err)
}
cache = newLRUCache
}
lm := &LockManager{
useCache: !cacheDisabled,
useCache: useCache,
cache: cache,
keyLocks: locksutil.CreateLocks(),
}
return lm
return lm, nil
}
func (lm *LockManager) CacheActive() bool {
func (lm *LockManager) GetCacheSize() int {
if !lm.useCache {
return 0
}
return lm.cache.Size()
}
func (lm *LockManager) GetUseCache() bool {
return lm.useCache
}
@ -178,7 +201,6 @@ func (lm *LockManager) RestorePolicy(ctx context.Context, storage logical.Storag
if lm.useCache {
lm.cache.Store(name, keyData.Policy)
}
return nil
}
@ -186,7 +208,7 @@ func (lm *LockManager) BackupPolicy(ctx context.Context, storage logical.Storage
var p *Policy
var err error
// Backup writes information about when the bacup took place, so we get an
// Backup writes information about when the backup took place, so we get an
// exclusive lock here
lock := locksutil.LockForKey(lm.keyLocks, name)
lock.Lock()

View File

@ -0,0 +1,29 @@
package keysutil
import lru "github.com/hashicorp/golang-lru"
type TransitLRU struct {
size int
lru *lru.TwoQueueCache
}
func NewTransitLRU(size int) (*TransitLRU, error) {
lru, err := lru.New2Q(size)
return &TransitLRU{lru: lru, size: size}, err
}
func (c *TransitLRU) Delete(key interface{}) {
c.lru.Remove(key)
}
func (c *TransitLRU) Load(key interface{}) (value interface{}, ok bool) {
return c.lru.Get(key)
}
func (c *TransitLRU) Store(key, value interface{}) {
c.lru.Add(key, value)
}
func (c *TransitLRU) Size() int {
return c.size
}

View File

@ -0,0 +1,29 @@
package keysutil
import (
"sync"
)
type TransitSyncMap struct {
syncmap sync.Map
}
func NewTransitSyncMap() *TransitSyncMap {
return &TransitSyncMap{syncmap: sync.Map{}}
}
func (c *TransitSyncMap) Delete(key interface{}) {
c.syncmap.Delete(key)
}
func (c *TransitSyncMap) Load(key interface{}) (value interface{}, ok bool) {
return c.syncmap.Load(key)
}
func (c *TransitSyncMap) Store(key, value interface{}) {
c.syncmap.Store(key, value)
}
func (c *TransitSyncMap) Size() int {
return 0
}

View File

@ -1252,3 +1252,64 @@ $ curl \
--data @payload.json \
http://127.0.0.1:8200/v1/transit/keys/my-key/trim
```
## Configure Cache
This endpoint is used to configure the transit engine's cache. Note that configuration
changes will not be applied until the transit plugin is reloaded which can be achieved
using the [`/sys/plugins/reload/backend`][sys-plugin-reload-backend] endpoint.
| Method | Path |
| :------------------------- | :--------------------- |
| `POST` | `/transit/cache-config` |
### Parameters
- `size` `(int: 0)` - Specifies the size in terms of number of entries. A size of
`0` means unlimited. A _Least Recently Used_ (LRU) caching strategy is used for a
non-zero cache size.
### Sample Payload
```json
{
"size": 456
}
```
### Sample Request
```
$ curl \
--header "X-Vault-Token: ..."
--request POST \
--data @payload.json \
http://127.0.0.1:8200/v1/transit/cache-config
```
## Read Transit Cache Configuration
This endpoint retrieves configurations for the transit engine's cache.
| Method | Path |
| :------------------------- | :--------------------- |
| `GET` | `/transit/cache-config` |
### Sample Request
```
$ curl \
--header "X-Vault-Token: ..."
--request GET \
http://127.0.0.1:8200/v1/transit/cache-config
```
### Sample Response
```json
"data": {
"size": 0
},
```
[sys-plugin-reload-backend]: /api/system/plugins-reload-backend.html#reload-plugins