Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Commit

Permalink
Give system admins ability to clear caches (#834)
Browse files Browse the repository at this point in the history
* Give system admins ability to clear caches

In some situations, a system admin may want to clear the caches, either
for development/testing or because a misconfiguration has made it into
the system. This introduces a way for system admins to perform cache
resets.

This required a bit of a refactor of the cacher, separating the
namespace property from the actual key value.

* Update descriptions

* Use public_keys
  • Loading branch information
sethvargo authored Oct 15, 2020
1 parent c29b6a2 commit 7a6fb4a
Show file tree
Hide file tree
Showing 32 changed files with 393 additions and 156 deletions.
5 changes: 1 addition & 4 deletions cmd/adminapi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,7 @@ func realMain(ctx context.Context) error {
logger.Infow("observability exporter", "config", oeConfig)

// Setup cacher
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.MultiKeyFunc(
cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey),
cache.PrefixKeyFunc("cache:"),
))
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey))
if err != nil {
return fmt.Errorf("failed to create cacher: %w", err)
}
Expand Down
5 changes: 1 addition & 4 deletions cmd/apiserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,7 @@ func realMain(ctx context.Context) error {
logger.Infow("observability exporter", "config", oeConfig)

// Setup cacher
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.MultiKeyFunc(
cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey),
cache.PrefixKeyFunc("cache:"),
))
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey))
if err != nil {
return fmt.Errorf("failed to create cacher: %w", err)
}
Expand Down
5 changes: 1 addition & 4 deletions cmd/enx-redirect/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,7 @@ func realMain(ctx context.Context) error {
logger.Infow("observability exporter", "config", oeConfig)

// Setup cacher
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.MultiKeyFunc(
cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey),
cache.PrefixKeyFunc("cache:"),
))
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey))
if err != nil {
return fmt.Errorf("failed to create cacher: %w", err)
}
Expand Down
5 changes: 1 addition & 4 deletions cmd/modeler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,7 @@ func realMain(ctx context.Context) error {
logger.Infow("observability exporter", "config", oeConfig)

// Setup cacher
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.MultiKeyFunc(
cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey),
cache.PrefixKeyFunc("cache:"),
))
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey))
if err != nil {
return fmt.Errorf("failed to create cacher: %w", err)
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/server/assets/admin/_nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
<a class="nav-link{{if .currentPath.IsDir "/admin/sms"}} active{{end}}" href="/admin/sms">SMS</a>
</li>

<li class="nav-item">
<a class="nav-link{{if .currentPath.IsDir "/admin/caches"}} active{{end}}" href="/admin/caches">Caches</a>
</li>

<li class="nav-item">
<a class="nav-link{{if .currentPath.IsDir "/admin/info"}} active{{end}}" href="/admin/info">Info</a>
</li>
Expand Down
41 changes: 41 additions & 0 deletions cmd/server/assets/admin/caches/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{{define "admin/caches/index"}}

{{$caches := .caches}}

<!doctype html>
<html lang="en">
<head>
{{template "head" .}}
</head>

<body class="tab-content">
{{template "admin/navbar" .}}

<main role="main" class="container">
{{template "flash" .}}

<div class="card mb-3 shadow-sm">
<div class="card-header">Caches</div>
<ul class="list-group list-group-flush">
{{range $k, $cache := $caches}}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-1">{{$cache.Name}}</h5>
<small>{{$cache.Description}}</small>
</div>
<a href="/admin/caches/clear/{{$k}}"
data-method="POST"
data-confirm="Are you sure you want to clear the {{$cache.Name}} cache?"
class="btn btn-danger">
Clear
</a>
</li>
{{end}}
</ul>
</div>
</main>

{{template "scripts" .}}
</body>
</html>
{{end}}
10 changes: 5 additions & 5 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,7 @@ func realMain(ctx context.Context) error {
sessions.Options.SameSite = http.SameSiteStrictMode

// Setup cacher
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.MultiKeyFunc(
cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey),
cache.PrefixKeyFunc("cache:"),
))
cacher, err := cache.CacherFor(ctx, &cfg.Cache, cache.HMACKeyFunc(sha1.New, cfg.Cache.HMACKey))
if err != nil {
return fmt.Errorf("failed to create cacher: %w", err)
}
Expand Down Expand Up @@ -444,7 +441,7 @@ func realMain(ctx context.Context) error {
adminSub.Use(requireSystemAdmin)
adminSub.Use(rateLimit)

adminController := admin.New(ctx, cfg, db, auth, h)
adminController := admin.New(ctx, cfg, cacher, db, auth, h)
adminSub.Handle("", http.RedirectHandler("/admin/realms", http.StatusSeeOther)).Methods("GET")
adminSub.Handle("/realms", adminController.HandleRealmsIndex()).Methods("GET")
adminSub.Handle("/realms", adminController.HandleRealmsCreate()).Methods("POST")
Expand All @@ -461,6 +458,9 @@ func realMain(ctx context.Context) error {
adminSub.Handle("/users/new", adminController.HandleUsersCreate()).Methods("GET")
adminSub.Handle("/users/{id:[0-9]+}", adminController.HandleUsersDelete()).Methods("DELETE")

adminSub.Handle("/caches", adminController.HandleCachesIndex()).Methods("GET")
adminSub.Handle("/caches/clear/{id}", adminController.HandleCachesClear()).Methods("POST")

adminSub.Handle("/info", adminController.HandleInfoShow()).Methods("GET")
}

Expand Down
50 changes: 35 additions & 15 deletions pkg/cache/cacher.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,33 @@ var (
// FetchFunc is a function used to Fetch in a cacher.
type FetchFunc func() (interface{}, error)

// Key is a cache key. It has an optional Namespace. Any KeyFunc are applied to
// the Key, but not the Namespace.
type Key struct {
Namespace string
Key string
}

// Compute builds the final value of the key. If f is not nil, it is called on
// the value of Key. If Namespace is not "", the result is prefixed with
// Namespace + ":".
func (k *Key) Compute(f KeyFunc) (string, error) {
key := k.Key

if f != nil {
var err error
key, err = f(key)
if err != nil {
return "", err
}
}

if k.Namespace != "" {
return k.Namespace + ":" + key, nil
}
return key, nil
}

// KeyFunc is a function that mutates the provided cache key before storing it
// in Redis. This can be used to hash or HMAC values to prevent their plaintext
// from appearing in Redis. A good example might be an API key lookup that you
Expand All @@ -58,17 +85,6 @@ func MultiKeyFunc(fns ...KeyFunc) KeyFunc {
}
}

// PrefixKeyFunc returns a KeyFunc that prefixes the key with the given constant
// before passing it to the cacher for storage.
func PrefixKeyFunc(prefix string) KeyFunc {
return func(in string) (string, error) {
if prefix != "" {
in = prefix + in
}
return in, nil
}
}

// HashKeyFunc returns a KeyFunc that hashes the provided key before passing it
// to the cacher for storage.
func HashKeyFunc(hasher func() hash.Hash) KeyFunc {
Expand Down Expand Up @@ -105,16 +121,20 @@ type Cacher interface {
// it calls FetchFunc to create the item. If FetchFunc returns an error, the
// error is bubbled up the stack and no value is cached. If FetchFunc
// succeeds, the value is cached for the provided TTL.
Fetch(context.Context, string, interface{}, time.Duration, FetchFunc) error
Fetch(context.Context, *Key, interface{}, time.Duration, FetchFunc) error

// Read gets an item from the cache and reads it into the provided interface.
// If it does not exist, it returns ErrNotFound.
Read(context.Context, string, interface{}) error
Read(context.Context, *Key, interface{}) error

// Write adds an item to the cache, overwriting if it already exists, caching
// for TTL. It returns any errors that occur on writing.
Write(context.Context, string, interface{}, time.Duration) error
Write(context.Context, *Key, interface{}, time.Duration) error

// Delete removes an item from the cache, returning any errors that occur.
Delete(context.Context, string) error
Delete(context.Context, *Key) error

// DeletePrefix removes all items from the cache that begin with the given
// value.
DeletePrefix(context.Context, string) error
}
44 changes: 39 additions & 5 deletions pkg/cache/cacher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,21 @@ func exerciseCacher(t *testing.T, cacher Cacher) {
t.Fatal(err)
}

if err := cacher.Read(ctx, "foo", nil); !errors.Is(err, ErrStopped) {
key := &Key{
Namespace: "foo",
Key: "bar",
}

if err := cacher.Read(ctx, key, nil); !errors.Is(err, ErrStopped) {
t.Errorf("expected %#v to be %#v", err, ErrStopped)
}
if err := cacher.Write(ctx, "foo", nil, 0); !errors.Is(err, ErrStopped) {
if err := cacher.Write(ctx, key, nil, 0); !errors.Is(err, ErrStopped) {
t.Errorf("expected %#v to be %#v", err, ErrStopped)
}
if err := cacher.Delete(ctx, "foo"); !errors.Is(err, ErrStopped) {
if err := cacher.Delete(ctx, key); !errors.Is(err, ErrStopped) {
t.Errorf("expected %#v to be %#v", err, ErrStopped)
}
if err := cacher.Fetch(ctx, "foo", nil, 0, nil); !errors.Is(err, ErrStopped) {
if err := cacher.Fetch(ctx, key, nil, 0, nil); !errors.Is(err, ErrStopped) {
t.Errorf("expected %#v to be %#v", err, ErrStopped)
}

Expand All @@ -138,7 +143,10 @@ func exerciseType(tb testing.TB, cacher Cacher, in, out interface{}) {
tb.Helper()

ctx := context.Background()
key := testRandomKey(tb)
key := &Key{
Namespace: testRandomKey(tb),
Key: testRandomKey(tb),
}

// Ensure the value isn't cached already
if err := cacher.Read(ctx, key, out); !errors.Is(err, ErrNotFound) {
Expand Down Expand Up @@ -188,4 +196,30 @@ func exerciseType(tb testing.TB, cacher Cacher, in, out interface{}) {
if err := cacher.Delete(ctx, key); err != nil {
tb.Fatal(err)
}

// Ensure value is deleted
if err := cacher.Read(ctx, key, out); !errors.Is(err, ErrNotFound) {
tb.Fatalf("expected %#v to be %#v", err, ErrNotFound)
}

// Create caches with the same namespace
for i := 0; i < 10; i++ {
key = &Key{
Namespace: key.Namespace,
Key: testRandomKey(tb),
}
if err := cacher.Write(ctx, key, in, 5*time.Minute); err != nil {
tb.Fatal(err)
}
}

// Delete the prefix
if err := cacher.DeletePrefix(ctx, key.Namespace); err != nil {
tb.Fatal(err)
}

// Ensure value is deleted
if err := cacher.Read(ctx, key, out); !errors.Is(err, ErrNotFound) {
tb.Fatalf("expected %#v to be %#v", err, ErrNotFound)
}
}
Loading

0 comments on commit 7a6fb4a

Please sign in to comment.