chore: add global caching to rbac (#7439)

Co-authored-by: Steven Masley <stevenmasley@coder.com>
This commit is contained in:
Ammar Bandukwala 2023-05-08 08:59:01 -05:00 committed by GitHub
parent 643a9efea9
commit 8899dd89ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 121 additions and 125 deletions

1
.gitignore vendored
View File

@ -53,3 +53,4 @@ site/stats/
# direnv # direnv
.envrc .envrc
*.test

View File

@ -56,6 +56,7 @@ site/stats/
# direnv # direnv
.envrc .envrc
*.test
# .prettierignore.include: # .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted # Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier. # by Prettier.

View File

@ -398,7 +398,6 @@ func New(options *Options) *API {
tracing.StatusWriterMiddleware, tracing.StatusWriterMiddleware,
tracing.Middleware(api.TracerProvider), tracing.Middleware(api.TracerProvider),
httpmw.AttachRequestID, httpmw.AttachRequestID,
httpmw.AttachAuthzCache,
httpmw.ExtractRealIP(api.RealIPConfig), httpmw.ExtractRealIP(api.RealIPConfig),
httpmw.Logger(api.Logger), httpmw.Logger(api.Logger),
httpmw.Prometheus(options.PrometheusRegistry), httpmw.Prometheus(options.PrometheusRegistry),

View File

@ -137,7 +137,7 @@ type Options struct {
} }
// New constructs a codersdk client connected to an in-memory API instance. // New constructs a codersdk client connected to an in-memory API instance.
func New(t *testing.T, options *Options) *codersdk.Client { func New(t testing.TB, options *Options) *codersdk.Client {
client, _ := newWithCloser(t, options) client, _ := newWithCloser(t, options)
return client return client
} }
@ -162,12 +162,12 @@ func NewWithProvisionerCloser(t *testing.T, options *Options) (*codersdk.Client,
// upon thee. Even the io.Closer that is exposed here shouldn't be exposed // upon thee. Even the io.Closer that is exposed here shouldn't be exposed
// and is a temporary measure while the API to register provisioners is ironed // and is a temporary measure while the API to register provisioners is ironed
// out. // out.
func newWithCloser(t *testing.T, options *Options) (*codersdk.Client, io.Closer) { func newWithCloser(t testing.TB, options *Options) (*codersdk.Client, io.Closer) {
client, closer, _ := NewWithAPI(t, options) client, closer, _ := NewWithAPI(t, options)
return client, closer return client, closer
} }
func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.CancelFunc, *url.URL, *coderd.Options) { func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.CancelFunc, *url.URL, *coderd.Options) {
if options == nil { if options == nil {
options = &Options{} options = &Options{}
} }
@ -190,8 +190,14 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
} }
if options.Authorizer == nil { if options.Authorizer == nil {
options.Authorizer = &RecordingAuthorizer{ defAuth := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
Wrapped: rbac.NewCachingAuthorizer(prometheus.NewRegistry()), if _, ok := t.(*testing.T); ok {
options.Authorizer = &RecordingAuthorizer{
Wrapped: defAuth,
}
} else {
// In benchmarks, the recording authorizer greatly skews results.
options.Authorizer = defAuth
} }
} }
@ -359,7 +365,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
// NewWithAPI constructs an in-memory API instance and returns a client to talk to it. // NewWithAPI constructs an in-memory API instance and returns a client to talk to it.
// Most tests never need a reference to the API, but AuthorizationTest in this module uses it. // Most tests never need a reference to the API, but AuthorizationTest in this module uses it.
// Do not expose the API or wrath shall descend upon thee. // Do not expose the API or wrath shall descend upon thee.
func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *coderd.API) { func NewWithAPI(t testing.TB, options *Options) (*codersdk.Client, io.Closer, *coderd.API) {
if options == nil { if options == nil {
options = &Options{} options = &Options{}
} }
@ -384,7 +390,7 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
// NewProvisionerDaemon launches a provisionerd instance configured to work // NewProvisionerDaemon launches a provisionerd instance configured to work
// well with coderd testing. It registers the "echo" provisioner for // well with coderd testing. It registers the "echo" provisioner for
// quick testing. // quick testing.
func NewProvisionerDaemon(t *testing.T, coderAPI *coderd.API) io.Closer { func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer {
echoClient, echoServer := provisionersdk.MemTransportPipe() echoClient, echoServer := provisionersdk.MemTransportPipe()
ctx, cancelFunc := context.WithCancel(context.Background()) ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(func() { t.Cleanup(func() {
@ -465,7 +471,7 @@ var FirstUserParams = codersdk.CreateFirstUserRequest{
// CreateFirstUser creates a user with preset credentials and authenticates // CreateFirstUser creates a user with preset credentials and authenticates
// with the passed in codersdk client. // with the passed in codersdk client.
func CreateFirstUser(t *testing.T, client *codersdk.Client) codersdk.CreateFirstUserResponse { func CreateFirstUser(t testing.TB, client *codersdk.Client) codersdk.CreateFirstUserResponse {
resp, err := client.CreateFirstUser(context.Background(), FirstUserParams) resp, err := client.CreateFirstUser(context.Background(), FirstUserParams)
require.NoError(t, err) require.NoError(t, err)
@ -1111,7 +1117,7 @@ sz9Di8sGIaUbLZI2rd0CQQCzlVwEtRtoNCyMJTTrkgUuNufLP19RZ5FpyXxBO5/u
QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8 QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
func DeploymentValues(t *testing.T) *codersdk.DeploymentValues { func DeploymentValues(t testing.TB) *codersdk.DeploymentValues {
var cfg codersdk.DeploymentValues var cfg codersdk.DeploymentValues
opts := cfg.Options() opts := cfg.Options()
err := opts.SetDefaults() err := opts.SetDefaults()

View File

@ -13,7 +13,7 @@ import (
"github.com/coder/coder/coderd/database/postgres" "github.com/coder/coder/coderd/database/postgres"
) )
func NewDB(t *testing.T) (database.Store, database.Pubsub) { func NewDB(t testing.TB) (database.Store, database.Pubsub) {
t.Helper() t.Helper()
db := dbfake.New() db := dbfake.New()

View File

@ -6,7 +6,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/coder/coder/coderd/database/dbauthz" "github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/rbac"
) )
// AsAuthzSystem is a chained handler that temporarily sets the dbauthz context // AsAuthzSystem is a chained handler that temporarily sets the dbauthz context
@ -36,16 +35,3 @@ func AsAuthzSystem(mws ...func(http.Handler) http.Handler) func(http.Handler) ht
}) })
} }
} }
// AttachAuthzCache enables the authz cache for the authorizer. All rbac checks will
// run against the cache, meaning duplicate checks will not be performed.
//
// Note the cache is safe for multiple actors. So mixing user and system checks
// is ok.
func AttachAuthzCache(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := rbac.WithCacheCtx(r.Context())
next.ServeHTTP(rw, r.WithContext(ctx))
})
}

View File

@ -2,11 +2,14 @@ package rbac
import ( import (
"context" "context"
"crypto/sha256"
_ "embed" _ "embed"
"encoding/json"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/ammario/tlru"
"github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego" "github.com/open-policy-agent/opa/rego"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -42,6 +45,31 @@ type AuthCall struct {
Object Object Object Object
} }
// hashAuthorizeCall guarantees a unique hash for a given auth call.
// If two hashes are equal, then the result of a given authorize() call
// will be the same.
//
// Note that this ignores some fields such as the permissions within a given
// role, as this assumes all roles are static to a given role name.
func hashAuthorizeCall(actor Subject, action Action, object Object) [32]byte {
var hashOut [32]byte
hash := sha256.New()
// We use JSON for the forward security benefits if the rbac structs are
// modified without consideration for the caching layer.
enc := json.NewEncoder(hash)
_ = enc.Encode(actor)
_ = enc.Encode(action)
_ = enc.Encode(object)
// We might be able to avoid this extra copy?
// sha256.Sum256() returns a [32]byte. We need to return
// an array vs a slice so we can use it as a key in the cache.
image := hash.Sum(nil)
copy(hashOut[:], image)
return hashOut
}
// Subject is a struct that contains all the elements of a subject in an rbac // Subject is a struct that contains all the elements of a subject in an rbac
// authorize. // authorize.
type Subject struct { type Subject struct {
@ -101,6 +129,9 @@ func (s Subject) SafeRoleNames() []string {
} }
type Authorizer interface { type Authorizer interface {
// Authorize will authorize the given subject to perform the given action
// on the given object. Authorize is pure and deterministic with respect to
// its arguments and the surrounding object.
Authorize(ctx context.Context, subject Subject, action Action, object Object) error Authorize(ctx context.Context, subject Subject, action Action, object Object) error
Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error)
} }
@ -310,6 +341,7 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action A
defer span.End() defer span.End()
err := a.authorize(ctx, subject, action, object) err := a.authorize(ctx, subject, action, object)
span.SetAttributes(attribute.Bool("authorized", err == nil)) span.SetAttributes(attribute.Bool("authorized", err == nil))
dur := time.Since(start) dur := time.Since(start)
@ -605,7 +637,12 @@ func (a *authorizedSQLFilter) SQLString() string {
return a.sqlString return a.sqlString
} }
type cachedCalls struct { type authCache struct {
// cache is a cache of hashed Authorize inputs to the result of the Authorize
// call.
// determistic function.
cache *tlru.Cache[[32]byte, error]
authz Authorizer authz Authorizer
} }
@ -617,94 +654,35 @@ type cachedCalls struct {
// //
// Cacher is safe for multiple actors. // Cacher is safe for multiple actors.
func Cacher(authz Authorizer) Authorizer { func Cacher(authz Authorizer) Authorizer {
return &cachedCalls{authz: authz} return &authCache{
authz: authz,
// In practice, this cache should never come close to filling since the
// authorization calls are kept for a minute at most.
cache: tlru.New[[32]byte](tlru.ConstantCost[error], 64*1024),
}
} }
func (c *cachedCalls) Authorize(ctx context.Context, subject Subject, action Action, object Object) error { func (c *authCache) Authorize(ctx context.Context, subject Subject, action Action, object Object) error {
cache := cacheFromContext(ctx) authorizeCacheKey := hashAuthorizeCall(subject, action, object)
resp, ok := cache.Load(subject, action, object) var err error
if ok { err, _, ok := c.cache.Get(authorizeCacheKey)
return resp if !ok {
err = c.authz.Authorize(ctx, subject, action, object)
// In case there is a caching bug, bound the TTL to 1 minute.
c.cache.Set(authorizeCacheKey, err, time.Minute)
} }
err := c.authz.Authorize(ctx, subject, action, object)
cache.Save(subject, action, object, err)
return err return err
} }
// Prepare returns the underlying PreparedAuthorized. The cache does not apply // Prepare returns the underlying PreparedAuthorized. The cache does not apply
// to prepared authorizations. These should be using a SQL filter, and // to prepared authorizations. These should be using a SQL filter, and
// therefore the cache is not needed. // therefore the cache is not needed.
func (c *cachedCalls) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) { func (c *authCache) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) {
return c.authz.Prepare(ctx, subject, action, objectType) return c.authz.Prepare(ctx, subject, action, objectType)
} }
// authorizeCache enabled caching of Authorizer calls for a given request. This
// prevents the cost of running the same rbac checks multiple times.
// A cache hit must match on all 3 values: subject, action, and object.
type authorizeCache struct {
sync.Mutex
// calls is a list of all calls made to the Authorizer.
// This list is cached per request context. The size of this list is expected
// to be incredibly small. Often 1 or 2 calls.
calls []cachedAuthCall
}
type cachedAuthCall struct {
AuthCall
Err error
}
// cacheContextKey is a context key used to store the cache in the context.
type cacheContextKey struct{}
// cacheFromContext returns the cache from the context.
// If there is no cache, a nil value is returned.
// The nil cache can still be called as a normal cache, but will not cache or
// return any values.
func cacheFromContext(ctx context.Context) *authorizeCache {
cache, _ := ctx.Value(cacheContextKey{}).(*authorizeCache)
return cache
}
func WithCacheCtx(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey{}, &authorizeCache{})
}
//nolint:revive
func (c *authorizeCache) Load(subject Subject, action Action, object Object) (error, bool) {
if c == nil {
return nil, false
}
c.Lock()
defer c.Unlock()
for _, call := range c.calls {
if call.Action == action && call.Object.Equal(object) && call.Actor.Equal(subject) {
return call.Err, true
}
}
return nil, false
}
func (c *authorizeCache) Save(subject Subject, action Action, object Object, err error) {
if c == nil {
return
}
c.Lock()
defer c.Unlock()
c.calls = append(c.calls, cachedAuthCall{
AuthCall: AuthCall{
Actor: subject,
Action: action,
Object: object,
},
Err: err,
})
}
// rbacTraceAttributes are the attributes that are added to all spans created by // rbacTraceAttributes are the attributes that are added to all spans created by
// the rbac package. These attributes should help to debug slow spans. // the rbac package. These attributes should help to debug slow spans.
func rbacTraceAttributes(actor Subject, action Action, objectType string, extra ...attribute.KeyValue) trace.SpanStartOption { func rbacTraceAttributes(actor Subject, action Action, objectType string, extra ...attribute.KeyValue) trace.SpanStartOption {

View File

@ -282,27 +282,29 @@ func benchmarkSetup(orgs []uuid.UUID, users []uuid.UUID, size int, opts ...func(
return objectList return objectList
} }
// BenchmarkCacher benchmarks the performance of the cacher with a given // BenchmarkCacher benchmarks the performance of the cacher.
// cache size. The expected cache size in prod will usually be 1-2. In Filter
// cases it can get as high as 10.
func BenchmarkCacher(b *testing.B) { func BenchmarkCacher(b *testing.B) {
b.ResetTimer() ctx := context.Background()
// Size of the cache. authz := rbac.Cacher(&coderdtest.FakeAuthorizer{AlwaysReturn: nil})
sizes := []int{1, 10, 100, 1000}
for _, size := range sizes {
b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) {
ctx := rbac.WithCacheCtx(context.Background())
authz := rbac.Cacher(&coderdtest.FakeAuthorizer{AlwaysReturn: nil})
for i := 0; i < size; i++ {
// Preload the cache of a given size
subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
_ = authz.Authorize(ctx, subj, action, obj)
}
// Cache is loaded as a slice, so this cache hit is always the last element. rats := []int{1, 10, 100}
subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
b.ResetTimer() for _, rat := range rats {
b.Run(fmt.Sprintf("%v:1", rat), func(b *testing.B) {
b.ReportAllocs()
var (
subj rbac.Subject
obj rbac.Object
action rbac.Action
)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
if i%rat == 0 {
// Cache miss
b.StopTimer()
subj, obj, action = coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
b.StartTimer()
}
_ = authz.Authorize(ctx, subj, action, obj) _ = authz.Authorize(ctx, subj, action, obj)
} }
}) })
@ -312,29 +314,28 @@ func BenchmarkCacher(b *testing.B) {
func TestCacher(t *testing.T) { func TestCacher(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("EmptyCacheCtx", func(t *testing.T) { t.Run("NoCache", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx := context.Background() ctx := context.Background()
rec := &coderdtest.RecordingAuthorizer{ rec := &coderdtest.RecordingAuthorizer{
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil}, Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
} }
authz := rbac.Cacher(rec)
subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
// Two identical calls // Two identical calls
_ = authz.Authorize(ctx, subj, action, obj) _ = rec.Authorize(ctx, subj, action, obj)
_ = authz.Authorize(ctx, subj, action, obj) _ = rec.Authorize(ctx, subj, action, obj)
// Yields two calls to the wrapped Authorizer // Yields two calls to the wrapped Authorizer
rec.AssertActor(t, subj, rec.Pair(action, obj), rec.Pair(action, obj)) rec.AssertActor(t, subj, rec.Pair(action, obj), rec.Pair(action, obj))
require.NoError(t, rec.AllAsserted(), "all assertions should have been made") require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
}) })
t.Run("CacheCtx", func(t *testing.T) { t.Run("Cache", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx := rbac.WithCacheCtx(context.Background()) ctx := context.Background()
rec := &coderdtest.RecordingAuthorizer{ rec := &coderdtest.RecordingAuthorizer{
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil}, Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
} }
@ -353,7 +354,7 @@ func TestCacher(t *testing.T) {
t.Run("MultipleSubjects", func(t *testing.T) { t.Run("MultipleSubjects", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx := rbac.WithCacheCtx(context.Background()) ctx := context.Background()
rec := &coderdtest.RecordingAuthorizer{ rec := &coderdtest.RecordingAuthorizer{
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil}, Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
} }

View File

@ -1671,3 +1671,18 @@ func sortUsers(users []codersdk.User) {
return users[i].CreatedAt.Before(users[j].CreatedAt) return users[i].CreatedAt.Before(users[j].CreatedAt)
}) })
} }
func BenchmarkUsersMe(b *testing.B) {
client := coderdtest.New(b, nil)
_ = coderdtest.CreateFirstUser(b, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := client.User(ctx, codersdk.Me)
require.NoError(b, err)
}
}

3
go.mod
View File

@ -174,6 +174,8 @@ require (
tailscale.com v1.32.2 tailscale.com v1.32.2
) )
require github.com/armon/go-radix v1.0.0 // indirect
require ( require (
cloud.google.com/go/compute v1.18.0 // indirect cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/logging v1.6.1 // indirect cloud.google.com/go/logging v1.6.1 // indirect
@ -189,6 +191,7 @@ require (
github.com/akutz/memconn v0.1.0 // indirect github.com/akutz/memconn v0.1.0 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect github.com/alecthomas/chroma v0.10.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
github.com/ammario/tlru v0.3.0
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/aymanbagabas/go-osc52 v1.2.1 // indirect github.com/aymanbagabas/go-osc52 v1.2.1 // indirect

4
go.sum
View File

@ -170,6 +170,8 @@ github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3Uu
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk=
github.com/ammario/tlru v0.3.0 h1:yK8ESoFlEyz/BVVL8yZQKAUzJwFJR/j9EfxjnKxtR/Q=
github.com/ammario/tlru v0.3.0/go.mod h1:aYzRFu0XLo4KavE9W8Lx7tzjkX+pAApz+NgcKYIFUBQ=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@ -191,6 +193,8 @@ github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hC
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E=

View File

@ -56,6 +56,7 @@ stats/
# direnv # direnv
.envrc .envrc
*.test
# .prettierignore.include: # .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted # Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier. # by Prettier.

View File

@ -56,6 +56,7 @@ stats/
# direnv # direnv
.envrc .envrc
*.test
# .prettierignore.include: # .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted # Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier. # by Prettier.