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

Give system admins ability to clear caches #834

Merged
merged 3 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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