Skip to content

Commit a7cdec5

Browse files
authored
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>
1 parent 1b6f9e5 commit a7cdec5

File tree

4 files changed

+402
-11
lines changed

4 files changed

+402
-11
lines changed

coderd/features.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package coderd
22

33
import (
44
"net/http"
5+
"reflect"
56

7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/coderd/audit"
610
"github.com/coder/coder/coderd/httpapi"
711
"github.com/coder/coder/codersdk"
812
)
@@ -11,11 +15,10 @@ import (
1115
type FeaturesService interface {
1216
EntitlementsAPI(w http.ResponseWriter, r *http.Request)
1317

14-
// TODO
15-
// Get returns the implementations for feature interfaces. Parameter `s `must be a pointer to a
18+
// Get returns the implementations for feature interfaces. Parameter `s` must be a pointer to a
1619
// struct type containing feature interfaces as fields. The FeatureService sets all fields to
1720
// the correct implementations depending on whether the features are turned on.
18-
// Get(s any) error
21+
Get(s any) error
1922
}
2023

2124
type featuresService struct{}
@@ -34,3 +37,57 @@ func (featuresService) EntitlementsAPI(rw http.ResponseWriter, _ *http.Request)
3437
HasLicense: false,
3538
})
3639
}
40+
41+
// Get returns the implementations for feature interfaces. Parameter `s` must be a pointer to a
42+
// struct type containing feature interfaces as fields. The AGPL featureService always returns the
43+
// "disabled" version of the feature interface because it doesn't include any enterprise features
44+
// by definition.
45+
func (featuresService) Get(ps any) error {
46+
if reflect.TypeOf(ps).Kind() != reflect.Pointer {
47+
return xerrors.New("input must be pointer to struct")
48+
}
49+
vs := reflect.ValueOf(ps).Elem()
50+
if vs.Kind() != reflect.Struct {
51+
return xerrors.New("input must be pointer to struct")
52+
}
53+
for i := 0; i < vs.NumField(); i++ {
54+
vf := vs.Field(i)
55+
tf := vf.Type()
56+
if tf.Kind() != reflect.Interface {
57+
return xerrors.Errorf("fields of input struct must be interfaces: %s", tf.String())
58+
}
59+
err := setImplementation(vf, tf)
60+
if err != nil {
61+
return err
62+
}
63+
}
64+
return nil
65+
}
66+
67+
// setImplementation finds the correct implementation for the field's type, and sets it on the
68+
// struct. It returns an error if unsuccessful
69+
func setImplementation(vf reflect.Value, tf reflect.Type) error {
70+
// when we get more than a few features it might make sense to have a data structure for finding
71+
// the correct implementation that's faster than just a linear search, but for now just spin
72+
// through the implementations we have.
73+
vd := reflect.ValueOf(DisabledImplementations)
74+
for j := 0; j < vd.NumField(); j++ {
75+
vdf := vd.Field(j)
76+
if vdf.Type() == tf {
77+
vf.Set(vdf)
78+
return nil
79+
}
80+
}
81+
return xerrors.Errorf("unable to find implementation of interface %s", tf.String())
82+
}
83+
84+
// FeatureInterfaces contains a field for each interface controlled by an enterprise feature.
85+
type FeatureInterfaces struct {
86+
Auditor audit.Auditor
87+
}
88+
89+
// DisabledImplementations includes all the implementations of turned-off features. There are no
90+
// turned-on implementations in AGPL code.
91+
var DisabledImplementations = FeatureInterfaces{
92+
Auditor: audit.NewNop(),
93+
}

coderd/features_internal_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/stretchr/testify/assert"
1010
"github.com/stretchr/testify/require"
1111

12+
"github.com/coder/coder/coderd/audit"
1213
"github.com/coder/coder/codersdk"
1314
)
1415

@@ -36,3 +37,64 @@ func TestEntitlements(t *testing.T) {
3637
}
3738
})
3839
}
40+
41+
func TestFeaturesServiceGet(t *testing.T) {
42+
t.Parallel()
43+
t.Run("Auditor", func(t *testing.T) {
44+
t.Parallel()
45+
uut := featuresService{}
46+
target := struct {
47+
Auditor audit.Auditor
48+
}{}
49+
err := uut.Get(&target)
50+
require.NoError(t, err)
51+
assert.NotNil(t, target.Auditor)
52+
})
53+
54+
t.Run("NotPointer", func(t *testing.T) {
55+
t.Parallel()
56+
uut := featuresService{}
57+
target := struct {
58+
Auditor audit.Auditor
59+
}{}
60+
err := uut.Get(target)
61+
require.Error(t, err)
62+
assert.Nil(t, target.Auditor)
63+
})
64+
65+
t.Run("UnknownInterface", func(t *testing.T) {
66+
t.Parallel()
67+
uut := featuresService{}
68+
target := struct {
69+
test testInterface
70+
}{}
71+
err := uut.Get(&target)
72+
require.Error(t, err)
73+
assert.Nil(t, target.test)
74+
})
75+
76+
t.Run("PointerToNonStruct", func(t *testing.T) {
77+
t.Parallel()
78+
uut := featuresService{}
79+
var target audit.Auditor
80+
err := uut.Get(&target)
81+
require.Error(t, err)
82+
assert.Nil(t, target)
83+
})
84+
85+
t.Run("StructWithNonInterfaces", func(t *testing.T) {
86+
t.Parallel()
87+
uut := featuresService{}
88+
target := struct {
89+
N int64
90+
Auditor audit.Auditor
91+
}{}
92+
err := uut.Get(&target)
93+
require.Error(t, err)
94+
assert.Nil(t, target.Auditor)
95+
})
96+
}
97+
98+
type testInterface interface {
99+
Test() error
100+
}

enterprise/coderd/features.go

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@ import (
55
"crypto/ed25519"
66
"fmt"
77
"net/http"
8+
"reflect"
89
"sync"
910
"time"
1011

12+
"github.com/coder/coder/enterprise/audit/backends"
13+
1114
"github.com/cenkalti/backoff/v4"
15+
"golang.org/x/xerrors"
1216

1317
"cdr.dev/slog"
1418

1519
agpl "github.com/coder/coder/coderd"
20+
agplAudit "github.com/coder/coder/coderd/audit"
1621
"github.com/coder/coder/coderd/database"
1722
"github.com/coder/coder/coderd/httpapi"
1823
"github.com/coder/coder/codersdk"
24+
"github.com/coder/coder/enterprise/audit"
1925
)
2026

2127
type Enablements struct {
@@ -29,6 +35,13 @@ type featuresService struct {
2935
keys map[string]ed25519.PublicKey
3036
enablements Enablements
3137
resyncInterval time.Duration
38+
// enabledImplementations includes an "enabled" implementation of every feature. This is
39+
// initialized at start of day and remains static. The consequence of this is that these things
40+
// are hanging around using memory even if not licensed or in use, but it greatly simplifies the
41+
// logic because we don't have to bother creating and destroying them as entitlements change.
42+
// If we have a particularly memory-hungry feature in future, we might wish to reconsider this
43+
// choice.
44+
enabledImplementations agpl.FeatureInterfaces
3245

3346
mu sync.RWMutex
3447
entitlements entitlements
@@ -44,11 +57,18 @@ func newFeaturesService(
4457
enablements Enablements,
4558
) agpl.FeaturesService {
4659
fs := &featuresService{
47-
logger: logger,
48-
database: db,
49-
pubsub: pubsub,
50-
keys: keys,
51-
enablements: enablements,
60+
logger: logger,
61+
database: db,
62+
pubsub: pubsub,
63+
keys: keys,
64+
enablements: enablements,
65+
enabledImplementations: agpl.FeatureInterfaces{
66+
Auditor: audit.NewAuditor(
67+
audit.DefaultFilter,
68+
backends.NewPostgres(db, true),
69+
backends.NewSlog(logger),
70+
),
71+
},
5272
resyncInterval: 10 * time.Minute,
5373
entitlements: entitlements{
5474
activeUsers: numericalEntitlement{
@@ -259,3 +279,48 @@ func max(a, b int64) int64 {
259279
}
260280
return b
261281
}
282+
283+
func (s *featuresService) Get(ps any) error {
284+
if reflect.TypeOf(ps).Kind() != reflect.Pointer {
285+
return xerrors.New("input must be pointer to struct")
286+
}
287+
vs := reflect.ValueOf(ps).Elem()
288+
if vs.Kind() != reflect.Struct {
289+
return xerrors.New("input must be pointer to struct")
290+
}
291+
// grab a local copy of entitlements so that we have a consistent set, but aren't keeping it
292+
// locked from updates while we process.
293+
s.mu.RLock()
294+
ent := s.entitlements
295+
s.mu.RUnlock()
296+
297+
for i := 0; i < vs.NumField(); i++ {
298+
vf := vs.Field(i)
299+
tf := vf.Type()
300+
if tf.Kind() != reflect.Interface {
301+
return xerrors.Errorf("fields of input struct must be interfaces: %s", tf.String())
302+
}
303+
304+
err := s.setImplementation(ent, vf, tf)
305+
if err != nil {
306+
return err
307+
}
308+
}
309+
return nil
310+
}
311+
312+
func (s *featuresService) setImplementation(ent entitlements, vf reflect.Value, tf reflect.Type) error {
313+
// c.f. https://stackoverflow.com/questions/7132848/how-to-get-the-reflect-type-of-an-interface
314+
switch tf {
315+
case reflect.TypeOf((*agplAudit.Auditor)(nil)).Elem():
316+
// Audit logging
317+
if !s.enablements.AuditLogs || ent.auditLogs.state == notEntitled {
318+
vf.Set(reflect.ValueOf(agpl.DisabledImplementations.Auditor))
319+
return nil
320+
}
321+
vf.Set(reflect.ValueOf(s.enabledImplementations.Auditor))
322+
return nil
323+
default:
324+
return xerrors.Errorf("unable to find implementation of interface %s", tf.String())
325+
}
326+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy