Skip to content

Commit 4527037

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 ccb5b4d commit 4527037

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
@@ -34,6 +34,23 @@ const (
3434
EntitlementNotEntitled Entitlement = "not_entitled"
3535
)
3636

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

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

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

0 commit comments

Comments
 (0)