Skip to content

Feature server implementation #3899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 6, 2022
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
63 changes: 60 additions & 3 deletions coderd/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package coderd

import (
"net/http"
"reflect"

"golang.org/x/xerrors"

"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
)
Expand All @@ -11,11 +15,10 @@ import (
type FeaturesService interface {
EntitlementsAPI(w http.ResponseWriter, r *http.Request)

// TODO
// Get returns the implementations for feature interfaces. Parameter `s `must be a pointer to a
// Get returns the implementations for feature interfaces. Parameter `s` must be a pointer to a
// struct type containing feature interfaces as fields. The FeatureService sets all fields to
// the correct implementations depending on whether the features are turned on.
// Get(s any) error
Get(s any) error
}

type featuresService struct{}
Expand All @@ -34,3 +37,57 @@ func (featuresService) EntitlementsAPI(rw http.ResponseWriter, _ *http.Request)
HasLicense: false,
})
}

// Get returns the implementations for feature interfaces. Parameter `s` must be a pointer to a
// struct type containing feature interfaces as fields. The AGPL featureService always returns the
// "disabled" version of the feature interface because it doesn't include any enterprise features
// by definition.
func (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")
}
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 := setImplementation(vf, tf)
if err != nil {
return err
}
}
return nil
}

// setImplementation finds the correct implementation for the field's type, and sets it on the
// struct. It returns an error if unsuccessful
func setImplementation(vf reflect.Value, tf reflect.Type) error {
// when we get more than a few features it might make sense to have a data structure for finding
// the correct implementation that's faster than just a linear search, but for now just spin
// through the implementations we have.
vd := reflect.ValueOf(DisabledImplementations)
for j := 0; j < vd.NumField(); j++ {
vdf := vd.Field(j)
if vdf.Type() == tf {
vf.Set(vdf)
return nil
}
}
return xerrors.Errorf("unable to find implementation of interface %s", tf.String())
}

// FeatureInterfaces contains a field for each interface controlled by an enterprise feature.
type FeatureInterfaces struct {
Auditor audit.Auditor
}

// DisabledImplementations includes all the implementations of turned-off features. There are no
// turned-on implementations in AGPL code.
var DisabledImplementations = FeatureInterfaces{
Auditor: audit.NewNop(),
}
62 changes: 62 additions & 0 deletions coderd/features_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/codersdk"
)

Expand Down Expand Up @@ -36,3 +37,64 @@ func TestEntitlements(t *testing.T) {
}
})
}

func TestFeaturesServiceGet(t *testing.T) {
t.Parallel()
t.Run("Auditor", func(t *testing.T) {
t.Parallel()
uut := featuresService{}
target := struct {
Auditor audit.Auditor
}{}
err := uut.Get(&target)
require.NoError(t, err)
assert.NotNil(t, target.Auditor)
})

t.Run("NotPointer", func(t *testing.T) {
t.Parallel()
uut := featuresService{}
target := struct {
Auditor audit.Auditor
}{}
err := uut.Get(target)
require.Error(t, err)
assert.Nil(t, target.Auditor)
})

t.Run("UnknownInterface", func(t *testing.T) {
t.Parallel()
uut := featuresService{}
target := struct {
test testInterface
}{}
err := uut.Get(&target)
require.Error(t, err)
assert.Nil(t, target.test)
})

t.Run("PointerToNonStruct", func(t *testing.T) {
t.Parallel()
uut := featuresService{}
var target audit.Auditor
err := uut.Get(&target)
require.Error(t, err)
assert.Nil(t, target)
})

t.Run("StructWithNonInterfaces", func(t *testing.T) {
t.Parallel()
uut := featuresService{}
target := struct {
N int64
Auditor audit.Auditor
}{}
err := uut.Get(&target)
require.Error(t, err)
assert.Nil(t, target.Auditor)
})
}

type testInterface interface {
Test() error
}
75 changes: 70 additions & 5 deletions enterprise/coderd/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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{
Expand Down Expand Up @@ -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())
}
}
Loading