Skip to content

Commit cc346af

Browse files
authored
Use licenses to populate the Entitlements API (#3715)
* Use licenses for entitlements API Signed-off-by: Spike Curtis <spike@coder.com> * Tests for entitlements API Signed-off-by: Spike Curtis <spike@coder.com> * Add commentary about FeatureService Signed-off-by: Spike Curtis <spike@coder.com> * Lint Signed-off-by: Spike Curtis <spike@coder.com> * Quiet down the logs Signed-off-by: Spike Curtis <spike@coder.com> * Tell revive it's ok Signed-off-by: Spike Curtis <spike@coder.com> Signed-off-by: Spike Curtis <spike@coder.com>
1 parent 05f932b commit cc346af

14 files changed

+773
-10
lines changed

cli/features.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"strings"
@@ -53,12 +54,14 @@ func featuresList() *cobra.Command {
5354
return xerrors.Errorf("render table: %w", err)
5455
}
5556
case "json":
56-
outBytes, err := json.Marshal(entitlements)
57+
buf := new(bytes.Buffer)
58+
enc := json.NewEncoder(buf)
59+
enc.SetIndent("", " ")
60+
err = enc.Encode(entitlements)
5761
if err != nil {
5862
return xerrors.Errorf("marshal features to JSON: %w", err)
5963
}
60-
61-
out = string(outBytes)
64+
out = buf.String()
6265
default:
6366
return xerrors.Errorf(`unknown output format %q, only "table" and "json" are supported`, outputFormat)
6467
}

coderd/coderd.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type Options struct {
6666
TracerProvider *sdktrace.TracerProvider
6767
AutoImportTemplates []AutoImportTemplate
6868
LicenseHandler http.Handler
69+
FeaturesService FeaturesService
6970
}
7071

7172
// New constructs a Coder API handler.
@@ -95,6 +96,9 @@ func New(options *Options) *API {
9596
if options.LicenseHandler == nil {
9697
options.LicenseHandler = licenses()
9798
}
99+
if options.FeaturesService == nil {
100+
options.FeaturesService = featuresService{}
101+
}
98102

99103
siteCacheDir := options.CacheDir
100104
if siteCacheDir != "" {
@@ -400,7 +404,7 @@ func New(options *Options) *API {
400404
})
401405
r.Route("/entitlements", func(r chi.Router) {
402406
r.Use(apiKeyMiddleware)
403-
r.Get("/", entitlements)
407+
r.Get("/", api.FeaturesService.EntitlementsAPI)
404408
})
405409
r.Route("/licenses", func(r chi.Router) {
406410
r.Use(apiKeyMiddleware)

coderd/database/databasefake/databasefake.go

+28
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,19 @@ func (q *fakeQuerier) GetUserCount(_ context.Context) (int64, error) {
246246
return int64(len(q.users)), nil
247247
}
248248

249+
func (q *fakeQuerier) GetActiveUserCount(_ context.Context) (int64, error) {
250+
q.mutex.RLock()
251+
defer q.mutex.RUnlock()
252+
253+
active := int64(0)
254+
for _, u := range q.users {
255+
if u.Status == database.UserStatusActive {
256+
active++
257+
}
258+
}
259+
return active, nil
260+
}
261+
249262
func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams) ([]database.User, error) {
250263
q.mutex.RLock()
251264
defer q.mutex.RUnlock()
@@ -2322,6 +2335,21 @@ func (q *fakeQuerier) GetLicenses(_ context.Context) ([]database.License, error)
23222335
return results, nil
23232336
}
23242337

2338+
func (q *fakeQuerier) GetUnexpiredLicenses(_ context.Context) ([]database.License, error) {
2339+
q.mutex.RLock()
2340+
defer q.mutex.RUnlock()
2341+
2342+
now := time.Now()
2343+
var results []database.License
2344+
for _, l := range q.licenses {
2345+
if l.Exp.After(now) {
2346+
results = append(results, l)
2347+
}
2348+
}
2349+
sort.Slice(results, func(i, j int) bool { return results[i].ID < results[j].ID })
2350+
return results, nil
2351+
}
2352+
23252353
func (q *fakeQuerier) DeleteLicense(_ context.Context, id int32) (int32, error) {
23262354
q.mutex.Lock()
23272355
defer q.mutex.Unlock()

coderd/database/querier.go

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+51
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/licenses.sql

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ SELECT *
1313
FROM licenses
1414
ORDER BY (id);
1515

16+
-- name: GetUnexpiredLicenses :many
17+
SELECT *
18+
FROM licenses
19+
WHERE exp > NOW()
20+
ORDER BY (id);
21+
1622
-- name: DeleteLicense :one
1723
DELETE
1824
FROM licenses

coderd/database/queries/users.sql

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ SELECT
2828
FROM
2929
users;
3030

31+
-- name: GetActiveUserCount :one
32+
SELECT
33+
COUNT(*)
34+
FROM
35+
users
36+
WHERE
37+
status = 'active'::public.user_status;
38+
3139
-- name: InsertUser :one
3240
INSERT INTO
3341
users (

coderd/features.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,20 @@ import (
77
"github.com/coder/coder/codersdk"
88
)
99

10-
func entitlements(rw http.ResponseWriter, _ *http.Request) {
10+
// FeaturesService is the interface for interacting with enterprise features.
11+
type FeaturesService interface {
12+
EntitlementsAPI(w http.ResponseWriter, r *http.Request)
13+
14+
// TODO
15+
// Get returns the implementations for feature interfaces. Parameter `s `must be a pointer to a
16+
// struct type containing feature interfaces as fields. The FeatureService sets all fields to
17+
// the correct implementations depending on whether the features are turned on.
18+
// Get(s any) error
19+
}
20+
21+
type featuresService struct{}
22+
23+
func (featuresService) EntitlementsAPI(rw http.ResponseWriter, _ *http.Request) {
1124
features := make(map[string]codersdk.Feature)
1225
for _, f := range codersdk.FeatureNames {
1326
features[f] = codersdk.Feature{

coderd/features_internal_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestEntitlements(t *testing.T) {
1818
t.Parallel()
1919
r := httptest.NewRequest("GET", "https://example.com/api/v2/entitlements", nil)
2020
rw := httptest.NewRecorder()
21-
entitlements(rw, r)
21+
featuresService{}.EntitlementsAPI(rw, r)
2222
resp := rw.Result()
2323
defer resp.Body.Close()
2424
assert.Equal(t, http.StatusOK, resp.StatusCode)

codersdk/features.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog}
2424
type Feature struct {
2525
Entitlement Entitlement `json:"entitlement"`
2626
Enabled bool `json:"enabled"`
27-
Limit *int64 `json:"limit"`
28-
Actual *int64 `json:"actual"`
27+
Limit *int64 `json:"limit,omitempty"`
28+
Actual *int64 `json:"actual,omitempty"`
2929
}
3030

3131
type Entitlements struct {

enterprise/coderd/coderd.go

+19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package coderd
22

33
import (
4+
"context"
5+
"os"
6+
"strings"
7+
48
"golang.org/x/xerrors"
59

610
"github.com/coder/coder/coderd"
711
"github.com/coder/coder/coderd/rbac"
812
)
913

14+
const EnvAuditLogEnable = "CODER_AUDIT_LOG_ENABLE"
15+
1016
func NewEnterprise(options *coderd.Options) *coderd.API {
1117
var eOpts = *options
1218
if eOpts.Authorizer == nil {
@@ -26,5 +32,18 @@ func NewEnterprise(options *coderd.Options) *coderd.API {
2632
Authorizer: eOpts.Authorizer,
2733
Logger: eOpts.Logger,
2834
}).handler()
35+
en := Enablements{AuditLogs: true}
36+
auditLog := os.Getenv(EnvAuditLogEnable)
37+
auditLog = strings.ToLower(auditLog)
38+
if auditLog == "disable" || auditLog == "false" || auditLog == "0" || auditLog == "no" {
39+
en.AuditLogs = false
40+
}
41+
eOpts.FeaturesService = newFeaturesService(
42+
context.Background(),
43+
eOpts.Logger,
44+
eOpts.Database,
45+
eOpts.Pubsub,
46+
en,
47+
)
2948
return coderd.New(&eOpts)
3049
}

0 commit comments

Comments
 (0)