Feature server implementation (#3899)

* Feature server implementation

Signed-off-by: Spike Curtis <spike@coder.com>

* Fix imports

Signed-off-by: Spike Curtis <spike@coder.com>

Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
Spike Curtis
2022-09-06 11:59:10 -07:00
committed by GitHub
parent 1b6f9e54a3
commit a7cdec5d39
4 changed files with 402 additions and 11 deletions

View File

@ -5,17 +5,23 @@ import (
"crypto/ed25519"
"fmt"
"net/http"
"reflect"
"sync"
"time"
"github.com/coder/coder/enterprise/audit/backends"
"github.com/cenkalti/backoff/v4"
"golang.org/x/xerrors"
"cdr.dev/slog"
agpl "github.com/coder/coder/coderd"
agplAudit "github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/audit"
)
type Enablements struct {
@ -29,6 +35,13 @@ type featuresService struct {
keys map[string]ed25519.PublicKey
enablements Enablements
resyncInterval time.Duration
// enabledImplementations includes an "enabled" implementation of every feature. This is
// initialized at start of day and remains static. The consequence of this is that these things
// are hanging around using memory even if not licensed or in use, but it greatly simplifies the
// logic because we don't have to bother creating and destroying them as entitlements change.
// If we have a particularly memory-hungry feature in future, we might wish to reconsider this
// choice.
enabledImplementations agpl.FeatureInterfaces
mu sync.RWMutex
entitlements entitlements
@ -44,11 +57,18 @@ func newFeaturesService(
enablements Enablements,
) agpl.FeaturesService {
fs := &featuresService{
logger: logger,
database: db,
pubsub: pubsub,
keys: keys,
enablements: enablements,
logger: logger,
database: db,
pubsub: pubsub,
keys: keys,
enablements: enablements,
enabledImplementations: agpl.FeatureInterfaces{
Auditor: audit.NewAuditor(
audit.DefaultFilter,
backends.NewPostgres(db, true),
backends.NewSlog(logger),
),
},
resyncInterval: 10 * time.Minute,
entitlements: entitlements{
activeUsers: numericalEntitlement{
@ -259,3 +279,48 @@ func max(a, b int64) int64 {
}
return b
}
func (s *featuresService) Get(ps any) error {
if reflect.TypeOf(ps).Kind() != reflect.Pointer {
return xerrors.New("input must be pointer to struct")
}
vs := reflect.ValueOf(ps).Elem()
if vs.Kind() != reflect.Struct {
return xerrors.New("input must be pointer to struct")
}
// grab a local copy of entitlements so that we have a consistent set, but aren't keeping it
// locked from updates while we process.
s.mu.RLock()
ent := s.entitlements
s.mu.RUnlock()
for i := 0; i < vs.NumField(); i++ {
vf := vs.Field(i)
tf := vf.Type()
if tf.Kind() != reflect.Interface {
return xerrors.Errorf("fields of input struct must be interfaces: %s", tf.String())
}
err := s.setImplementation(ent, vf, tf)
if err != nil {
return err
}
}
return nil
}
func (s *featuresService) setImplementation(ent entitlements, vf reflect.Value, tf reflect.Type) error {
// c.f. https://stackoverflow.com/questions/7132848/how-to-get-the-reflect-type-of-an-interface
switch tf {
case reflect.TypeOf((*agplAudit.Auditor)(nil)).Elem():
// Audit logging
if !s.enablements.AuditLogs || ent.auditLogs.state == notEntitled {
vf.Set(reflect.ValueOf(agpl.DisabledImplementations.Auditor))
return nil
}
vf.Set(reflect.ValueOf(s.enabledImplementations.Auditor))
return nil
default:
return xerrors.Errorf("unable to find implementation of interface %s", tf.String())
}
}