Skip to content

Commit 15fda23

Browse files
authored
feat: implement premium vs enterprise licenses (coder#13907)
* feat: implement premium vs enterprise licenses Implement different sets of licensed features.
1 parent 0d9615b commit 15fda23

File tree

8 files changed

+971
-150
lines changed

8 files changed

+971
-150
lines changed

codersdk/deployment.go

Lines changed: 172 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"path/filepath"
1111
"reflect"
12+
"slices"
1213
"strconv"
1314
"strings"
1415
"time"
@@ -34,6 +35,21 @@ const (
3435
EntitlementNotEntitled Entitlement = "not_entitled"
3536
)
3637

38+
// Weight converts the enum types to a numerical value for easier
39+
// comparisons. Easier than sets of if statements.
40+
func (e Entitlement) Weight() int {
41+
switch e {
42+
case EntitlementEntitled:
43+
return 2
44+
case EntitlementGracePeriod:
45+
return 1
46+
case EntitlementNotEntitled:
47+
return -1
48+
default:
49+
return -2
50+
}
51+
}
52+
3753
// FeatureName represents the internal name of a feature.
3854
// To add a new feature, add it to this set of enums as well as the FeatureNames
3955
// array below.
@@ -95,8 +111,11 @@ func (n FeatureName) Humanize() string {
95111
}
96112

97113
// AlwaysEnable returns if the feature is always enabled if entitled.
98-
// Warning: We don't know if we need this functionality.
99-
// This method may disappear at any time.
114+
// This is required because some features are only enabled if they are entitled
115+
// and not required.
116+
// E.g: "multiple-organizations" is disabled by default in AGPL and enterprise
117+
// deployments. This feature should only be enabled for premium deployments
118+
// when it is entitled.
100119
func (n FeatureName) AlwaysEnable() bool {
101120
return map[FeatureName]bool{
102121
FeatureMultipleExternalAuth: true,
@@ -105,16 +124,144 @@ func (n FeatureName) AlwaysEnable() bool {
105124
FeatureWorkspaceBatchActions: true,
106125
FeatureHighAvailability: true,
107126
FeatureCustomRoles: true,
127+
FeatureMultipleOrganizations: true,
108128
}[n]
109129
}
110130

131+
// FeatureSet represents a grouping of features. Rather than manually
132+
// assigning features al-la-carte when making a license, a set can be specified.
133+
// Sets are dynamic in the sense a feature can be added to a set, granting the
134+
// feature to existing licenses out in the wild.
135+
// If features were granted al-la-carte, we would need to reissue the existing
136+
// old licenses to include the new feature.
137+
type FeatureSet string
138+
139+
const (
140+
FeatureSetNone FeatureSet = ""
141+
FeatureSetEnterprise FeatureSet = "enterprise"
142+
FeatureSetPremium FeatureSet = "premium"
143+
)
144+
145+
func (set FeatureSet) Features() []FeatureName {
146+
switch FeatureSet(strings.ToLower(string(set))) {
147+
case FeatureSetEnterprise:
148+
// Enterprise is the set 'AllFeatures' minus some select features.
149+
150+
// Copy the list of all features
151+
enterpriseFeatures := make([]FeatureName, len(FeatureNames))
152+
copy(enterpriseFeatures, FeatureNames)
153+
// Remove the selection
154+
enterpriseFeatures = slices.DeleteFunc(enterpriseFeatures, func(f FeatureName) bool {
155+
switch f {
156+
// Add all features that should be excluded in the Enterprise feature set.
157+
case FeatureMultipleOrganizations:
158+
return true
159+
default:
160+
return false
161+
}
162+
})
163+
164+
return enterpriseFeatures
165+
case FeatureSetPremium:
166+
premiumFeatures := make([]FeatureName, len(FeatureNames))
167+
copy(premiumFeatures, FeatureNames)
168+
// FeatureSetPremium is just all features.
169+
return premiumFeatures
170+
}
171+
// By default, return an empty set.
172+
return []FeatureName{}
173+
}
174+
111175
type Feature struct {
112176
Entitlement Entitlement `json:"entitlement"`
113177
Enabled bool `json:"enabled"`
114178
Limit *int64 `json:"limit,omitempty"`
115179
Actual *int64 `json:"actual,omitempty"`
116180
}
117181

182+
// Compare compares two features and returns an integer representing
183+
// if the first feature (f) is greater than, equal to, or less than the second
184+
// feature (b). "Greater than" means the first feature has more functionality
185+
// than the second feature. It is assumed the features are for the same FeatureName.
186+
//
187+
// A feature is considered greater than another feature if:
188+
// 1. Graceful & capable > Entitled & not capable
189+
// 2. The entitlement is greater
190+
// 3. The limit is greater
191+
// 4. Enabled is greater than disabled
192+
// 5. The actual is greater
193+
func (f Feature) Compare(b Feature) int {
194+
if !f.Capable() || !b.Capable() {
195+
// If either is incapable, then it is possible a grace period
196+
// feature can be "greater" than an entitled.
197+
// If either is "NotEntitled" then we can defer to a strict entitlement
198+
// check.
199+
if f.Entitlement.Weight() >= 0 && b.Entitlement.Weight() >= 0 {
200+
if f.Capable() && !b.Capable() {
201+
return 1
202+
}
203+
if b.Capable() && !f.Capable() {
204+
return -1
205+
}
206+
}
207+
}
208+
209+
// Strict entitlement check. Higher is better
210+
entitlementDifference := f.Entitlement.Weight() - b.Entitlement.Weight()
211+
if entitlementDifference != 0 {
212+
return entitlementDifference
213+
}
214+
215+
// If the entitlement is the same, then we can compare the limits.
216+
if f.Limit == nil && b.Limit != nil {
217+
return -1
218+
}
219+
if f.Limit != nil && b.Limit == nil {
220+
return 1
221+
}
222+
if f.Limit != nil && b.Limit != nil {
223+
difference := *f.Limit - *b.Limit
224+
if difference != 0 {
225+
return int(difference)
226+
}
227+
}
228+
229+
// Enabled is better than disabled.
230+
if f.Enabled && !b.Enabled {
231+
return 1
232+
}
233+
if !f.Enabled && b.Enabled {
234+
return -1
235+
}
236+
237+
// Higher actual is better
238+
if f.Actual == nil && b.Actual != nil {
239+
return -1
240+
}
241+
if f.Actual != nil && b.Actual == nil {
242+
return 1
243+
}
244+
if f.Actual != nil && b.Actual != nil {
245+
difference := *f.Actual - *b.Actual
246+
if difference != 0 {
247+
return int(difference)
248+
}
249+
}
250+
251+
return 0
252+
}
253+
254+
// Capable is a helper function that returns if a given feature has a limit
255+
// that is greater than or equal to the actual.
256+
// If this condition is not true, then the feature is not capable of being used
257+
// since the limit is not high enough.
258+
func (f Feature) Capable() bool {
259+
if f.Limit != nil && f.Actual != nil {
260+
return *f.Limit >= *f.Actual
261+
}
262+
return true
263+
}
264+
118265
type Entitlements struct {
119266
Features map[FeatureName]Feature `json:"features"`
120267
Warnings []string `json:"warnings"`
@@ -125,6 +272,29 @@ type Entitlements struct {
125272
RefreshedAt time.Time `json:"refreshed_at" format:"date-time"`
126273
}
127274

275+
// AddFeature will add the feature to the entitlements iff it expands
276+
// the set of features granted by the entitlements. If it does not, it will
277+
// be ignored and the existing feature with the same name will remain.
278+
//
279+
// All features should be added as atomic items, and not merged in any way.
280+
// Merging entitlements could lead to unexpected behavior, like a larger user
281+
// limit in grace period merging with a smaller one in an "entitled" state. This
282+
// could lead to the larger limit being extended as "entitled", which is not correct.
283+
func (e *Entitlements) AddFeature(name FeatureName, add Feature) {
284+
existing, ok := e.Features[name]
285+
if !ok {
286+
e.Features[name] = add
287+
return
288+
}
289+
290+
// Compare the features, keep the one that is "better"
291+
comparison := add.Compare(existing)
292+
if comparison > 0 {
293+
e.Features[name] = add
294+
return
295+
}
296+
}
297+
128298
func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) {
129299
res, err := c.Request(ctx, http.MethodGet, "/api/v2/entitlements", nil)
130300
if err != nil {

0 commit comments

Comments
 (0)