Skip to content

Commit cae444b

Browse files
committed
chore: implement feature sets to licenses
Implement different sets of licensed features. Refactor the license logic to fix up some edge cases
1 parent 1f24ace commit cae444b

File tree

3 files changed

+274
-104
lines changed

3 files changed

+274
-104
lines changed

codersdk/deployment.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ const (
3333
EntitlementNotEntitled Entitlement = "not_entitled"
3434
)
3535

36+
func CompareEntitlements(a, b Entitlement) int {
37+
return entitlementWeight(a) - entitlementWeight(b)
38+
}
39+
40+
func entitlementWeight(e Entitlement) int {
41+
switch e {
42+
case EntitlementEntitled:
43+
return 2
44+
case EntitlementGracePeriod:
45+
return 1
46+
case EntitlementNotEntitled:
47+
return 0
48+
default:
49+
return -1
50+
}
51+
}
52+
3653
// FeatureName represents the internal name of a feature.
3754
// To add a new feature, add it to this set of enums as well as the FeatureNames
3855
// array below.
@@ -107,6 +124,51 @@ func (n FeatureName) AlwaysEnable() bool {
107124
}[n]
108125
}
109126

127+
// FeatureSet represents a grouping of features. This is easier
128+
// than manually assigning features al-la-carte when making a license.
129+
// These sets are dynamic in the sense a feature can be added to an existing
130+
// set to grant an additional feature to an existing license.
131+
// If features were granted al-la-carte, we would need to reissue the license
132+
// to include the new feature.
133+
type FeatureSet string
134+
135+
const (
136+
FeatureSetNone FeatureSet = ""
137+
FeatureSetEnterprise FeatureSet = "enterprise"
138+
FeatureSetPremium FeatureSet = "premium"
139+
)
140+
141+
func (set FeatureSet) Features() []FeatureName {
142+
switch FeatureSet(strings.ToLower(string(set))) {
143+
case FeatureSetEnterprise:
144+
// List all features that should be included in the Enterprise feature set.
145+
return []FeatureName{
146+
FeatureUserLimit,
147+
FeatureAuditLog,
148+
FeatureBrowserOnly,
149+
FeatureSCIM,
150+
FeatureTemplateRBAC,
151+
FeatureHighAvailability,
152+
FeatureMultipleExternalAuth,
153+
FeatureExternalProvisionerDaemons,
154+
FeatureAppearance,
155+
FeatureAdvancedTemplateScheduling,
156+
FeatureWorkspaceProxy,
157+
FeatureUserRoleManagement,
158+
FeatureExternalTokenEncryption,
159+
FeatureWorkspaceBatchActions,
160+
FeatureAccessControl,
161+
FeatureControlSharedPorts,
162+
FeatureCustomRoles,
163+
}
164+
case FeatureSetPremium:
165+
// FeatureSetPremium is a superset of Enterprise
166+
return append(FeatureSetEnterprise.Features())
167+
}
168+
// By default, return an empty set.
169+
return []FeatureName{}
170+
}
171+
110172
type Feature struct {
111173
Entitlement Entitlement `json:"entitlement"`
112174
Enabled bool `json:"enabled"`
@@ -124,6 +186,56 @@ type Entitlements struct {
124186
RefreshedAt time.Time `json:"refreshed_at" format:"date-time"`
125187
}
126188

189+
// AddFeature will add the feature to the entitlements iff it expands
190+
// the set of features granted by the entitlements. If it does not, it will
191+
// be ignored and the existing feature with the same name will remain.
192+
//
193+
// All features should be added as atomic items, and not merged in any way.
194+
// Merging entitlements could lead to unexpected behavior, like a larger user
195+
// limit in grace period merging with a smaller one in a grace period. This could
196+
// lead to the larger limit being extended as "entitled", which is not correct.
197+
func (e *Entitlements) AddFeature(name FeatureName, add Feature) {
198+
existing, ok := e.Features[name]
199+
if !ok {
200+
e.Features[name] = add
201+
return
202+
}
203+
204+
comparison := CompareEntitlements(add.Entitlement, existing.Entitlement)
205+
// If the new entitlement is greater than the existing entitlement, replace it.
206+
// The edge case is if the previous entitlement is in a grace period with a
207+
// higher value.
208+
// TODO: Address the edge case.
209+
if comparison > 0 {
210+
e.Features[name] = add
211+
return
212+
}
213+
214+
// If they have the same entitlement, then we can compare the limits.
215+
if comparison == 0 {
216+
if add.Limit != nil {
217+
if existing.Limit == nil || *add.Limit > *existing.Limit {
218+
e.Features[name] = add
219+
return
220+
}
221+
}
222+
223+
// Enabled is better than disabled.
224+
if add.Enabled && !existing.Enabled {
225+
e.Features[name] = add
226+
return
227+
}
228+
229+
// If the actual value is greater than the existing actual value, replace it.
230+
if add.Actual != nil {
231+
if existing.Actual == nil || *add.Actual > *existing.Actual {
232+
e.Features[name] = add
233+
return
234+
}
235+
}
236+
}
237+
}
238+
127239
func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) {
128240
res, err := c.Request(ctx, http.MethodGet, "/api/v2/entitlements", nil)
129241
if err != nil {

0 commit comments

Comments
 (0)